From 920cd7bb1cf5e42533e3b5b90b6f36621206b6ff Mon Sep 17 00:00:00 2001
From: Philippe Dellaert <philippe@dellaert.org>
Date: Thu, 10 Aug 2017 03:50:58 +0200
Subject: [PATCH] New module: Waiting for VMware tools to become available
 (#27236)

* Adding VMware tools module
Functionality: Waits for VMware tools to become available (running
state)

* Adding base integration test preparations
Until govcsim supports actual guest tool status, the tests are disabled

* Cleanup and better getvm method

* Updating Changelog

* Adding required metaclass and future import

* Rename to vmware_guest_tools_wait

* Cleanup of documentation

* Fixing review remarks
---
 CHANGELOG.md                                  |   1 +
 .../cloud/vmware/vmware_guest_tools_wait.py   | 194 ++++++++++++++++++
 .../targets/vmware_guest_tools_wait/aliases   |   2 +
 .../vmware_guest_tools_wait/tasks/main.yml    |  97 +++++++++
 4 files changed, 294 insertions(+)
 create mode 100644 lib/ansible/modules/cloud/vmware/vmware_guest_tools_wait.py
 create mode 100644 test/integration/targets/vmware_guest_tools_wait/aliases
 create mode 100644 test/integration/targets/vmware_guest_tools_wait/tasks/main.yml

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b139613ae01..5997f958541 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -296,6 +296,7 @@ Ansible Changes By Release
 - vmware
   * vcenter_license
   * vmware_guest_find
+  * vmware_guest_tools_wait
 - windows
   * win_defrag
   * win_domain_group
diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest_tools_wait.py b/lib/ansible/modules/cloud/vmware/vmware_guest_tools_wait.py
new file mode 100644
index 00000000000..96f6431e9dc
--- /dev/null
+++ b/lib/ansible/modules/cloud/vmware/vmware_guest_tools_wait.py
@@ -0,0 +1,194 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017 Philippe Dellaert <philippe@dellaert.org>
+# 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.0',
+                    'status': ['preview'],
+                    'supported_by': 'community'}
+
+
+DOCUMENTATION = '''
+---
+module: vmware_guest_tools_wait
+short_description: Wait for VMware tools to become available
+description:
+    - Wait for VMware tools to become available on the VM and return facts.
+version_added: '2.4'
+author:
+    - Philippe Dellaert (@pdellaert) <philippe@dellaert.org>
+notes:
+    - Tested on vSphere 6.5
+requirements:
+    - python >= 2.6
+    - PyVmomi
+options:
+   name:
+        description:
+            - Name of the VM for which to wait until the tools become available.
+            - This is required if uuid is not supplied.
+   name_match:
+        description:
+            - If multiple VMs match the name, use the first or last found.
+        default: 'first'
+        choices: ['first', 'last']
+   folder:
+        description:
+            - Destination folder, absolute or relative path to find an existing guest.
+            - This is required if C(name) is supplied.
+            - The folder should include the datacenter. ESX's datacenter is C(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'
+        default: /vm
+   uuid:
+        description:
+            - UUID of the VM  for which to wait until the tools become available, if known. This is VMware's unique identifier.
+            - This is required if C(name) is not supplied.
+extends_documentation_fragment: vmware.documentation
+'''
+
+EXAMPLES = '''
+- name: Wait for VMware tools to become available by UUID
+  vmware_guest_tools_wait:
+    hostname: 192.168.1.209
+    username: administrator@vsphere.local
+    password: vmware
+    validate_certs: no
+    uuid: 421e4592-c069-924d-ce20-7e7533fab926
+  delegate_to: localhost
+  register: facts
+
+- name: Wait for VMware tools to become available by name
+  vmware_guest_tools_wait:
+    hostname: 192.168.1.209
+    username: administrator@vsphere.local
+    password: vmware
+    validate_certs: no
+    name: test-vm
+    folder: /datacenter1/vm
+  delegate_to: localhost
+  register: facts
+'''
+
+RETURN = """
+instance:
+    description: metadata about the virtual machine
+    returned: always
+    type: dict
+    sample: None
+"""
+
+import time
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+from ansible.module_utils.vmware import connect_to_api, gather_vm_facts, vmware_argument_spec, find_vm_by_id
+
+HAS_PYVMOMI = False
+try:
+    import pyVmomi
+    from pyVmomi import vim
+
+    HAS_PYVMOMI = True
+except ImportError:
+    pass
+
+
+class PyVmomiHelper(object):
+    def __init__(self, module):
+        if not HAS_PYVMOMI:
+            module.fail_json(msg='pyvmomi module required')
+
+        self.module = module
+        self.params = module.params
+        self.content = connect_to_api(self.module)
+
+    def getvm(self, name=None, uuid=None, folder=None):
+        vm = None
+        match_first = False
+        if uuid:
+            vm = find_vm_by_id(self.content, vm_id=uuid, vm_id_type="uuid")
+        elif folder and name:
+            if self.params['name_match'] == 'first':
+                match_first = True
+            vm = find_vm_by_id(self.content, vm_id=name, vm_id_type="inventory_path", folder=folder, match_first=match_first)
+        return vm
+
+    def gather_facts(self, vm):
+        return gather_vm_facts(self.content, vm)
+
+    def wait_for_tools(self, vm, poll=100, sleep=5):
+        tools_running = False
+        vm_facts = {}
+        poll_num = 0
+        vm_uuid = vm.config.uuid
+        while not tools_running and poll_num <= poll:
+            newvm = self.getvm(uuid=vm_uuid)
+            vm_facts = self.gather_facts(newvm)
+            if vm_facts['guest_tools_status'] == 'guestToolsRunning':
+                tools_running = True
+            else:
+                time.sleep(sleep)
+                poll_num += 1
+
+        if not tools_running:
+            return {'failed': True, 'msg': 'VMware tools either not present or not running after {0} seconds'.format((poll * sleep))}
+
+        changed = False
+        if poll_num > 0:
+            changed = True
+        return {'changed': changed, 'failed': False, 'instance': vm_facts}
+
+
+def main():
+    argument_spec = vmware_argument_spec()
+    argument_spec.update(
+        name=dict(type='str'),
+        name_match=dict(type='str', default='first'),
+        folder=dict(type='str', default='/vm'),
+        uuid=dict(type='str'),
+    )
+    module = AnsibleModule(
+        argument_spec=argument_spec,
+        required_one_of=[['name', 'uuid']],
+        required_together=['name', 'folder']
+    )
+
+    # FindByInventoryPath() does not require an absolute path
+    # so we should leave the input folder path unmodified
+    module.params['folder'] = module.params['folder'].rstrip('/')
+
+    pyv = PyVmomiHelper(module)
+    # Check if the VM exists before continuing
+    vm = pyv.getvm(name=module.params['name'],
+                   folder=module.params['folder'],
+                   uuid=module.params['uuid'])
+
+    if not vm:
+        vm_id = module.params.get('name') or module.params.get('uuid')
+        module.fail_json(msg="Unable to wait for tools for non-existing VM {0:s}".format(vm_id))
+
+    try:
+        result = pyv.wait_for_tools(vm)
+    except Exception as e:
+        module.fail_json(msg="Waiting for tools failed with exception: {0:s}".format(to_native(e)))
+
+    if result['failed']:
+        module.fail_json(**result)
+    else:
+        module.exit_json(**result)
+
+if __name__ == '__main__':
+    main()
diff --git a/test/integration/targets/vmware_guest_tools_wait/aliases b/test/integration/targets/vmware_guest_tools_wait/aliases
new file mode 100644
index 00000000000..1c56b8da49a
--- /dev/null
+++ b/test/integration/targets/vmware_guest_tools_wait/aliases
@@ -0,0 +1,2 @@
+posix/ci/cloud/vcenter
+cloud/vcenter
diff --git a/test/integration/targets/vmware_guest_tools_wait/tasks/main.yml b/test/integration/targets/vmware_guest_tools_wait/tasks/main.yml
new file mode 100644
index 00000000000..05b46c89e07
--- /dev/null
+++ b/test/integration/targets/vmware_guest_tools_wait/tasks/main.yml
@@ -0,0 +1,97 @@
+# Test code for the vmware_guest_tools_wait module.
+
+# Copyright (c) 2017 Philippe Dellaert <philippe@dellaert.org>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- name: make sure pyvmomi is installed
+  pip:
+    name: pyvmomi
+    state: latest
+
+- name: store the vcenter container ip
+  set_fact:
+    vcsim: "{{ lookup('env', 'vcenter_host') }}"
+
+- debug: var=vcsim
+
+- name: Wait for Flask controller to come up online
+  wait_for:
+    host: "{{ vcsim }}"
+    port: 5000
+    state: started
+
+- name: kill vcsim
+  uri:
+    url: "{{ 'http://' + vcsim + ':5000/killall' }}"
+
+- name: start vcsim
+  uri:
+    url: "{{ 'http://' + vcsim + ':5000/spawn?datacenter=1&cluster=1&folder=0' }}"
+  register: vcsim_instance
+
+- name: Wait for vcsim server to come up online
+  wait_for:
+    host: "{{ vcsim }}"
+    port: 443
+    state: started
+
+- name: get a list of virtual machines from vcsim
+  uri:
+    url: "{{ 'http://' + vcsim + ':5000/govc_find?filter=VM' }}"
+  register: vms
+
+- set_fact: vm1="{{ vms['json'][0] }}"
+
+- name: Power on VM1
+  vmware_guest:
+    validate_certs: False
+    hostname: "{{ vcsim }}"
+    username: "{{ vcsim_instance['json']['username'] }}"
+    password: "{{ vcsim_instance['json']['password'] }}"
+    name: "{{ vm1|basename }}"
+    datacenter: "{{ (vm1|basename).split('_')[0] }}"
+    state: poweredon
+    folder: "{{ vm1|dirname }}"
+
+# FixMe: govcsim does not support VMware tools status reporting
+## Testcase 0001: Wait for VMware tools to become available by name
+#- name: Waiting until VMware tools becomes available
+#  vmware_guest_tools_wait:
+#    validate_certs: False
+#    hostname: "{{ vcsim }}"
+#    username: "{{ vcsim_instance['json']['username'] }}"
+#    password: "{{ vcsim_instance['json']['password'] }}"
+#    name: "{{ vm1 | basename }}"
+#    folder: "{{ vm1 | dirname }}"
+#  register: guest_facts_0001
+#
+#- debug: msg="{{ guest_facts_0001 }}"
+#
+#- assert:
+#    that:
+#      - "guest_facts_0001['instance']['hw_name'] == vm1 | basename"
+#      - "guest_facts_0001['instance']['hw_product_uuid'] is defined"
+#      - "guest_facts_0001['instance']['guest_tools_status'] == 'guestToolsRunning'"
+#
+#- set_fact: vm1_uuid="{{ guest_facts_0001['instance']['hw_product_uuid'] }}"
+#
+#- debug: var=vm1_uuid
+#
+# Testcase 0002: Wait for VMware tools to become available by UUID
+#- name: Waiting until VMware tools becomes available
+#  vmware_guest_tools_wait:
+#    validate_certs: False
+#    hostname: "{{ vcsim }}"
+#    username: "{{ vcsim_instance['json']['username'] }}"
+#    password: "{{ vcsim_instance['json']['password'] }}"
+#    uuid: "{{ vm1_uuid }}"
+#  register: guest_facts_0002
+#
+#- debug: msg="{{ guest_facts_0002 }}"
+#
+#- assert:
+#    that:
+#      - "guest_facts_0002['instance']['hw_name'] == vm1 | basename"
+#      - "guest_facts_0002['instance']['hw_product_uuid'] is defined"
+#      - "guest_facts_0002['instance']['hw_product_uuid'] == vm1_uuid"
+#      - "guest_facts_0002['instance']['guest_tools_status'] == 'guestToolsRunning'"