From f2495ef0d320678a84e4c8f1da199e9dde4e9a5a Mon Sep 17 00:00:00 2001 From: "James E. King III" Date: Mon, 25 Feb 2019 06:03:40 -0500 Subject: [PATCH] Add ability to get vmware_guest_facts using vsphere schema output (#47446) --- lib/ansible/module_utils/vmware.py | 108 +++++++++++++++++- .../cloud/vmware/vmware_guest_facts.py | 62 ++++++++-- .../targets/vmware_guest_facts/tasks/main.yml | 41 ++++--- 3 files changed, 188 insertions(+), 23 deletions(-) diff --git a/lib/ansible/module_utils/vmware.py b/lib/ansible/module_utils/vmware.py index cae4343810..7559ea98d6 100644 --- a/lib/ansible/module_utils/vmware.py +++ b/lib/ansible/module_utils/vmware.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2015, Joseph Callen # Copyright: (c) 2018, Ansible Project +# Copyright: (c) 2018, James E. King III (@jeking3) # 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 atexit +import ansible.module_utils.common._collections_compat as collections_compat +import json import os import re import ssl @@ -27,11 +30,13 @@ except ImportError: PYVMOMI_IMP_ERR = None try: from pyVim import connect - from pyVmomi import vim, vmodl + from pyVmomi import vim, vmodl, VmomiSupport HAS_PYVMOMI = True + HAS_PYVMOMIJSON = hasattr(VmomiSupport, 'VmomiJSONEncoder') except ImportError: PYVMOMI_IMP_ERR = traceback.format_exc() HAS_PYVMOMI = False + HAS_PYVMOMIJSON = False from ansible.module_utils._text import to_text, to_native from ansible.module_utils.six import integer_types, iteritems, string_types, raise_from @@ -1274,3 +1279,104 @@ class PyVmomi(object): return f self.module.fail_json(msg="No vmdk file found for path specified [%s]" % vmdk_path) + + # + # Conversion to JSON + # + + def _deepmerge(self, d, u): + """ + Deep merges u into d. + + Credit: + https://bit.ly/2EDOs1B (stackoverflow question 3232943) + License: + cc-by-sa 3.0 (https://creativecommons.org/licenses/by-sa/3.0/) + Changes: + using collections_compat for compatibility + + Args: + - d (dict): dict to merge into + - u (dict): dict to merge into d + + Returns: + dict, with u merged into d + """ + for k, v in iteritems(u): + if isinstance(v, collections_compat.Mapping): + d[k] = self._deepmerge(d.get(k, {}), v) + else: + d[k] = v + return d + + def _extract(self, data, remainder): + """ + This is used to break down dotted properties for extraction. + + Args: + - data (dict): result of _jsonify on a property + - remainder: the remainder of the dotted property to select + + Return: + dict + """ + result = dict() + if '.' not in remainder: + result[remainder] = data[remainder] + return result + key, remainder = remainder.split('.', 1) + result[key] = self._extract(data[key], remainder) + return result + + def _jsonify(self, obj): + """ + Convert an object from pyVmomi into JSON. + + Args: + - obj (object): vim object + + Return: + dict + """ + return json.loads(json.dumps(obj, cls=VmomiSupport.VmomiJSONEncoder, + sort_keys=True, strip_dynamic=True)) + + def to_json(self, obj, properties=None): + """ + Convert a vSphere (pyVmomi) Object into JSON. This is a deep + transformation. The list of properties is optional - if not + provided then all properties are deeply converted. The resulting + JSON is sorted to improve human readability. + + Requires upstream support from pyVmomi > 6.7.1 + (https://github.com/vmware/pyvmomi/pull/732) + + Args: + - obj (object): vim object + - properties (list, optional): list of properties following + the property collector specification, for example: + ["config.hardware.memoryMB", "name", "overallStatus"] + default is a complete object dump, which can be large + + Return: + dict + """ + if not HAS_PYVMOMIJSON: + self.module.fail_json(msg='The installed version of pyvmomi lacks JSON output support; need pyvmomi>6.7.1') + + result = dict() + if properties: + for prop in properties: + try: + if '.' in prop: + key, remainder = prop.split('.', 1) + tmp = dict() + tmp[key] = self._extract(self._jsonify(getattr(obj, key)), remainder) + self._deepmerge(result, tmp) + else: + result[prop] = self._jsonify(getattr(obj, prop)) + except (AttributeError, KeyError): + self.module.fail_json(msg="Property '{0}' not found.".format(prop)) + else: + result = self._jsonify(obj) + return result diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest_facts.py b/lib/ansible/modules/cloud/vmware/vmware_guest_facts.py index ea0facbba1..a9d6bc9749 100644 --- a/lib/ansible/modules/cloud/vmware/vmware_guest_facts.py +++ b/lib/ansible/modules/cloud/vmware/vmware_guest_facts.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # # This module is also sponsored by E.T.A.I. (www.etai.fr) +# Copyright (C) 2018 James E. King III (@jeking3) # 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 @@ -25,7 +26,7 @@ version_added: 2.3 author: - Loic Blot (@nerzhul) notes: - - Tested on vSphere 5.5 + - Tested on vSphere 5.5, 6.7 requirements: - "python >= 2.6" - PyVmomi @@ -79,6 +80,32 @@ options: default: 'no' type: bool version_added: '2.8' + schema: + description: + - Specify the output schema desired. + - The 'summary' output schema is the legacy output from the module + - The 'vsphere' output schema is the vSphere API class definition + which requires pyvmomi>6.7.1 + choices: ['summary', 'vsphere'] + default: 'summary' + type: str + version_added: '2.8' + properties: + description: + - Specify the properties to retrieve. + - If not specified, all properties are retrieved (deeply). + - Results are returned in a structure identical to the vsphere API. + - 'Example:' + - ' properties: [' + - ' "config.hardware.memoryMB",' + - ' "config.hardware.numCPU",' + - ' "guest.disk",' + - ' "overallStatus"' + - ' ]' + - Only valid when C(schema) is C(vsphere). + type: list + required: False + version_added: '2.8' extends_documentation_fragment: vmware.documentation ''' @@ -93,6 +120,19 @@ EXAMPLES = ''' uuid: 421e4592-c069-924d-ce20-7e7533fab926 delegate_to: localhost register: facts + +- name: Gather some facts from a guest using the vSphere API output schema + vmware_guest_facts: + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + validate_certs: no + datacenter: "{{ datacenter_name }}" + name: "{{ vm_name }}" + schema: "vsphere" + properties: ["config.hardware.memoryMB", "guest.disk", "overallStatus"] + delegate_to: localhost + register: facts ''' RETURN = """ @@ -168,11 +208,6 @@ except ImportError: HAS_VCLOUD = False -class PyVmomiHelper(PyVmomi): - def __init__(self, module): - super(PyVmomiHelper, self).__init__(module) - - class VmwareTag(VmwareRestClient): def __init__(self, module): super(VmwareTag, self).__init__(module) @@ -189,7 +224,9 @@ def main(): use_instance_uuid=dict(type='bool', default=False), folder=dict(type='str'), datacenter=dict(type='str', required=True), - tags=dict(type='bool', default=False) + tags=dict(type='bool', default=False), + schema=dict(type='str', choices=['summary', 'vsphere'], default='summary'), + properties=dict(type='list') ) module = AnsibleModule(argument_spec=argument_spec, required_one_of=[['name', 'uuid']]) @@ -199,14 +236,21 @@ def main(): # so we should leave the input folder path unmodified module.params['folder'] = module.params['folder'].rstrip('/') - pyv = PyVmomiHelper(module) + if module.params['schema'] != 'vsphere' and module.params.get('properties'): + module.fail_json(msg="The option 'properties' is only valid when the schema is 'vsphere'") + + pyv = PyVmomi(module) # Check if the VM exists before continuing vm = pyv.get_vm() # VM already exists if vm: try: - instance = pyv.gather_facts(vm) + if module.params['schema'] == 'summary': + instance = pyv.gather_facts(vm) + else: + instance = pyv.to_json(vm, module.params['properties']) + if module.params.get('tags'): if not HAS_VCLOUD: module.fail_json(msg="Unable to find 'vCloud Suite SDK' Python library which is required." diff --git a/test/integration/targets/vmware_guest_facts/tasks/main.yml b/test/integration/targets/vmware_guest_facts/tasks/main.yml index ade4d6283c..bdba6d4a59 100644 --- a/test/integration/targets/vmware_guest_facts/tasks/main.yml +++ b/test/integration/targets/vmware_guest_facts/tasks/main.yml @@ -1,7 +1,8 @@ # Test code for the vmware_guest_facts module. # Copyright: (c) 2017, Abhijeet Kasurde +# Copyright: (c) 2018, James E. King III (@jeking3) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - +--- - name: store the vcenter container ip set_fact: vcsim: "{{ lookup('env', 'vcenter_host') }}" @@ -62,7 +63,7 @@ folder: "{{ vm1 | dirname }}" register: guest_facts_0001 -- debug: msg="{{ guest_facts_0001 }}" +- debug: var=guest_facts_0001 - assert: that: @@ -98,7 +99,25 @@ uuid: "{{ vm1_uuid }}" register: guest_facts_0002 -- debug: msg="{{ guest_facts_0002 }}" +- debug: var=guest_facts_0002 + +- name: "Get specific details about virtual machines using the vsphere output schema" + vmware_guest_facts: + validate_certs: False + hostname: "{{ vcsim }}" + username: "{{ vcsim_instance['json']['username'] }}" + password: "{{ vcsim_instance['json']['password'] }}" + datacenter: "{{ dc1 | basename }}" + uuid: "{{ vm1_uuid }}" + schema: vsphere + properties: + - config.hardware.memoryMB + - guest + - name + - summary.runtime.connectionState + register: guest_facts_0002b + +- debug: var=guest_facts_0002b - assert: that: @@ -106,14 +125,10 @@ - "guest_facts_0002['instance']['hw_product_uuid'] is defined" - "guest_facts_0002['instance']['hw_product_uuid'] == vm1_uuid" - "guest_facts_0002['instance']['hw_cores_per_socket'] is defined" - - "guest_facts_0001['instance']['hw_datastores'] is defined" - - "guest_facts_0001['instance']['hw_esxi_host'] == h1 | basename" - - "guest_facts_0001['instance']['hw_files'] is defined" - - "guest_facts_0001['instance']['hw_guest_ha_state'] is defined" - - "guest_facts_0001['instance']['hw_is_template'] is defined" - - "guest_facts_0001['instance']['hw_folder'] is defined" - - "guest_facts_0001['instance']['guest_question'] is defined" - - "guest_facts_0001['instance']['guest_consolidation_needed'] is defined" + - "guest_facts_0002b['instance']['config']['hardware']['memoryMB'] is defined" + - "guest_facts_0002b['instance']['config']['hardware']['numCoresPerSocket'] is not defined" + - "guest_facts_0002b['instance']['guest']['toolsVersion'] is defined" + - "guest_facts_0002b['instance']['overallStatus'] is not defined" # Testcase 0003: Get details about virtual machines without snapshots using UUID - name: get empty list of snapshots from virtual machine using UUID @@ -126,7 +141,7 @@ uuid: "{{ vm1_uuid }}" register: guest_facts_0003 -- debug: msg="{{ guest_facts_0003 }}" +- debug: var=guest_facts_0003 - assert: that: @@ -169,7 +184,7 @@ # uuid: "{{ vm1_uuid }}" # register: guest_facts_0004 -#- debug: msg="{{ guest_facts_0004 }}" +#- debug: var=guest_facts_0004 #- assert: # that: