1# Copyright: (c) 2018, Deric Crago <deric.crago@gmail.com> 2# Copyright: (c) 2018, Ansible Project 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import absolute_import, division, print_function 6__metaclass__ = type 7 8import re 9from os.path import exists, getsize 10from socket import gaierror 11from ssl import SSLError 12from time import sleep 13import traceback 14 15REQUESTS_IMP_ERR = None 16try: 17 import requests 18 HAS_REQUESTS = True 19except ImportError: 20 REQUESTS_IMP_ERR = traceback.format_exc() 21 HAS_REQUESTS = False 22 23try: 24 from requests.packages import urllib3 25 HAS_URLLIB3 = True 26except ImportError: 27 try: 28 import urllib3 29 HAS_URLLIB3 = True 30 except ImportError: 31 HAS_URLLIB3 = False 32 33from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleConnectionFailure 34from ansible.module_utils._text import to_bytes, to_native 35from ansible.plugins.connection import ConnectionBase 36from ansible.module_utils.basic import missing_required_lib 37 38try: 39 from pyVim.connect import Disconnect, SmartConnect, SmartConnectNoSSL 40 from pyVmomi import vim 41 42 HAS_PYVMOMI = True 43except ImportError: 44 HAS_PYVMOMI = False 45 PYVMOMI_IMP_ERR = traceback.format_exc() 46 47 48DOCUMENTATION = """ 49 author: Deric Crago <deric.crago@gmail.com> 50 connection: vmware_tools 51 short_description: Execute tasks inside a VM via VMware Tools 52 description: 53 - Use VMware tools to run tasks in, or put/fetch files to guest operating systems running in VMware infrastructure. 54 - In case of Windows VMs, set C(ansible_shell_type) to C(powershell). 55 - Does not work with 'become'. 56 version_added: "2.8" 57 requirements: 58 - pyvmomi (Python library) 59 - requests (Python library) 60 options: 61 vmware_host: 62 description: 63 - FQDN or IP Address for the connection (vCenter or ESXi Host). 64 env: 65 - name: VI_SERVER 66 - name: VMWARE_HOST 67 vars: 68 - name: ansible_host 69 - name: ansible_vmware_host 70 required: True 71 vmware_user: 72 description: 73 - Username for the connection. 74 - "Requires the following permissions on the VM: 75 - VirtualMachine.GuestOperations.Execute 76 - VirtualMachine.GuestOperations.Modify 77 - VirtualMachine.GuestOperations.Query" 78 env: 79 - name: VI_USERNAME 80 - name: VMWARE_USER 81 vars: 82 - name: ansible_vmware_user 83 required: True 84 vmware_password: 85 description: 86 - Password for the connection. 87 env: 88 - name: VI_PASSWORD 89 - name: VMWARE_PASSWORD 90 vars: 91 - name: ansible_vmware_password 92 required: True 93 vmware_port: 94 description: 95 - Port for the connection. 96 env: 97 - name: VI_PORTNUMBER 98 - name: VMWARE_PORT 99 vars: 100 - name: ansible_port 101 - name: ansible_vmware_port 102 required: False 103 default: 443 104 validate_certs: 105 description: 106 - Verify SSL for the connection. 107 - "Note: This will validate certs for both C(vmware_host) and the ESXi host running the VM." 108 env: 109 - name: VMWARE_VALIDATE_CERTS 110 vars: 111 - name: ansible_vmware_validate_certs 112 default: True 113 type: bool 114 vm_path: 115 description: 116 - VM path absolute to the connection. 117 - "vCenter Example: C(Datacenter/vm/Discovered virtual machine/testVM)." 118 - "ESXi Host Example: C(ha-datacenter/vm/testVM)." 119 - Must include VM name, appended to 'folder' as would be passed to M(vmware_guest). 120 - Needs to include I(vm) between the Datacenter and the rest of the VM path. 121 - Datacenter default value for ESXi server is C(ha-datacenter). 122 - Folder I(vm) is not visible in the vSphere Web Client but necessary for VMware API to work. 123 vars: 124 - name: ansible_vmware_guest_path 125 required: True 126 vm_user: 127 description: 128 - VM username. 129 vars: 130 - name: ansible_user 131 - name: ansible_vmware_tools_user 132 required: True 133 vm_password: 134 description: 135 - Password for the user in guest operating system. 136 vars: 137 - name: ansible_password 138 - name: ansible_vmware_tools_password 139 required: True 140 exec_command_sleep_interval: 141 description: 142 - Time in seconds to sleep between execution of command. 143 vars: 144 - name: ansible_vmware_tools_exec_command_sleep_interval 145 default: 0.5 146 type: float 147 file_chunk_size: 148 description: 149 - File chunk size. 150 - "(Applicable when writing a file to disk, example: using the C(fetch) module.)" 151 vars: 152 - name: ansible_vmware_tools_file_chunk_size 153 default: 128 154 type: integer 155 executable: 156 description: 157 - shell to use for execution inside container 158 default: /bin/sh 159 ini: 160 - section: defaults 161 key: executable 162 env: 163 - name: ANSIBLE_EXECUTABLE 164 vars: 165 - name: ansible_executable 166 - name: ansible_vmware_tools_executable 167""" 168 169EXAMPLES = r''' 170# example vars.yml 171--- 172ansible_connection: vmware_tools 173 174ansible_vmware_host: vcenter.example.com 175ansible_vmware_user: administrator@vsphere.local 176ansible_vmware_password: Secr3tP4ssw0rd!12 177ansible_vmware_validate_certs: no # default is yes 178 179# vCenter Connection VM Path Example 180ansible_vmware_guest_path: DATACENTER/vm/FOLDER/{{ inventory_hostname }} 181# ESXi Connection VM Path Example 182ansible_vmware_guest_path: ha-datacenter/vm/{{ inventory_hostname }} 183 184ansible_vmware_tools_user: root 185ansible_vmware_tools_password: MyR00tPassw0rD 186 187# if the target VM guest is Windows set the 'ansible_shell_type' to 'powershell' 188ansible_shell_type: powershell 189 190 191# example playbook_linux.yml 192--- 193- name: Test VMware Tools Connection Plugin for Linux 194 hosts: linux 195 tasks: 196 - command: whoami 197 198 - ping: 199 200 - copy: 201 src: foo 202 dest: /home/user/foo 203 204 - fetch: 205 src: /home/user/foo 206 dest: linux-foo 207 flat: yes 208 209 - file: 210 path: /home/user/foo 211 state: absent 212 213 214# example playbook_windows.yml 215--- 216- name: Test VMware Tools Connection Plugin for Windows 217 hosts: windows 218 tasks: 219 - win_command: whoami 220 221 - win_ping: 222 223 - win_copy: 224 src: foo 225 dest: C:\Users\user\foo 226 227 - fetch: 228 src: C:\Users\user\foo 229 dest: windows-foo 230 flat: yes 231 232 - win_file: 233 path: C:\Users\user\foo 234 state: absent 235''' 236 237 238class Connection(ConnectionBase): 239 """VMware Tools Connection.""" 240 241 transport = "vmware_tools" 242 243 @property 244 def vmware_host(self): 245 """Read-only property holding the connection address.""" 246 return self.get_option("vmware_host") 247 248 @property 249 def validate_certs(self): 250 """Read-only property holding whether the connection should validate certs.""" 251 return self.get_option("validate_certs") 252 253 @property 254 def authManager(self): 255 """Guest Authentication Manager.""" 256 return self._si.content.guestOperationsManager.authManager 257 258 @property 259 def fileManager(self): 260 """Guest File Manager.""" 261 return self._si.content.guestOperationsManager.fileManager 262 263 @property 264 def processManager(self): 265 """Guest Process Manager.""" 266 return self._si.content.guestOperationsManager.processManager 267 268 @property 269 def windowsGuest(self): 270 """Return if VM guest family is windows.""" 271 return self.vm.guest.guestFamily == "windowsGuest" 272 273 def __init__(self, *args, **kwargs): 274 """init.""" 275 super(Connection, self).__init__(*args, **kwargs) 276 if hasattr(self, "_shell") and self._shell.SHELL_FAMILY == "powershell": 277 self.module_implementation_preferences = (".ps1", ".exe", "") 278 self.become_methods = ["runas"] 279 self.allow_executable = False 280 self.has_pipelining = False 281 self.allow_extras = True 282 283 def _establish_connection(self): 284 connection_kwargs = { 285 "host": self.vmware_host, 286 "user": self.get_option("vmware_user"), 287 "pwd": self.get_option("vmware_password"), 288 "port": self.get_option("vmware_port"), 289 } 290 291 if self.validate_certs: 292 connect = SmartConnect 293 else: 294 if HAS_URLLIB3: 295 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 296 connect = SmartConnectNoSSL 297 298 try: 299 self._si = connect(**connection_kwargs) 300 except SSLError: 301 raise AnsibleError("SSL Error: Certificate verification failed.") 302 except (gaierror): 303 raise AnsibleError("Connection Error: Unable to connect to '%s'." % to_native(connection_kwargs["host"])) 304 except vim.fault.InvalidLogin as e: 305 raise AnsibleError("Connection Login Error: %s" % to_native(e.msg)) 306 307 def _establish_vm(self): 308 searchIndex = self._si.content.searchIndex 309 self.vm = searchIndex.FindByInventoryPath(self.get_option("vm_path")) 310 311 if self.vm is None: 312 raise AnsibleError("Unable to find VM by path '%s'" % to_native(self.get_option("vm_path"))) 313 314 self.vm_auth = vim.NamePasswordAuthentication( 315 username=self.get_option("vm_user"), password=self.get_option("vm_password"), interactiveSession=False 316 ) 317 318 try: 319 self.authManager.ValidateCredentialsInGuest(vm=self.vm, auth=self.vm_auth) 320 except vim.fault.InvalidPowerState as e: 321 raise AnsibleError("VM Power State Error: %s" % to_native(e.msg)) 322 except vim.fault.RestrictedVersion as e: 323 raise AnsibleError("Restricted Version Error: %s" % to_native(e.msg)) 324 except vim.fault.GuestOperationsUnavailable as e: 325 raise AnsibleError("VM Guest Operations (VMware Tools) Error: %s" % to_native(e.msg)) 326 except vim.fault.InvalidGuestLogin as e: 327 raise AnsibleError("VM Login Error: %s" % to_native(e.msg)) 328 except vim.fault.NoPermission as e: 329 raise AnsibleConnectionFailure("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId))) 330 331 def _connect(self): 332 if not HAS_REQUESTS: 333 raise AnsibleError("%s : %s" % (missing_required_lib('requests'), REQUESTS_IMP_ERR)) 334 335 if not HAS_PYVMOMI: 336 raise AnsibleError("%s : %s" % (missing_required_lib('PyVmomi'), PYVMOMI_IMP_ERR)) 337 338 super(Connection, self)._connect() 339 340 if self.connected: 341 pass 342 343 self._establish_connection() 344 self._establish_vm() 345 346 self._connected = True 347 348 def close(self): 349 """Close connection.""" 350 super(Connection, self).close() 351 352 Disconnect(self._si) 353 self._connected = False 354 355 def reset(self): 356 """Reset the connection.""" 357 super(Connection, self).reset() 358 359 self.close() 360 self._connect() 361 362 def create_temporary_file_in_guest(self, prefix="", suffix=""): 363 """Create a temporary file in the VM.""" 364 try: 365 return self.fileManager.CreateTemporaryFileInGuest(vm=self.vm, auth=self.vm_auth, prefix=prefix, suffix=suffix) 366 except vim.fault.NoPermission as e: 367 raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId))) 368 369 def _get_program_spec_program_path_and_arguments(self, cmd): 370 if self.windowsGuest: 371 ''' 372 we need to warp the execution of powershell into a cmd /c because 373 the call otherwise fails with "Authentication or permission failure" 374 #FIXME: Fix the unecessary invocation of cmd and run the command directly 375 ''' 376 program_path = "cmd.exe" 377 arguments = "/c %s" % cmd 378 else: 379 program_path = self.get_option("executable") 380 arguments = re.sub(r"^%s\s*" % program_path, "", cmd) 381 382 return program_path, arguments 383 384 def _get_guest_program_spec(self, cmd, stdout, stderr): 385 guest_program_spec = vim.GuestProgramSpec() 386 387 program_path, arguments = self._get_program_spec_program_path_and_arguments(cmd) 388 389 arguments += " 1> %s 2> %s" % (stdout, stderr) 390 391 guest_program_spec.programPath = program_path 392 guest_program_spec.arguments = arguments 393 394 return guest_program_spec 395 396 def _get_pid_info(self, pid): 397 try: 398 processes = self.processManager.ListProcessesInGuest(vm=self.vm, auth=self.vm_auth, pids=[pid]) 399 except vim.fault.NoPermission as e: 400 raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId))) 401 return processes[0] 402 403 def _fix_url_for_hosts(self, url): 404 """ 405 Fix url if connection is a host. 406 407 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 408 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 409 be http://*/guestFile?id=1&token=1234. The client replaces the asterisk with the server name on which it invoked the call. 410 411 https://code.vmware.com/apis/358/vsphere#/doc/vim.vm.guest.FileManager.FileTransferInformation.html 412 """ 413 return url.replace("*", self.vmware_host) 414 415 def _fetch_file_from_vm(self, guestFilePath): 416 try: 417 fileTransferInformation = self.fileManager.InitiateFileTransferFromGuest(vm=self.vm, auth=self.vm_auth, guestFilePath=guestFilePath) 418 except vim.fault.NoPermission as e: 419 raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId))) 420 421 url = self._fix_url_for_hosts(fileTransferInformation.url) 422 response = requests.get(url, verify=self.validate_certs, stream=True) 423 424 if response.status_code != 200: 425 raise AnsibleError("Failed to fetch file") 426 427 return response 428 429 def delete_file_in_guest(self, filePath): 430 """Delete file from VM.""" 431 try: 432 self.fileManager.DeleteFileInGuest(vm=self.vm, auth=self.vm_auth, filePath=filePath) 433 except vim.fault.NoPermission as e: 434 raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId))) 435 436 def exec_command(self, cmd, in_data=None, sudoable=True): 437 """Execute command.""" 438 super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) 439 440 stdout = self.create_temporary_file_in_guest(suffix=".stdout") 441 stderr = self.create_temporary_file_in_guest(suffix=".stderr") 442 443 guest_program_spec = self._get_guest_program_spec(cmd, stdout, stderr) 444 445 try: 446 pid = self.processManager.StartProgramInGuest(vm=self.vm, auth=self.vm_auth, spec=guest_program_spec) 447 except vim.fault.NoPermission as e: 448 raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId))) 449 except vim.fault.FileNotFound as e: 450 raise AnsibleError("StartProgramInGuest Error: %s" % to_native(e.msg)) 451 452 pid_info = self._get_pid_info(pid) 453 454 while pid_info.endTime is None: 455 sleep(self.get_option("exec_command_sleep_interval")) 456 pid_info = self._get_pid_info(pid) 457 458 stdout_response = self._fetch_file_from_vm(stdout) 459 self.delete_file_in_guest(stdout) 460 461 stderr_response = self._fetch_file_from_vm(stderr) 462 self.delete_file_in_guest(stderr) 463 464 return pid_info.exitCode, stdout_response.text, stderr_response.text 465 466 def fetch_file(self, in_path, out_path): 467 """Fetch file.""" 468 super(Connection, self).fetch_file(in_path, out_path) 469 470 in_path_response = self._fetch_file_from_vm(in_path) 471 472 with open(out_path, "wb") as fd: 473 for chunk in in_path_response.iter_content(chunk_size=self.get_option("file_chunk_size")): 474 fd.write(chunk) 475 476 def put_file(self, in_path, out_path): 477 """Put file.""" 478 super(Connection, self).put_file(in_path, out_path) 479 480 if not exists(to_bytes(in_path, errors="surrogate_or_strict")): 481 raise AnsibleFileNotFound("file or module does not exist: '%s'" % to_native(in_path)) 482 483 try: 484 put_url = self.fileManager.InitiateFileTransferToGuest( 485 vm=self.vm, auth=self.vm_auth, guestFilePath=out_path, fileAttributes=vim.GuestFileAttributes(), fileSize=getsize(in_path), overwrite=True 486 ) 487 except vim.fault.NoPermission as e: 488 raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId))) 489 490 url = self._fix_url_for_hosts(put_url) 491 492 # file size of 'in_path' must be greater than 0 493 with open(in_path, "rb") as fd: 494 response = requests.put(url, verify=self.validate_certs, data=fd) 495 496 if response.status_code != 200: 497 raise AnsibleError("File transfer failed") 498