diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest_sendkey.py b/lib/ansible/modules/cloud/vmware/vmware_guest_sendkey.py new file mode 100644 index 0000000000..d384b06e1a --- /dev/null +++ b/lib/ansible/modules/cloud/vmware/vmware_guest_sendkey.py @@ -0,0 +1,354 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# Copyright: (c) 2018, Diane Wang +# 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 + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: vmware_guest_sendkey +short_description: Send USB HID codes to the Virtual Machine's keyboard. +description: + - This module is used to send keystrokes to given virtual machine. + - All parameters and VMware object names are case sensitive. +version_added: '2.9' +author: + - Diane Wang (@Tomorrow9) +notes: + - Tested on vSphere 6.5 and 6.7 +requirements: + - "python >= 2.6" + - PyVmomi +options: + name: + description: + - Name of the virtual machine. + - This is a required parameter, if parameter C(uuid) is not supplied. + uuid: + description: + - UUID of the instance to gather facts if known, this is VMware's unique identifier. + - This is a required parameter, if parameter C(name) is not supplied. + folder: + description: + - Destination folder, absolute or relative path to find an existing guest. + - This is a required parameter, only if multiple VMs are found with same name. + - The folder should include the datacenter. ESXi server's datacenter is ha-datacenter. + - 'Examples:' + - ' folder: /ha-datacenter/vm' + - ' folder: ha-datacenter/vm' + - ' folder: /datacenter1/vm' + - ' folder: datacenter1/vm' + - ' folder: /datacenter1/vm/folder1' + - ' folder: datacenter1/vm/folder1' + - ' folder: /folder1/datacenter1/vm' + - ' folder: folder1/datacenter1/vm' + - ' folder: /folder1/datacenter1/vm/folder2' + cluster: + description: + - The name of cluster where the virtual machine is running. + - This is a required parameter, if C(esxi_hostname) is not set. + - C(esxi_hostname) and C(cluster) are mutually exclusive parameters. + esxi_hostname: + description: + - The ESXi hostname where the virtual machine is running. + - This is a required parameter, if C(cluster) is not set. + - C(esxi_hostname) and C(cluster) are mutually exclusive parameters. + datacenter: + description: + - The datacenter name to which virtual machine belongs to. + string_send: + description: + - The string will be sent to the virtual machine. + - This string can contain valid special character, alphabet and digit on the keyboard. + keys_send: + description: + - The list of the keys will be sent to the virtual machine. + - 'Valid values are C(ENTER), C(ESC), C(BACKSPACE), C(TAB), C(SPACE), C(CAPSLOCK), C(DELETE), C(CTRL_ALT_DEL), + C(CTRL_C) and C(F1) to C(F12), C(RIGHTARROW), C(LEFTARROW), C(DOWNARROW), C(UPARROW).' + - If both C(keys_send) and C(string_send) are specified, keys in C(keys_send) list will be sent in front of the C(string_send). +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' +- name: send list of keys to virtual machine + vmware_guest_sendkey: + validate_certs: no + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ datacenter_name }}" + folder: "{{ folder_name }}" + name: "{{ vm_name }}" + keys_send: + - TAB + - TAB + - ENTER + delegate_to: localhost + register: keys_num_sent + +- name: send a string to virtual machine + vmware_guest_sendkey: + validate_certs: no + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ datacenter_name }}" + folder: "{{ folder_name }}" + name: "{{ vm_name }}" + string_send: "user_logon" + delegate_to: localhost + register: keys_num_sent +''' + +RETURN = """ +sendkey_facts: + description: display the keys and the number of keys sent to the virtual machine + returned: always + type: dict + sample: { + "virtual_machine": "test_vm", + "keys_send": [ + "SPACE", + "DOWNARROW", + "DOWNARROW", + "ENTER" + ], + "string_send": null, + "keys_send_number": 4, + "returned_keys_send_number": 4, + } +""" + +try: + from pyVmomi import vim, vmodl +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible.module_utils.vmware import PyVmomi, vmware_argument_spec + + +class PyVmomiHelper(PyVmomi): + def __init__(self, module): + super(PyVmomiHelper, self).__init__(module) + self.change_detected = False + self.usb_scan_code_spec = vim.UsbScanCodeSpec() + self.num_keys_send = 0 + # HID usage tables https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf + # define valid characters and keys value, hex_code, key value and key modifier + self.keys_hid_code = [ + (('a', 'A'), '0x04', [('a', []), ('A', ['LEFTSHIFT'])]), + (('b', 'B'), '0x05', [('b', []), ('B', ['LEFTSHIFT'])]), + (('c', 'C'), '0x06', [('c', []), ('C', ['LEFTSHIFT'])]), + (('d', 'D'), '0x07', [('d', []), ('D', ['LEFTSHIFT'])]), + (('e', 'E'), '0x08', [('e', []), ('E', ['LEFTSHIFT'])]), + (('f', 'F'), '0x09', [('f', []), ('F', ['LEFTSHIFT'])]), + (('g', 'G'), '0x0a', [('g', []), ('G', ['LEFTSHIFT'])]), + (('h', 'H'), '0x0b', [('h', []), ('H', ['LEFTSHIFT'])]), + (('i', 'I'), '0x0c', [('i', []), ('I', ['LEFTSHIFT'])]), + (('j', 'J'), '0x0d', [('j', []), ('J', ['LEFTSHIFT'])]), + (('k', 'K'), '0x0e', [('k', []), ('K', ['LEFTSHIFT'])]), + (('l', 'L'), '0x0f', [('l', []), ('L', ['LEFTSHIFT'])]), + (('m', 'M'), '0x10', [('m', []), ('M', ['LEFTSHIFT'])]), + (('n', 'N'), '0x11', [('n', []), ('N', ['LEFTSHIFT'])]), + (('o', 'O'), '0x12', [('o', []), ('O', ['LEFTSHIFT'])]), + (('p', 'P'), '0x13', [('p', []), ('P', ['LEFTSHIFT'])]), + (('q', 'Q'), '0x14', [('q', []), ('Q', ['LEFTSHIFT'])]), + (('r', 'R'), '0x15', [('r', []), ('R', ['LEFTSHIFT'])]), + (('s', 'S'), '0x16', [('s', []), ('S', ['LEFTSHIFT'])]), + (('t', 'T'), '0x17', [('t', []), ('T', ['LEFTSHIFT'])]), + (('u', 'U'), '0x18', [('u', []), ('U', ['LEFTSHIFT'])]), + (('v', 'V'), '0x19', [('v', []), ('V', ['LEFTSHIFT'])]), + (('w', 'W'), '0x1a', [('w', []), ('W', ['LEFTSHIFT'])]), + (('x', 'X'), '0x1b', [('x', []), ('X', ['LEFTSHIFT'])]), + (('y', 'Y'), '0x1c', [('y', []), ('Y', ['LEFTSHIFT'])]), + (('z', 'Z'), '0x1d', [('z', []), ('Z', ['LEFTSHIFT'])]), + (('1', '!'), '0x1e', [('1', []), ('!', ['LEFTSHIFT'])]), + (('2', '@'), '0x1f', [('2', []), ('@', ['LEFTSHIFT'])]), + (('3', '#'), '0x20', [('3', []), ('#', ['LEFTSHIFT'])]), + (('4', '$'), '0x21', [('4', []), ('$', ['LEFTSHIFT'])]), + (('5', '%'), '0x22', [('5', []), ('%', ['LEFTSHIFT'])]), + (('6', '^'), '0x23', [('6', []), ('^', ['LEFTSHIFT'])]), + (('7', '&'), '0x24', [('7', []), ('&', ['LEFTSHIFT'])]), + (('8', '*'), '0x25', [('8', []), ('*', ['LEFTSHIFT'])]), + (('9', '('), '0x26', [('9', []), ('(', ['LEFTSHIFT'])]), + (('0', ')'), '0x27', [('0', []), (')', ['LEFTSHIFT'])]), + (('-', '_'), '0x2d', [('-', []), ('_', ['LEFTSHIFT'])]), + (('=', '+'), '0x2e', [('=', []), ('+', ['LEFTSHIFT'])]), + (('[', '{'), '0x2f', [('[', []), ('{', ['LEFTSHIFT'])]), + ((']', '}'), '0x30', [(']', []), ('}', ['LEFTSHIFT'])]), + (('\\', '|'), '0x31', [('\\', []), ('|', ['LEFTSHIFT'])]), + ((';', ':'), '0x33', [(';', []), (':', ['LEFTSHIFT'])]), + (('\'', '"'), '0x34', [('\'', []), ('"', ['LEFTSHIFT'])]), + (('`', '~'), '0x35', [('`', []), ('~', ['LEFTSHIFT'])]), + ((',', '<'), '0x36', [(',', []), ('<', ['LEFTSHIFT'])]), + (('.', '>'), '0x37', [('.', []), ('>', ['LEFTSHIFT'])]), + (('/', '?'), '0x38', [('/', []), ('?', ['LEFTSHIFT'])]), + ('ENTER', '0x28', [('', [])]), + ('ESC', '0x29', [('', [])]), + ('BACKSPACE', '0x2a', [('', [])]), + ('TAB', '0x2b', [('', [])]), + ('SPACE', '0x2c', [(' ', [])]), + ('CAPSLOCK', '0x39', [('', [])]), + ('F1', '0x3a', [('', [])]), + ('F2', '0x3b', [('', [])]), + ('F3', '0x3c', [('', [])]), + ('F4', '0x3d', [('', [])]), + ('F5', '0x3e', [('', [])]), + ('F6', '0x3f', [('', [])]), + ('F7', '0x40', [('', [])]), + ('F8', '0x41', [('', [])]), + ('F9', '0x42', [('', [])]), + ('F10', '0x43', [('', [])]), + ('F11', '0x44', [('', [])]), + ('F12', '0x45', [('', [])]), + ('DELETE', '0x4c', [('', [])]), + ('CTRL_ALT_DEL', '0x4c', [('', ['CTRL', 'ALT'])]), + ('CTRL_C', '0x06', [('', ['CTRL'])]), + ('RIGHTARROW', '0x4f', [('', [])]), + ('LEFTARROW', '0x50', [('', [])]), + ('DOWNARROW', '0x51', [('', [])]), + ('UPARROW', '0x52', [('', [])]), + ] + + @staticmethod + def hid_to_hex(hid_code): + return int(hid_code, 16) << 16 | 0o0007 + + def get_hid_from_key(self, key): + if key == ' ': + return '0x2c', [] + for keys_name, key_code, keys_value in self.keys_hid_code: + if isinstance(keys_name, tuple): + for keys in keys_value: + if key == keys[0]: + return key_code, keys[1] + else: + if key == keys_name: + return key_code, keys_value[0][1] + + def get_key_event(self, hid_code, modifiers): + key_event = vim.UsbScanCodeSpecKeyEvent() + key_modifier = vim.UsbScanCodeSpecModifierType() + key_modifier.leftAlt = False + key_modifier.leftControl = False + key_modifier.leftGui = False + key_modifier.leftShift = False + key_modifier.rightAlt = False + key_modifier.rightControl = False + key_modifier.rightGui = False + key_modifier.rightShift = False + # rightShift, rightControl, rightAlt, leftGui, rightGui are not used + if "LEFTSHIFT" in modifiers: + key_modifier.leftShift = True + if "CTRL" in modifiers: + key_modifier.leftControl = True + if "ALT" in modifiers: + key_modifier.leftAlt = True + key_event.modifiers = key_modifier + key_event.usbHidCode = self.hid_to_hex(hid_code) + + return key_event + + def get_sendkey_facts(self, vm_obj, returned_value=0): + sendkey_facts = dict() + if vm_obj is not None: + sendkey_facts = dict( + virtual_machine=vm_obj.name, + keys_send=self.params['keys_send'], + string_send=self.params['string_send'], + keys_send_number=self.num_keys_send, + returned_keys_send_number=returned_value, + ) + + return sendkey_facts + + def send_key_to_vm(self, vm_obj): + key_event = None + num_keys_returned = 0 + if self.params['keys_send']: + for specified_key in self.params['keys_send']: + key_found = False + for keys in self.keys_hid_code: + if (isinstance(keys[0], tuple) and specified_key in keys[0]) or \ + (not isinstance(keys[0], tuple) and specified_key == keys[0]): + hid_code, modifiers = self.get_hid_from_key(specified_key) + key_event = self.get_key_event(hid_code, modifiers) + self.usb_scan_code_spec.keyEvents.append(key_event) + self.num_keys_send += 1 + key_found = True + break + if not key_found: + self.module.fail_json(msg="keys_send parameter: '%s' in %s not supported." + % (specified_key, self.params['keys_send'])) + + if self.params['string_send']: + for char in self.params['string_send']: + key_found = False + for keys in self.keys_hid_code: + if (isinstance(keys[0], tuple) and char in keys[0]) or char == ' ': + hid_code, modifiers = self.get_hid_from_key(char) + key_event = self.get_key_event(hid_code, modifiers) + self.usb_scan_code_spec.keyEvents.append(key_event) + self.num_keys_send += 1 + key_found = True + break + if not key_found: + self.module.fail_json(msg="string_send parameter: '%s' contains char: '%s' not supported." + % (self.params['string_send'], char)) + + if self.usb_scan_code_spec.keyEvents: + try: + num_keys_returned = vm_obj.PutUsbScanCodes(self.usb_scan_code_spec) + self.change_detected = True + except vmodl.RuntimeFault as e: + self.module.fail_json(msg="Failed to send key %s to virtual machine due to %s" % (key_event, to_native(e.msg))) + + sendkey_facts = self.get_sendkey_facts(vm_obj, num_keys_returned) + if num_keys_returned != self.num_keys_send: + results = {'changed': self.change_detected, 'failed': True, 'sendkey_facts': sendkey_facts} + else: + results = {'changed': self.change_detected, 'failed': False, 'sendkey_facts': sendkey_facts} + + return results + + +def main(): + argument_spec = vmware_argument_spec() + argument_spec.update( + name=dict(type='str'), + uuid=dict(type='str'), + folder=dict(type='str'), + datacenter=dict(type='str'), + esxi_hostname=dict(type='str'), + cluster=dict(type='str'), + keys_send=dict(type='list', default=[]), + string_send=dict(type='str') + ) + + module = AnsibleModule(argument_spec=argument_spec, required_one_of=[['name', 'uuid']]) + pyv = PyVmomiHelper(module) + vm = pyv.get_vm() + if not vm: + module.fail_json(msg='Unable to find the specified virtual machine uuid: %s, name: %s ' + % ((module.params.get('uuid')), (module.params.get('name')))) + + result = pyv.send_key_to_vm(vm) + if result['failed']: + module.fail_json(**result) + else: + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/vmware_guest_sendkey/aliases b/test/integration/targets/vmware_guest_sendkey/aliases new file mode 100644 index 0000000000..3eede2cbf0 --- /dev/null +++ b/test/integration/targets/vmware_guest_sendkey/aliases @@ -0,0 +1,3 @@ +cloud/vcenter +shippable/vcenter/group1 +needs/target/prepare_vmware_tests diff --git a/test/integration/targets/vmware_guest_sendkey/tasks/main.yml b/test/integration/targets/vmware_guest_sendkey/tasks/main.yml new file mode 100644 index 0000000000..4a1e0fedd1 --- /dev/null +++ b/test/integration/targets/vmware_guest_sendkey/tasks/main.yml @@ -0,0 +1,45 @@ +# Test code for the vmware_guest_sendkey module +# Copyright: (c) 2017, Diane Wang (Tomorrow9) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- when: vcsim is not defined + block: + - import_role: + name: prepare_vmware_tests + vars: + setup_attach_host: true + setup_datastore: true + setup_virtualmachines: true + + - name: send keys to virtual machine + vmware_guest_sendkey: + validate_certs: False + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + name: "{{ infra.vm_list[0] }}" + keys_send: + - DOWNARROW + - DOWNARROW + - ENTER + register: send_key + - debug: var=send_key + - name: assert the keys were sent to VM + assert: + that: + - "send_key.changed == true" + + - name: send string to virtual machine + vmware_guest_sendkey: + validate_certs: False + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + name: "{{ infra.vm_list[0] }}" + string_send: "test-user" + register: send_string + - debug: var=send_string + - name: assert the string was sent to VM + assert: + that: + - "send_string.changed == true"