# -*- coding: utf-8 -*- # Copyright (c) 2024 Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = r""" module: xen_orchestra_instance short_description: Management of instances on Xen Orchestra description: - Allows you to create/delete/restart/stop instances on Xen Orchestra. version_added: 10.3.0 extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: none diff_mode: support: none options: api_host: description: API host to XOA API. required: true type: str user: description: Xen Orchestra user. required: true type: str password: description: Xen Orchestra password. required: true type: str validate_certs: description: Verify TLS certificate if using HTTPS. type: bool default: true use_tls: description: Use wss when connecting to the Xen Orchestra API. type: bool default: true state: description: - State in which the Virtual Machine should be. - If O(state=present) then O(template) and O(label) are required. - If O(state=absent), O(state=started), O(state=stopped) or O(state=restarted) then O(vm_uid) is required. - When state is O(present) then O(boot_after_create) can be used to boot the VM after creation. - There is no idempotence guarantee when O(state=present), a new VM will always be created. type: str choices: ['present', 'started', 'absent', 'stopped', 'restarted'] default: present vm_uid: description: - UID of the target Virtual Machine. Required when O(state=absent), O(state=started), O(state=stopped) or O(state=restarted). type: str label: description: Label of the Virtual Machine to create, can be used when O(state=present). type: str description: description: Description of the Virtual Machine to create, can be used when O(state=present). type: str template: description: - UID of a template to create Virtual Machine from. - Muse be provided when O(state=present). type: str boot_after_create: description: Boot Virtual Machine after creation, can be used when O(state=present). type: bool default: false requirements: - websocket-client >= 1.0.0 author: - Samori Gorse (@shinuza) seealso: - name: Xen Orchestra documentation description: Official documentation of Xen Orchestra CLI. link: https://docs.xen-orchestra.com/architecture#xo-cli-cli """ EXAMPLES = r""" - name: Create a new virtual machine community.general.xen_orchestra: api_host: xen-orchestra.lab user: user password: passw0rd validate_certs: false state: present template: 355ee47d-ff4c-4924-3db2-fd86ae629676-a3d70e4d-c5ac-4dfb-999b-30a0a7efe546 label: This is a test from ansible description: This is a test from ansible boot_after_create: false - name: Start an existing virtual machine community.general.xen_orchestra: api_host: xen-orchestra.lab user: user password: passw0rd validate_certs: false state: started - name: Stop an existing virtual machine community.general.xen_orchestra: api_host: xen-orchestra.lab user: user password: passw0rd validate_certs: false state: stopped - name: Restart an existing virtual machine community.general.xen_orchestra: api_host: xen-orchestra.lab user: user password: passw0rd validate_certs: false state: stopped - name: Delete a virtual machine community.general.xen_orchestra: api_host: xen-orchestra.lab user: user password: passw0rd validate_certs: false state: absent """ import json import ssl import traceback from time import sleep from ansible_collections.community.general.plugins.module_utils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib # 3rd party imports try: HAS_WEBSOCKET = True WEBSOCKET_IMP_ERR = None import websocket from websocket import create_connection if LooseVersion(websocket.__version__) < LooseVersion('1.0.0'): raise ImportError except ImportError: WEBSOCKET_IMP_ERR = traceback.format_exc() HAS_WEBSOCKET = False OBJECT_NOT_FOUND = 1 VM_STATE_ERROR = 13 class XenOrchestra(object): CALL_TIMEOUT = 100 '''Number of 1/10ths of a second to wait before method call times out.''' def __init__(self, module): # from config self.counter = -1 self.con = None self.module = module self.create_connection(module.params['api_host']) self.login(module.params['user'], module.params['password']) @property def pointer(self): self.counter += 1 return self.counter def create_connection(self, xoa_api_host): validate_certs = self.module.params['validate_certs'] use_tls = self.module.params['use_tls'] proto = 'wss' if use_tls else 'ws' sslopt = None if validate_certs else {'cert_reqs': ssl.CERT_NONE} self.conn = create_connection( '{0}://{1}/api/'.format(proto, xoa_api_host), sslopt=sslopt) def call(self, method, params): '''Calls a method on the XO server with the provided parameters.''' pointer = self.pointer self.conn.send(json.dumps({ 'id': pointer, 'jsonrpc': '2.0', 'method': method, 'params': params })) waited = 0 while waited < self.CALL_TIMEOUT: response = json.loads(self.conn.recv()) if response.get('id') == pointer: return response else: sleep(0.1) waited += 1 raise self.module.fail_json( 'Method call {method} timed out after {timeout} seconds.'.format(method=method, timeout=self.CALL_TIMEOUT / 10)) def login(self, user, password): answer = self.call('session.signIn', { 'username': user, 'password': password }) if 'error' in answer: raise self.module.fail_json( 'Could not connect: {0}'.format(answer['error'])) return answer['result'] def create_vm(self): params = { 'template': self.module.params['template'], 'name_label': self.module.params['label'], 'bootAfterCreate': self.module.params.get('boot_after_create', False) } description = self.module.params.get('description') if description: params['name_description'] = description answer = self.call('vm.create', params) if 'error' in answer: raise self.module.fail_json( 'Could not create vm: {0}'.format(answer['error'])) return answer['result'] def restart_vm(self, vm_uid): answer = self.call('vm.restart', {'id': vm_uid, 'force': True}) if 'error' in answer: raise self.module.fail_json( 'Could not restart vm: {0}'.format(answer['error'])) return answer['result'] def stop_vm(self, vm_uid): answer = self.call('vm.stop', {'id': vm_uid, 'force': True}) if 'error' in answer: # VM is not paused, suspended or running if answer['error']['code'] == VM_STATE_ERROR: return False raise self.module.fail_json( 'Could not stop vm: {0}'.format(answer['error'])) return answer['result'] def start_vm(self, vm_uid): answer = self.call('vm.start', {'id': vm_uid}) if 'error' in answer: # VM is already started, nothing to do if answer['error']['code'] == VM_STATE_ERROR: return False raise self.module.fail_json( 'Could not start vm: {0}'.format(answer['error'])) return answer['result'] def delete_vm(self, vm_uid): answer = self.call('vm.delete', {'id': vm_uid}) if 'error' in answer: if answer['error']['code'] == OBJECT_NOT_FOUND: return False raise self.module.fail_json( 'Could not delete vm: {0}'.format(answer['error'])) return answer['result'] def main(): module_args = dict( api_host=dict(type='str', required=True), user=dict(type='str', required=True), password=dict(type='str', required=True, no_log=True), validate_certs=dict(type='bool', default=True), use_tls=dict(type='bool', default=True), template=dict(type='str'), vm_uid=dict(type='str'), label=dict(type='str'), description=dict(type='str'), boot_after_create=dict(type='bool', default=False), state=dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted']), ) module = AnsibleModule( argument_spec=module_args, required_if=[ ('state', 'present', ['template', 'label']), ('state', 'absent', ('vm_uid',)), ('state', 'started', ('vm_uid',)), ('state', 'restarted', ('vm_uid',)), ('state', 'stopped', ('vm_uid',)), ], ) if HAS_WEBSOCKET is False: module.fail_json(msg=missing_required_lib('websocket-client'), exception=WEBSOCKET_IMP_ERR) xen_orchestra = XenOrchestra(module) state = module.params['state'] vm_uid = module.params['vm_uid'] if state == 'stopped': result = xen_orchestra.stop_vm(vm_uid) module.exit_json(changed=result) if state == 'started': result = xen_orchestra.start_vm(vm_uid) module.exit_json(changed=result) if state == 'restarted': result = xen_orchestra.restart_vm(vm_uid) module.exit_json(changed=result) if state == 'absent': result = xen_orchestra.delete_vm(vm_uid) module.exit_json(changed=result) if state == 'present': result = xen_orchestra.create_vm() module.exit_json(changed=False, vm_uid=result) if __name__ == '__main__': main()