diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ac56ba5a..cba745b112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -220,6 +220,8 @@ Ansible Changes By Release - tempfile - tower: * tower_organization +- vmware_guest_facts +- vmware_guest_snapshot - web_infrastructure * jenkins_script - windows: diff --git a/lib/ansible/module_utils/vmware.py b/lib/ansible/module_utils/vmware.py index e681aa68d5..3f34d5621f 100644 --- a/lib/ansible/module_utils/vmware.py +++ b/lib/ansible/module_utils/vmware.py @@ -61,6 +61,7 @@ def find_dvspg_by_name(dv_switch, portgroup_name): return None + def find_entity_child_by_path(content, entityRootFolder, path): entity = entityRootFolder @@ -88,6 +89,7 @@ def find_cluster_by_name_datacenter(datacenter, cluster_name): return folder return None + def find_cluster_by_name(content, cluster_name, datacenter=None): if datacenter: @@ -102,6 +104,7 @@ def find_cluster_by_name(content, cluster_name, datacenter=None): return None + def find_datacenter_by_name(content, datacenter_name): datacenters = get_all_objs(content, [vim.Datacenter]) @@ -138,6 +141,7 @@ def find_hostsystem_by_name(content, hostname): return host return None + def find_vm_by_id(content, vm_id, vm_id_type="vm_name", datacenter=None, cluster=None): """ UUID is unique to a VM, every other id returns the first match. """ si = content.searchIndex @@ -147,7 +151,7 @@ def find_vm_by_id(content, vm_id, vm_id_type="vm_name", datacenter=None, cluster vm = si.FindByDnsName(datacenter=datacenter, dnsName=vm_id, vmSearch=True) elif vm_id_type == 'inventory_path': vm = si.FindByInventoryPath(inventoryPath=vm_id) - if type(vm) != type(vim.VirtualMachine): + if isinstance(vm, vim.VirtualMachine): vm = None elif vm_id_type == 'uuid': vm = si.FindByUuid(datacenter=datacenter, instanceUuid=vm_id, vmSearch=True) @@ -166,7 +170,7 @@ def find_vm_by_id(content, vm_id, vm_id_type="vm_name", datacenter=None, cluster def find_vm_by_name(content, vm_name, folder=None, recurse=True): - vms = get_all_objs(content, [vim.VirtualMachine], folder, recurse=True) + vms = get_all_objs(content, [vim.VirtualMachine], folder, recurse=recurse) for vm in vms: if vm.name == vm_name: return vm @@ -181,6 +185,113 @@ def find_host_portgroup_by_name(host, portgroup_name): return None +def gather_vm_facts(content, vm): + """ Gather facts from vim.VirtualMachine object. """ + facts = { + 'module_hw': True, + 'hw_name': vm.config.name, + 'hw_power_status': vm.summary.runtime.powerState, + 'hw_guest_full_name': vm.summary.guest.guestFullName, + 'hw_guest_id': vm.summary.guest.guestId, + 'hw_product_uuid': vm.config.uuid, + 'hw_processor_count': vm.config.hardware.numCPU, + 'hw_memtotal_mb': vm.config.hardware.memoryMB, + 'hw_interfaces': [], + 'ipv4': None, + 'ipv6': None, + 'annotation': vm.config.annotation, + 'customvalues': {}, + 'snapshots': [], + 'current_snapshot': None, + } + + cfm = content.customFieldsManager + # Resolve custom values + for value_obj in vm.summary.customValue: + kn = value_obj.key + if cfm is not None and cfm.field: + for f in cfm.field: + if f.key == value_obj.key: + kn = f.name + # Exit the loop immediately, we found it + break + + facts['customvalues'][kn] = value_obj.value + + net_dict = {} + for device in vm.guest.net: + net_dict[device.macAddress] = list(device.ipAddress) + + for k, v in iteritems(net_dict): + for ipaddress in v: + if ipaddress: + if '::' in ipaddress: + facts['ipv6'] = ipaddress + else: + facts['ipv4'] = ipaddress + + ethernet_idx = 0 + for idx, entry in enumerate(vm.config.hardware.device): + if not hasattr(entry, 'macAddress'): + continue + + factname = 'hw_eth' + str(ethernet_idx) + facts[factname] = { + 'addresstype': entry.addressType, + 'label': entry.deviceInfo.label, + 'macaddress': entry.macAddress, + 'ipaddresses': net_dict.get(entry.macAddress, None), + 'macaddress_dash': entry.macAddress.replace(':', '-'), + 'summary': entry.deviceInfo.summary, + } + facts['hw_interfaces'].append('eth' + str(ethernet_idx)) + ethernet_idx += 1 + + snapshot_facts = list_snapshots(vm) + if 'snapshots' in snapshot_facts: + facts['snapshots'] = snapshot_facts['snapshots'] + facts['current_snapshot'] = snapshot_facts['current_snapshot'] + return facts + + +def deserialize_snapshot_obj(obj): + return {'id': obj.id, + 'name': obj.name, + 'description': obj.description, + 'creation_time': obj.createTime, + 'state': obj.state} + + +def list_snapshots_recursively(snapshots): + snapshot_data = [] + for snapshot in snapshots: + snapshot_data.append(deserialize_snapshot_obj(snapshot)) + snapshot_data = snapshot_data + list_snapshots_recursively(snapshot.childSnapshotList) + return snapshot_data + + +def get_current_snap_obj(snapshots, snapob): + snap_obj = [] + for snapshot in snapshots: + if snapshot.snapshot == snapob: + snap_obj.append(snapshot) + snap_obj = snap_obj + get_current_snap_obj(snapshot.childSnapshotList, snapob) + return snap_obj + + +def list_snapshots(vm): + result = {} + if vm.snapshot is None: + return result + + result['snapshots'] = list_snapshots_recursively(vm.snapshot.rootSnapshotList) + current_snapref = vm.snapshot.currentSnapshot + current_snap_obj = get_current_snap_obj(vm.snapshot.rootSnapshotList, current_snapref) + result['current_snapshot'] = deserialize_snapshot_obj(current_snap_obj[0]) + + return result + + def vmware_argument_spec(): return dict( @@ -199,7 +310,8 @@ def connect_to_api(module, disconnect_atexit=True): validate_certs = module.params['validate_certs'] if validate_certs and not hasattr(ssl, 'SSLContext'): - module.fail_json(msg='pyVim does not support changing verification mode with python < 2.7.9. Either update python or or use validate_certs=false') + module.fail_json(msg='pyVim does not support changing verification mode with python < 2.7.9. Either update ' + 'python or or use validate_certs=false') try: service_instance = connect.SmartConnect(host=hostname, user=username, pwd=password) @@ -224,6 +336,7 @@ def connect_to_api(module, disconnect_atexit=True): atexit.register(connect.Disconnect, service_instance) return service_instance.RetrieveContent() + def get_all_objs(content, vimtype, folder=None, recurse=True): if not folder: folder = content.rootFolder @@ -234,6 +347,7 @@ def get_all_objs(content, vimtype, folder=None, recurse=True): obj.update({managed_object_ref: managed_object_ref.name}) return obj + def fetch_file_from_guest(content, vm, username, password, src, dest): """ Use VMWare's filemanager api to fetch a file over http """ @@ -282,6 +396,7 @@ def fetch_file_from_guest(content, vm, username, password, src, dest): return result + def push_file_to_guest(content, vm, username, password, src, dest, overwrite=True): """ Use VMWare's filemanager api to fetch a file over http """ @@ -331,6 +446,7 @@ def push_file_to_guest(content, vm, username, password, src, dest, overwrite=Tru return result + def run_command_in_guest(content, vm, username, password, program_path, program_args, program_cwd, program_env): result = {'failed': False} diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest.py b/lib/ansible/modules/cloud/vmware/vmware_guest.py index a8871080e6..c04f6bbbc5 100644 --- a/lib/ansible/modules/cloud/vmware/vmware_guest.py +++ b/lib/ansible/modules/cloud/vmware/vmware_guest.py @@ -132,11 +132,6 @@ options: - Add an optional C(dns_servers) or C(domain) entry per interface (Windows) - Add an optional C(device_type) to configure the virtual NIC (pcnet32, vmxnet2, vmxnet3, e1000, e1000e) version_added: "2.3" - snapshot_op: - description: - - A key, value pair of snapshot operation types and their additional required parameters. - - Beware that this functionality will disappear in v2.3 and move into module C(vmware_guest_snapshot) - version_added: "2.3" customization: description: - "Parameters to customize template" @@ -163,17 +158,6 @@ extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' -# Gather facts only - - name: gather the VM facts - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - validate_certs: no - esxi_hostname: 192.168.1.117 - uuid: 421e4592-c069-924d-ce20-7e7533fab926 - register: facts - # Create a VM from a template - name: create the VM vmware_guest: @@ -276,68 +260,6 @@ EXAMPLES = ''' password: vmware uuid: 421e4592-c069-924d-ce20-7e7533fab926 state: absent - -### Snapshot Operations - -# BEWARE: This functionality will move into vmware_guest_snapshot before release ! - -# Create snapshot - - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - name: dummy_vm - snapshot_op: - op_type: create - name: snap1 - description: snap1_description - -# Remove a snapshot - - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - name: dummy_vm - snapshot_op: - op_type: remove - name: snap1 - -# Revert to a snapshot - - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - name: dummy_vm - snapshot_op: - op_type: revert - name: snap1 - -# List all snapshots of a VM - - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - name: dummy_vm - snapshot_op: - op_type: list_all - -# List current snapshot of a VM - - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - name: dummy_vm - snapshot_op: - op_type: list_current - -# Remove all snapshots of a VM - - vmware_guest: - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - name: dummy_vm - snapshot_op: - op_type: remove_all ''' RETURN = """ @@ -356,7 +278,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception from ansible.module_utils.six import iteritems from ansible.module_utils.urls import fetch_url -from ansible.module_utils.vmware import get_all_objs, connect_to_api +from ansible.module_utils.vmware import get_all_objs, connect_to_api, gather_vm_facts try: import json @@ -611,66 +533,7 @@ class PyVmomiHelper(object): return result def gather_facts(self, vm): - """ Gather facts from vim.VirtualMachine object. """ - facts = { - 'module_hw': True, - 'hw_name': vm.config.name, - 'hw_power_status': vm.summary.runtime.powerState, - 'hw_guest_full_name': vm.summary.guest.guestFullName, - 'hw_guest_id': vm.summary.guest.guestId, - 'hw_product_uuid': vm.config.uuid, - 'hw_processor_count': vm.config.hardware.numCPU, - 'hw_memtotal_mb': vm.config.hardware.memoryMB, - 'hw_interfaces': [], - 'ipv4': None, - 'ipv6': None, - 'annotation': vm.config.annotation, - 'customvalues': {}, - } - - cfm = self.content.customFieldsManager - # Resolve customvalues - for value_obj in vm.summary.customValue: - kn = value_obj.key - if cfm is not None and cfm.field: - for f in cfm.field: - if f.key == value_obj.key: - kn = f.name - # Exit the loop immediately, we found it - break - - facts['customvalues'][kn] = value_obj.value - - netDict = {} - for device in vm.guest.net: - netDict[device.macAddress] = list(device.ipAddress) - - for k, v in iteritems(netDict): - for ipaddress in v: - if ipaddress: - if '::' in ipaddress: - facts['ipv6'] = ipaddress - else: - facts['ipv4'] = ipaddress - - ethernet_idx = 0 - for idx, entry in enumerate(vm.config.hardware.device): - if not hasattr(entry, 'macAddress'): - continue - - factname = 'hw_eth' + str(ethernet_idx) - facts[factname] = { - 'addresstype': entry.addressType, - 'label': entry.deviceInfo.label, - 'macaddress': entry.macAddress, - 'ipaddresses': netDict.get(entry.macAddress, None), - 'macaddress_dash': entry.macAddress.replace(':', '-'), - 'summary': entry.deviceInfo.summary, - } - facts['hw_interfaces'].append('eth' + str(ethernet_idx)) - ethernet_idx += 1 - - return facts + return gather_vm_facts(self.content, vm) def remove_vm(self, vm): # https://www.vmware.com/support/developer/converter-sdk/conv60_apireference/vim.ManagedEntity.html#destroy @@ -714,6 +577,7 @@ class PyVmomiHelper(object): elif vm_creation and not self.should_deploy_from_template(): self.module.fail_json(msg="hardware.memory_mb attribute is mandatory for VM creation") + def get_vm_network_interfaces(self, vm=None): if vm is None: return [] @@ -1283,7 +1147,7 @@ class PyVmomiHelper(object): return {'changed': change_applied, 'failed': True, 'msg': task.info.error.msg} # Rename VM - if self.params['uuid'] and self.params['name'] and self.params['name'] != vm.config.name: + if self.params['uuid'] and self.params['name'] and self.params['name'] != self.current_vm_obj.config.name: task = self.current_vm_obj.Rename_Task(self.params['name']) self.wait_for_task(task) change_applied = True @@ -1323,110 +1187,6 @@ class PyVmomiHelper(object): return facts - def list_snapshots_recursively(self, snapshots): - snapshot_data = [] - for snapshot in snapshots: - snap_text = 'Id: %s; Name: %s; Description: %s; CreateTime: %s; State: %s' % (snapshot.id, snapshot.name, - snapshot.description, - snapshot.createTime, - snapshot.state) - snapshot_data.append(snap_text) - snapshot_data = snapshot_data + self.list_snapshots_recursively(snapshot.childSnapshotList) - return snapshot_data - - def get_snapshots_by_name_recursively(self, snapshots, snapname): - snap_obj = [] - for snapshot in snapshots: - if snapshot.name == snapname: - snap_obj.append(snapshot) - else: - snap_obj = snap_obj + self.get_snapshots_by_name_recursively(snapshot.childSnapshotList, snapname) - return snap_obj - - def get_current_snap_obj(self, snapshots, snapob): - snap_obj = [] - for snapshot in snapshots: - if snapshot.snapshot == snapob: - snap_obj.append(snapshot) - snap_obj = snap_obj + self.get_current_snap_obj(snapshot.childSnapshotList, snapob) - return snap_obj - - def snapshot_vm(self, vm, guest, snapshot_op): - """ To perform snapshot operations create/remove/revert/list_all/list_current/remove_all """ - - snapshot_op_name = None - try: - snapshot_op_name = snapshot_op['op_type'] - except KeyError: - self.module.fail_json(msg="Specify op_type - create/remove/revert/list_all/list_current/remove_all") - - task = None - result = {} - - if snapshot_op_name not in ['create', 'remove', 'revert', 'list_all', 'list_current', 'remove_all']: - self.module.fail_json(msg="Specify op_type - create/remove/revert/list_all/list_current/remove_all") - - if snapshot_op_name != 'create' and vm.snapshot is None: - self.module.exit_json(msg="VM - %s doesn't have any snapshots" % guest) - - if snapshot_op_name == 'create': - try: - snapname = snapshot_op['name'] - except KeyError: - self.module.fail_json(msg="specify name & description(optional) to create a snapshot") - - if 'description' in snapshot_op: - snapdesc = snapshot_op['description'] - else: - snapdesc = '' - - dumpMemory = False - quiesce = False - task = vm.CreateSnapshot(snapname, snapdesc, dumpMemory, quiesce) - - elif snapshot_op_name in ['remove', 'revert']: - try: - snapname = snapshot_op['name'] - except KeyError: - self.module.fail_json(msg="specify snapshot name") - - snap_obj = self.get_snapshots_by_name_recursively(vm.snapshot.rootSnapshotList, snapname) - - # if len(snap_obj) is 0; then no snapshots with specified name - if len(snap_obj) == 1: - snap_obj = snap_obj[0].snapshot - if snapshot_op_name == 'remove': - task = snap_obj.RemoveSnapshot_Task(True) - else: - task = snap_obj.RevertToSnapshot_Task() - else: - self.module.exit_json( - msg="Couldn't find any snapshots with specified name: %s on VM: %s" % (snapname, guest)) - - elif snapshot_op_name == 'list_all': - snapshot_data = self.list_snapshots_recursively(vm.snapshot.rootSnapshotList) - result['snapshot_data'] = snapshot_data - - elif snapshot_op_name == 'list_current': - current_snapref = vm.snapshot.currentSnapshot - current_snap_obj = self.get_current_snap_obj(vm.snapshot.rootSnapshotList, current_snapref) - result['current_snapshot'] = 'Id: %s; Name: %s; Description: %s; CreateTime: %s; State: %s' % ( - current_snap_obj[0].id, - current_snap_obj[0].name, current_snap_obj[0].description, current_snap_obj[0].createTime, - current_snap_obj[0].state) - - elif snapshot_op_name == 'remove_all': - task = vm.RemoveAllSnapshots() - - if task: - self.wait_for_task(task) - if task.info.state == 'error': - result = {'changed': False, 'failed': True, 'msg': task.info.error.msg} - else: - result = {'changed': True, 'failed': False} - - return result - def get_obj(content, vimtype, name): """ @@ -1473,7 +1233,6 @@ def main(): 'absent', 'restarted', 'suspended', - 'gatherfacts', ], default='present'), validate_certs=dict(required=False, type='bool', default=True), @@ -1483,7 +1242,6 @@ def main(): customvalues=dict(required=False, type='list', default=[]), name=dict(required=True, type='str'), name_match=dict(required=False, type='str', default='first'), - snapshot_op=dict(required=False, type='dict', default={}), uuid=dict(required=False, type='str'), folder=dict(required=False, type='str', default='/vm'), guest_id=dict(required=False, type='str', default=None), @@ -1538,15 +1296,6 @@ def main(): result["changed"] = True if not tmp_result["failed"]: result["failed"] = False - elif module.params['state'] == 'gatherfacts': - # Run for facts only - try: - module.exit_json(instance=pyv.gather_facts(vm)) - except Exception: - e = get_exception() - module.fail_json(msg="Fact gather failed with exception %s" % e) - elif module.params['snapshot_op']: - result = pyv.snapshot_vm(vm, module.params['name'], module.params['snapshot_op']) else: # This should not happen assert False @@ -1555,8 +1304,6 @@ def main(): if module.params['state'] in ['poweredon', 'poweredoff', 'present', 'restarted', 'suspended']: # Create it ... result = pyv.deploy_vm() - elif module.params['state'] == 'gatherfacts': - module.fail_json(msg="Unable to gather facts for non-existing VM %(name)s" % module.params) if 'failed' not in result: result['failed'] = False diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest_facts.py b/lib/ansible/modules/cloud/vmware/vmware_guest_facts.py new file mode 100644 index 0000000000..2608c3951e --- /dev/null +++ b/lib/ansible/modules/cloud/vmware/vmware_guest_facts.py @@ -0,0 +1,214 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This module is also sponsored by E.T.A.I. (www.etai.fr) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + +DOCUMENTATION = ''' +--- +module: vmware_guest_facts +short_description: Gather facts about a single VM +description: + - Gather facts about a single VM on a VMware ESX cluster +version_added: 2.3 +author: + - Loic Blot (@nerzhul) +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + name: + description: + - Name of the VM to work with + required: True + name_match: + description: + - If multiple VMs matching the name, use the first or last found + default: 'first' + choices: ['first', 'last'] + uuid: + description: + - UUID of the instance to manage if known, this is VMware's unique identifier. + - This is required if name is not supplied. + folder: + description: + - Destination folder, absolute path to find an existing guest. + - This is required if name is supplied. + datacenter: + description: + - Destination datacenter for the deploy operation + required: True +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' +# Gather facts + - name: gather the VM facts + vmware_guest_facts: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + validate_certs: no + uuid: 421e4592-c069-924d-ce20-7e7533fab926 + register: facts +''' + +RETURN = """ +instance: + descripton: metadata about the virtualmachine + returned: always + type: dict + sample: None +""" + +import os +import time + +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six import iteritems +from ansible.module_utils.vmware import connect_to_api, find_vm_by_id, gather_vm_facts + +try: + import json +except ImportError: + import simplejson as json + +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 + + if uuid: + vm = find_vm_by_id(self.content, vm_id=uuid, vm_id_type="uuid") + elif folder: + # Build the absolute folder path to pass into the search method + if not self.params['folder'].startswith('/'): + self.module.fail_json(msg="Folder %(folder)s needs to be an absolute path, starting with '/'." % self.params) + searchpath = '%(datacenter)s%(folder)s' % self.params + + # get all objects for this path ... + f_obj = self.content.searchIndex.FindByInventoryPath(searchpath) + if f_obj: + if isinstance(f_obj, vim.Datacenter): + f_obj = f_obj.vmFolder + for c_obj in f_obj.childEntity: + if not isinstance(c_obj, vim.VirtualMachine): + continue + if c_obj.name == name: + vm = c_obj + if self.params['name_match'] == 'first': + break + + return vm + + def gather_facts(self, vm): + return gather_vm_facts(self.content, vm) + + +def get_obj(content, vimtype, name): + """ + Return an object by name, if name is None the + first found object is returned + """ + obj = None + container = content.viewManager.CreateContainerView( + content.rootFolder, vimtype, True) + for c in container.view: + if name: + if c.name == name: + obj = c + break + else: + obj = c + break + + container.Destroy() + return obj + + +def main(): + module = AnsibleModule( + argument_spec=dict( + hostname=dict( + type='str', + default=os.environ.get('VMWARE_HOST') + ), + username=dict( + type='str', + default=os.environ.get('VMWARE_USER') + ), + password=dict( + type='str', no_log=True, + default=os.environ.get('VMWARE_PASSWORD') + ), + validate_certs=dict(required=False, type='bool', default=True), + name=dict(required=True, type='str'), + name_match=dict(required=False, type='str', default='first'), + uuid=dict(required=False, type='str'), + folder=dict(required=False, type='str', default='/vm'), + datacenter=dict(required=True, type='str'), + ), + ) + + # Prepend /vm if it was missing from the folder path, also strip trailing slashes + if not module.params['folder'].startswith('/vm') and module.params['folder'].startswith('/'): + module.params['folder'] = '/vm%(folder)s' % module.params + 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']) + + # VM already exists + if vm: + try: + module.exit_json(instance=pyv.gather_facts(vm)) + except Exception: + e = get_exception() + module.fail_json(msg="Fact gather failed with exception %s" % e) + else: + module.fail_json(msg="Unable to gather facts for non-existing VM %(name)s" % module.params) + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest_snapshot.py b/lib/ansible/modules/cloud/vmware/vmware_guest_snapshot.py new file mode 100644 index 0000000000..17423c1d67 --- /dev/null +++ b/lib/ansible/modules/cloud/vmware/vmware_guest_snapshot.py @@ -0,0 +1,312 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This module is also sponsored by E.T.A.I. (www.etai.fr) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + +DOCUMENTATION = ''' +--- +module: vmware_guest_snapshot +short_description: Manages virtual machines snapshots in vcenter +description: + - Create virtual machines snapshots +version_added: 2.3 +author: + - James Tanner (@jctanner) + - Loic Blot (@nerzhul) +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + state: + description: + - Manage snapshots attached to a specific virtual machine. + required: True + choices: ['present', 'absent', 'revert', 'remove_all'] + name: + description: + - Name of the VM to work with + required: True + name_match: + description: + - If multiple VMs matching the name, use the first or last found + default: 'first' + choices: ['first', 'last'] + uuid: + description: + - UUID of the instance to manage if known, this is VMware's unique identifier. + - This is required if name is not supplied. + folder: + description: + - Define instance folder location. + datacenter: + description: + - Destination datacenter for the deploy operation + required: True + snapshot_name: + description: + - Sets the snapshot name to manage. + - This param is required only if state is not C(remove_all) + description: + description: + - Define an arbitrary description to attach to snapshot. +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' + - name: Create snapshot + vmware_guest_snapshot: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + name: dummy_vm + state: present + snapshot_name: snap1 + description: snap1_description + + - name: Remove a snapshot + vmware_guest_snapshot: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + name: dummy_vm + state: remove + snapshot_name: snap1 + + - name: Revert to a snapshot + vmware_guest_snapshot: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + name: dummy_vm + state: revert + snapshot_name: snap1 + + - name: Remove all snapshots of a VM + vmware_guest_snapshot: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + name: dummy_vm + state: remove_all +''' + +RETURN = """ +instance: + descripton: metadata about the new virtualmachine + returned: always + type: dict + sample: None +""" + +import os +import time + +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six import iteritems +from ansible.module_utils.vmware import connect_to_api + +try: + import json +except ImportError: + import simplejson as json + +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.si = None + self.content = connect_to_api(self.module) + self.change_detected = False + + def getvm(self, name=None, uuid=None, folder=None): + + # https://www.vmware.com/support/developer/vc-sdk/visdk2xpubs/ReferenceGuide/vim.SearchIndex.html + # self.si.content.searchIndex.FindByInventoryPath('DC1/vm/test_folder') + + vm = None + + if uuid: + vm = self.content.searchIndex.FindByUuid(uuid=uuid, vmSearch=True) + elif folder: + # Build the absolute folder path to pass into the search method + if not self.params['folder'].startswith('/'): + self.module.fail_json(msg="Folder %(folder)s needs to be an absolute path, starting with '/'." % self.params) + searchpath = '%(datacenter)s%(folder)s' % self.params + + # get all objects for this path ... + f_obj = self.content.searchIndex.FindByInventoryPath(searchpath) + if f_obj: + if isinstance(f_obj, vim.Datacenter): + f_obj = f_obj.vmFolder + for c_obj in f_obj.childEntity: + if not isinstance(c_obj, vim.VirtualMachine): + continue + if c_obj.name == name: + vm = c_obj + if self.params['name_match'] == 'first': + break + + return vm + + @staticmethod + def wait_for_task(task): + # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.Task.html + # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.TaskInfo.html + # https://github.com/virtdevninja/pyvmomi-community-samples/blob/master/samples/tools/tasks.py + while task.info.state not in ['success', 'error']: + time.sleep(1) + + def get_snapshots_by_name_recursively(self, snapshots, snapname): + snap_obj = [] + for snapshot in snapshots: + if snapshot.name == snapname: + snap_obj.append(snapshot) + else: + snap_obj = snap_obj + self.get_snapshots_by_name_recursively(snapshot.childSnapshotList, snapname) + return snap_obj + + def snapshot_vm(self, vm): + dump_memory = False + quiesce = False + return vm.CreateSnapshot(self.module.params["snapshot_name"], + self.module.params["description"], + dump_memory, + quiesce) + + def remove_or_revert_snapshot(self, vm): + if vm.snapshot is None: + self.module.exit_json(msg="VM - %s doesn't have any snapshots" % self.module.params["name"]) + + snap_obj = self.get_snapshots_by_name_recursively(vm.snapshot.rootSnapshotList, + self.module.params["snapshot_name"]) + task = None + if len(snap_obj) == 1: + snap_obj = snap_obj[0].snapshot + if self.module.params["state"] == "absent": + task = snap_obj.RemoveSnapshot_Task(True) + elif self.module.params["state"] == "revert": + task = snap_obj.RevertToSnapshot_Task() + else: + self.module.exit_json( + msg="Couldn't find any snapshots with specified name: %s on VM: %s" % + (self.module.params["snapshot_name"], self.module.params["name"])) + + return task + + def apply_snapshot_op(self, vm): + result = {} + if self.module.params["state"] == "present": + task = self.snapshot_vm(vm) + elif self.module.params["state"] in ["absent", "revert"]: + task = self.remove_or_revert_snapshot(vm) + elif self.module.params["state"] == "remove_all": + task = vm.RemoveAllSnapshots() + else: + # This should not happen + assert False + + if task: + self.wait_for_task(task) + if task.info.state == 'error': + result = {'changed': False, 'failed': True, 'msg': task.info.error.msg} + else: + result = {'changed': True, 'failed': False} + + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + hostname=dict( + type='str', + default=os.environ.get('VMWARE_HOST') + ), + username=dict( + type='str', + default=os.environ.get('VMWARE_USER') + ), + password=dict( + type='str', no_log=True, + default=os.environ.get('VMWARE_PASSWORD') + ), + state=dict( + required=False, + choices=['present', 'absent', 'revert', 'remove_all'], + default='present'), + validate_certs=dict(required=False, type='bool', default=True), + name=dict(required=True, type='str'), + name_match=dict(required=False, type='str', default='first'), + uuid=dict(required=False, type='str'), + folder=dict(required=False, type='str', default='/vm'), + datacenter=dict(required=True, type='str'), + snapshot_name=dict(required=False, type='str'), + description=dict(required=False, type='str', default=''), + ), + ) + + # Prepend /vm if it was missing from the folder path, also strip trailing slashes + if not module.params['folder'].startswith('/vm') and module.params['folder'].startswith('/'): + module.params['folder'] = '/vm%(folder)s' % module.params + 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: + module.fail_json(msg="Unable to manage snapshots for non-existing VM %(name)s" % module.params) + + if not module.params['snapshot_name'] and module.params['state'] != 'remove_all': + module.fail_json(msg="snapshot_name param is required when state is '%(state)s'" % module.params) + + result = pyv.apply_snapshot_op(vm) + + if 'failed' not in result: + result['failed'] = False + + if result['failed']: + module.fail_json(**result) + else: + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt index 6cfb723eba..a592c42fb5 100644 --- a/test/sanity/pep8/legacy-files.txt +++ b/test/sanity/pep8/legacy-files.txt @@ -25,7 +25,6 @@ lib/ansible/module_utils/f5.py lib/ansible/module_utils/facts.py lib/ansible/module_utils/known_hosts.py lib/ansible/module_utils/mysql.py -lib/ansible/module_utils/vmware.py lib/ansible/modules/cloud/amazon/_ec2_vpc.py lib/ansible/modules/cloud/amazon/aws_kms.py lib/ansible/modules/cloud/amazon/cloudformation.py