From 02b5c7a8a3db3a634b812d9925210e970f2df337 Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Mon, 18 Sep 2017 16:59:07 +0530 Subject: [PATCH] New module - vmware_guest_powerstate Fix adds a new module 'vmware_guest_powerstate' to manage power states of virtual machine. Fixes: #30371 Signed-off-by: Abhijeet Kasurde --- lib/ansible/module_utils/vmware.py | 107 +++++++++++++++ .../modules/cloud/vmware/vmware_guest.py | 117 ++--------------- .../cloud/vmware/vmware_guest_powerstate.py | 124 ++++++++++++++++++ .../targets/vmware_guest_powerstate/aliases | 4 + .../vmware_guest_powerstate/tasks/main.yml | 17 +++ .../tasks/poweroff_d1_c1_f0.yml | 55 ++++++++ .../tasks/poweroff_d1_c1_f1.yml | 67 ++++++++++ 7 files changed, 384 insertions(+), 107 deletions(-) create mode 100644 lib/ansible/modules/cloud/vmware/vmware_guest_powerstate.py create mode 100644 test/integration/targets/vmware_guest_powerstate/aliases create mode 100644 test/integration/targets/vmware_guest_powerstate/tasks/main.yml create mode 100644 test/integration/targets/vmware_guest_powerstate/tasks/poweroff_d1_c1_f0.yml create mode 100644 test/integration/targets/vmware_guest_powerstate/tasks/poweroff_d1_c1_f1.yml diff --git a/lib/ansible/module_utils/vmware.py b/lib/ansible/module_utils/vmware.py index 02062c3a438..2029a511ee0 100644 --- a/lib/ansible/module_utils/vmware.py +++ b/lib/ansible/module_utils/vmware.py @@ -34,6 +34,7 @@ except ImportError: from ansible.module_utils._text import to_text from ansible.module_utils.urls import fetch_url from ansible.module_utils.six import integer_types, iteritems, string_types +from ansible.module_utils._text import to_text class TaskError(Exception): @@ -652,3 +653,109 @@ def find_host_by_cluster_datacenter(module, content, datacenter_name, cluster_na return host, cluster return None, cluster + + +def set_vm_power_state(content, vm, state, force): + """ + Set the power status for a VM determined by the current and + requested states. force is forceful + """ + facts = gather_vm_facts(content, vm) + expected_state = state.replace('_', '').lower() + current_state = facts['hw_power_status'].lower() + result = dict( + changed=False, + failed=False, + ) + + # Need Force + if not force and current_state not in ['poweredon', 'poweredoff']: + result['failed'] = True + result['msg'] = "Virtual Machine is in %s power state. Force is required!" % current_state + return result + + # State is not already true + if current_state != expected_state: + task = None + try: + if expected_state == 'poweredoff': + task = vm.PowerOff() + + elif expected_state == 'poweredon': + task = vm.PowerOn() + + elif expected_state == 'restarted': + if current_state in ('poweredon', 'poweringon', 'resetting', 'poweredoff'): + task = vm.Reset() + else: + result['failed'] = True + result['msg'] = "Cannot restart virtual machine in the current state %s" % current_state + + elif expected_state == 'suspended': + if current_state in ('poweredon', 'poweringon'): + task = vm.Suspend() + else: + result['failed'] = True + result['msg'] = 'Cannot suspend virtual machine in the current state %s' % current_state + + elif expected_state in ['shutdownguest', 'rebootguest']: + if current_state == 'poweredon': + if vm.guest.toolsRunningStatus == 'guestToolsRunning': + if expected_state == 'shutdownguest': + task = vm.ShutdownGuest() + else: + task = vm.RebootGuest() + # Set result['changed'] immediately because + # shutdown and reboot return None. + result['changed'] = True + else: + result['failed'] = True + result['msg'] = "VMware tools should be installed for guest shutdown/reboot" + else: + result['failed'] = True + result['msg'] = "Virtual machine %s must be in poweredon state for guest shutdown/reboot" % vm.name + + except Exception as e: + result['failed'] = True + result['msg'] = to_text(e) + + if task: + wait_for_task(task) + if task.info.state == 'error': + result['failed'] = True + result['msg'] = task.info.error.msg + else: + result['changed'] = True + + # need to get new metadata if changed + if result['changed']: + result['instance'] = gather_vm_facts(content, vm) + + return result + + +class PyVmomi(object): + def __init__(self, module): + if not HAS_PYVMOMI: + module.fail_json(msg='PyVmomi Python module required. Install using "pip install PyVmomi"') + + self.module = module + self.params = module.params + self.si = None + self.current_vm_obj = None + self.content = connect_to_api(self.module) + + def get_vm(self): + vm = None + match_first = (self.params['name_match'] == 'first') + + if self.params['uuid']: + vm = find_vm_by_id(self.content, vm_id=self.params['uuid'], vm_id_type="uuid") + elif self.params['folder'] and self.params['name']: + vm = find_vm_by_id(self.content, vm_id=self.params['name'], vm_id_type="inventory_path", + folder=self.params['folder'], match_first=match_first) + + if vm: + self.current_vm_obj = vm + + return vm diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest.py b/lib/ansible/modules/cloud/vmware/vmware_guest.py index 7a2de068e97..c4dfad2f02f 100644 --- a/lib/ansible/modules/cloud/vmware/vmware_guest.py +++ b/lib/ansible/modules/cloud/vmware/vmware_guest.py @@ -314,9 +314,9 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_text -from ansible.module_utils.vmware import (connect_to_api, find_obj, gather_vm_facts, get_all_objs, - compile_folder_path_for_object, serialize_spec, find_vm_by_id, - vmware_argument_spec) +from ansible.module_utils.vmware import (find_obj, gather_vm_facts, get_all_objs, + compile_folder_path_for_object, serialize_spec, + vmware_argument_spec, set_vm_power_state, PyVmomi) class PyVmomiDeviceHelper(object): @@ -496,112 +496,15 @@ class PyVmomiCache(object): return datacenter -class PyVmomiHelper(object): +class PyVmomiHelper(PyVmomi): def __init__(self, module): - if not HAS_PYVMOMI: - module.fail_json(msg='pyvmomi module required') - - self.module = module + super(PyVmomiHelper, self).__init__(module) self.device_helper = PyVmomiDeviceHelper(self.module) - self.params = module.params - self.si = None - self.content = connect_to_api(self.module) self.configspec = None self.change_detected = False self.customspec = None - self.current_vm_obj = None self.cache = PyVmomiCache(self.content, dc_name=self.params['datacenter']) - 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) - if vm: - self.current_vm_obj = vm - - return vm - - def set_powerstate(self, vm, state, force): - """ - Set the power status for a VM determined by the current and - requested states. force is forceful - """ - facts = self.gather_facts(vm) - expected_state = state.replace('_', '').lower() - current_state = facts['hw_power_status'].lower() - result = dict( - changed=False, - failed=False, - ) - - # Need Force - if not force and current_state not in ['poweredon', 'poweredoff']: - result['failed'] = True - result['msg'] = "VM is in %s power state. Force is required!" % current_state - return result - - # State is not already true - if current_state != expected_state: - task = None - try: - if expected_state == 'poweredoff': - task = vm.PowerOff() - - elif expected_state == 'poweredon': - task = vm.PowerOn() - - elif expected_state == 'restarted': - if current_state in ('poweredon', 'poweringon', 'resetting', 'poweredoff'): - task = vm.Reset() - else: - result['failed'] = True - result['msg'] = "Cannot restart VM in the current state %s" % current_state - - elif expected_state == 'suspended': - if current_state in ('poweredon', 'poweringon'): - task = vm.Suspend() - else: - result['failed'] = True - result['msg'] = 'Cannot suspend VM in the current state %s' % current_state - - elif expected_state in ['shutdownguest', 'rebootguest']: - if current_state == 'poweredon' and vm.guest.toolsRunningStatus == 'guestToolsRunning': - if expected_state == 'shutdownguest': - task = vm.ShutdownGuest() - else: - task = vm.RebootGuest() - # Set result['changed'] immediately because - # shutdown and reboot return None. - result['changed'] = True - else: - result['failed'] = True - result['msg'] = "VM %s must be in poweredon state & tools should be installed for guest shutdown/reboot" % vm.name - - except Exception as e: - result['failed'] = True - result['msg'] = to_text(e) - - if task: - self.wait_for_task(task) - if task.info.state == 'error': - result['failed'] = True - result['msg'] = str(task.info.error.msg) - else: - result['changed'] = True - - # need to get new metadata if changed - if result['changed']: - newvm = self.getvm(uuid=vm.config.uuid) - facts = self.gather_facts(newvm) - result['instance'] = facts - - return result - def gather_facts(self, vm): return gather_vm_facts(self.content, vm) @@ -1336,7 +1239,7 @@ class PyVmomiHelper(object): self.customize_customvalues(vm_obj=vm) if self.params['wait_for_ip_address'] or self.params['state'] in ['poweredon', 'restarted']: - self.set_powerstate(vm, 'poweredon', force=False) + set_vm_power_state(self.content, vm, 'poweredon', force=False) if self.params['wait_for_ip_address']: self.wait_for_vm_ip(vm) @@ -1422,7 +1325,7 @@ class PyVmomiHelper(object): facts = {} thispoll = 0 while not ips and thispoll <= poll: - newvm = self.getvm(uuid=vm.config.uuid) + newvm = self.get_vm() facts = self.gather_facts(newvm) if facts['ipv4'] or facts['ipv6']: ips = True @@ -1477,7 +1380,7 @@ def main(): 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 = pyv.get_vm() # VM already exists if vm: @@ -1485,13 +1388,13 @@ def main(): # destroy it if module.params['force']: # has to be poweredoff first - pyv.set_powerstate(vm, 'poweredoff', module.params['force']) + set_vm_power_state(pyv.content, vm, 'poweredoff', module.params['force']) result = pyv.remove_vm(vm) elif module.params['state'] == 'present': result = pyv.reconfigure_vm() elif module.params['state'] in ['poweredon', 'poweredoff', 'restarted', 'suspended', 'shutdownguest', 'rebootguest']: # set powerstate - tmp_result = pyv.set_powerstate(vm, module.params['state'], module.params['force']) + tmp_result = set_vm_power_state(pyv.content, vm, module.params['state'], module.params['force']) if tmp_result['changed']: result["changed"] = True if not tmp_result["failed"]: diff --git a/lib/ansible/modules/cloud/vmware/vmware_guest_powerstate.py b/lib/ansible/modules/cloud/vmware/vmware_guest_powerstate.py new file mode 100644 index 00000000000..a9d69c165fb --- /dev/null +++ b/lib/ansible/modules/cloud/vmware/vmware_guest_powerstate.py @@ -0,0 +1,124 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, Abhijeet Kasurde +# 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 = r''' +--- +module: vmware_guest_powerstate +short_description: Manages power states of virtual machines in vCenter +description: +- Power on / Power off / Restart a virtual machine. +version_added: '2.5' +author: +- Abhijeet Kasurde (@akasurde) +requirements: +- python >= 2.6 +- PyVmomi +options: + state: + description: + - Set the state of the virtual machine. + choices: [ powered-off, powered-on, reboot-guest, restarted, shutdown-guest, suspended ] + name: + description: + - Name of the virtual machine to work with. + - Virtual machine names in vCenter are not necessarily unique, which may be problematic, see C(name_match). + name_match: + description: + - If multiple virtual machines 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 or relative path to find an existing guest or create the new guest. + - The folder should include the datacenter. ESX'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' + - ' folder: vm/folder2' + - ' folder: folder2' + default: /vm +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = r''' +- name: Set the state of a virtual machine to poweroff + vmware_guest_powerstate: + hostname: 192.0.2.44 + username: administrator@vsphere.local + password: vmware + validate_certs: no + folder: /testvms + name: testvm_2 + state: powered-off + delegate_to: localhost + register: deploy +''' + +RETURN = r''' # ''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.vmware import PyVmomi, set_vm_power_state, vmware_argument_spec + + +def main(): + argument_spec = vmware_argument_spec() + argument_spec.update( + state=dict(type='str', default='present', + choices=['powered-off', 'powered-on', 'reboot-guest', 'restarted', 'shutdown-guest', 'suspended']), + name=dict(type='str'), + name_match=dict(type='str', choices=['first', 'last'], default='first'), + uuid=dict(type='str'), + folder=dict(type='str', default='/vm'), + force=dict(type='bool', default=False), + ) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=False, + mutually_exclusive=[ + ['name', 'uuid'], + ], + ) + + result = dict(changed=False,) + + pyv = PyVmomi(module) + + # Check if the VM exists before continuing + vm = pyv.get_vm() + + if vm: + # VM already exists, so set power state + result = set_vm_power_state(pyv.content, vm, module.params['state'], module.params['force']) + else: + module.fail_json(msg="Unable to set power state for non-existing virtual machine : '%s'" % (module.params.get('uuid') or module.params.get('name'))) + + if result.get('failed') is True: + module.fail_json(**result) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/vmware_guest_powerstate/aliases b/test/integration/targets/vmware_guest_powerstate/aliases new file mode 100644 index 00000000000..748b11da6d3 --- /dev/null +++ b/test/integration/targets/vmware_guest_powerstate/aliases @@ -0,0 +1,4 @@ +posix/ci/cloud/group1/vcenter +cloud/vcenter +skip/python3 +destructive diff --git a/test/integration/targets/vmware_guest_powerstate/tasks/main.yml b/test/integration/targets/vmware_guest_powerstate/tasks/main.yml new file mode 100644 index 00000000000..64771eb634c --- /dev/null +++ b/test/integration/targets/vmware_guest_powerstate/tasks/main.yml @@ -0,0 +1,17 @@ +# +# Copyright: (c) 2017, Abhijeet Kasurde +# 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 + when: "{{ ansible_user_id == 'root' }}" + +- name: store the vcenter container ip + set_fact: + vcsim: "{{ lookup('env', 'vcenter_host') }}" +- debug: var=vcsim + +- include: poweroff_d1_c1_f0.yml +- include: poweroff_d1_c1_f1.yml \ No newline at end of file diff --git a/test/integration/targets/vmware_guest_powerstate/tasks/poweroff_d1_c1_f0.yml b/test/integration/targets/vmware_guest_powerstate/tasks/poweroff_d1_c1_f0.yml new file mode 100644 index 00000000000..b1b7948edab --- /dev/null +++ b/test/integration/targets/vmware_guest_powerstate/tasks/poweroff_d1_c1_f0.yml @@ -0,0 +1,55 @@ +# +# Copyright: (c) 2017, Abhijeet Kasurde +# 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: 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 with no folders + uri: + url: "{{ 'http://' + vcsim + ':5000/spawn?datacenter=1&cluster=1&folder=0' }}" + register: vcsim_instance + +- name: Wait for Flask controller to come up online + wait_for: + host: "{{ vcsim }}" + port: 443 + state: started + +- name: get a list of VMS from vcsim + uri: + url: "{{ 'http://' + vcsim + ':5000/govc_find?filter=VM' }}" + register: vmlist + +- debug: var=vcsim_instance +- debug: var=vmlist + +- name: set state to poweroff on all VMs + vmware_guest_powerstate: + validate_certs: False + hostname: "{{ vcsim }}" + username: "{{ vcsim_instance['json']['username'] }}" + password: "{{ vcsim_instance['json']['password'] }}" + name: "{{ item|basename }}" + state: powered-off + folder: "{{ item|dirname }}" + with_items: "{{ vmlist['json'] }}" + register: poweroff_d1_c1_f0 + +- debug: var=poweroff_d1_c1_f0 + +- name: make sure no changes were made + assert: + that: + - "poweroff_d1_c1_f0.results|map(attribute='changed')|unique|list == [False]" diff --git a/test/integration/targets/vmware_guest_powerstate/tasks/poweroff_d1_c1_f1.yml b/test/integration/targets/vmware_guest_powerstate/tasks/poweroff_d1_c1_f1.yml new file mode 100644 index 00000000000..49abcc86c2d --- /dev/null +++ b/test/integration/targets/vmware_guest_powerstate/tasks/poweroff_d1_c1_f1.yml @@ -0,0 +1,67 @@ +# +# Copyright: (c) 2017, Abhijeet Kasurde +# 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: Wait for Flask controller to come up online + wait_for: + host: "{{ vcsim }}" + port: 5000 + state: started + +- name: store the vcenter container ip + set_fact: + vcsim: "{{ lookup('env', 'vcenter_host') }}" +- debug: var=vcsim + +- name: kill vcsim + uri: + url: "{{ 'http://' + vcsim + ':5000/killall' }}" + +- name: start vcsim with folders + uri: + url: "{{ 'http://' + vcsim + ':5000/spawn?datacenter=1&cluster=1&folder=1' }}" + register: vcsim_instance + +- name: Wait for Flask controller to come up online + wait_for: + host: "{{ vcsim }}" + port: 443 + state: started + +- name: get a list of VMS from vcsim + uri: + url: "{{ 'http://' + vcsim + ':5000/govc_find?filter=VM' }}" + register: vmlist + +- debug: var=vcsim_instance +- debug: var=vmlist + +# https://github.com/ansible/ansible/issues/25011 +# Sending "-folders 1" to vcsim nests the datacenter under +# the folder so that the path prefix is no longer /vm +# +# /F0/DC0/vm/F0/DC0_H0_VM0 + +- name: set state to poweredoff on all VMs + vmware_guest_powerstate: + validate_certs: False + hostname: "{{ vcsim }}" + username: "{{ vcsim_instance['json']['username'] }}" + password: "{{ vcsim_instance['json']['password'] }}" + name: "{{ item|basename }}" + state: powered-off + folder: "{{ item|dirname }}" + with_items: "{{ vmlist['json'] }}" + register: poweroff_d1_c1_f1 + +- debug: var=poweroff_d1_c1_f1 + +- name: make sure no changes were made + assert: + that: + - "poweroff_d1_c1_f1.results|map(attribute='changed')|unique|list == [False]"