Added xen_orchestra module

This commit is contained in:
Samori Gorse 2024-06-06 15:31:58 +02:00
commit 1e12684eb9

View file

@ -1,232 +1,312 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2021 Ansible Project # Copyright (c) 2021 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) # 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 # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
name: xen_orchestra name: xen_orchestra
short_description: Xen Orchestra inventory source short_description: Management of instances on Xen Orchestra
version_added: 4.1.0 version_added: 4.1.0
author: author:
- Dom Del Nano (@ddelnano) <ddelnano@gmail.com> - Samori Gorse (@shinuza) <samorigorse@gmail.com>
- Samori Gorse (@shinuza) <samorigorse@gmail.com> requirements:
requirements: - websocket-client >= 1.0.0
- websocket-client >= 1.0.0 description:
description: - Allows you to create/delete/restart/stop instances on Xen Orchestra
- Get inventory hosts from a Xen Orchestra deployment. options:
- 'Uses a configuration file as an inventory source, it must end in C(.xen_orchestra.yml) or C(.xen_orchestra.yaml).' api_host:
extends_documentation_fragment: description: API host to XOA API.
- constructed type: str
- inventory_cache user:
options: description: Xen Orchestra user.
plugin: required: true
description: The name of this plugin, it should always be set to V(community.general.xen_orchestra) for this plugin to recognize it as its own. type: str
required: true password:
choices: ['community.general.xen_orchestra'] description: Xen Orchestra password.
type: str required: true
api_host: type: str
description: validate_certs:
- API host to XOA API. description: Verify TLS certificate if using HTTPS.
- If the value is not specified in the inventory configuration, the value of environment variable E(ANSIBLE_XO_HOST) will be used instead. type: boolean
type: str default: true
env: use_ssl:
- name: ANSIBLE_XO_HOST description: Use wss when connecting to the Xen Orchestra API
user: type: boolean
description: default: true
- Xen Orchestra user. state:
- If the value is not specified in the inventory configuration, the value of environment variable E(ANSIBLE_XO_USER) will be used instead. description: State in which the Virtual Machine should be
required: true choices: ['present', 'started', 'absent', 'stopped', 'restarted']
type: str default: present
env: label:
- name: ANSIBLE_XO_USER description: Label of the Virtual Machine to create, can be used when O(state=present)
password: type: boolean
description: default: false
- Xen Orchestra password. description:
- If the value is not specified in the inventory configuration, the value of environment variable E(ANSIBLE_XO_PASSWORD) will be used instead. description: Description of the Virtual Machine to create, can be used when O(state=present)
required: true type: boolean
type: str default: false
env: boot_after_create:
- name: ANSIBLE_XO_PASSWORD description: Boot Virtual Machine after creation, can be used when O(state=present)
validate_certs: type: boolean
description: Verify TLS certificate if using HTTPS. default: false
type: boolean '''
default: true
use_ssl:
description: Use wss when connecting to the Xen Orchestra API EXAMPLES = r'''
type: boolean - name: Create a new virtual machine
default: true community.general.xen_orchestra:
''' api_host: xen-orchestra.lab
user: user
password: passw0rd
EXAMPLES = ''' validate_certs: no
# file must be named xen_orchestra.yaml or xen_orchestra.yml state: present
plugin: community.general.xen_orchestra template: 355ee47d-ff4c-4924-3db2-fd86ae629676-a3d70e4d-c5ac-4dfb-999b-30a0a7efe546
api_host: 192.168.1.255 label: This is a test from ansible
user: xo description: This is a test from ansible
password: xo_pwd boot_after_create: no
validate_certs: true
use_ssl: true - name: Start an existing virtual machine
groups: community.general.xen_orchestra:
kube_nodes: "'kube_node' in tags" api_host: xen-orchestra.lab
compose: user: user
ansible_port: 2222 password: passw0rd
validate_certs: no
''' state: started
import json - name: Stop an existing virtual machine
import ssl community.general.xen_orchestra:
from time import sleep api_host: xen-orchestra.lab
user: user
from ansible.errors import AnsibleError password: passw0rd
validate_certs: no
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion state: stop
from ansible.module_utils.basic import AnsibleModule
- name: Restart an existing virtual machine
# 3rd party imports community.general.xen_orchestra:
try: api_host: xen-orchestra.lab
HAS_WEBSOCKET = True user: user
import websocket password: passw0rd
from websocket import create_connection validate_certs: no
state: stopped
if LooseVersion(websocket.__version__) <= LooseVersion('1.0.0'):
raise ImportError - name: Delete a virtual machine
except ImportError as e: community.general.xen_orchestra:
HAS_WEBSOCKET = False api_host: xen-orchestra.lab
user: user
password: passw0rd
HALTED = 'Halted' validate_certs: no
PAUSED = 'Paused' state: absent
RUNNING = 'Running' '''
SUSPENDED = 'Suspended'
POWER_STATES = [RUNNING, HALTED, SUSPENDED, PAUSED] import json
HOST_GROUP = 'xo_hosts' import ssl
POOL_GROUP = 'xo_pools' from time import sleep
from ansible.errors import AnsibleError
def clean_group_name(label):
return label.lower().replace(' ', '-').replace('-', '_') from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule
class XenOrchestra(object): # 3rd party imports
''' Host inventory parser for ansible using XenOrchestra as source. ''' try:
HAS_WEBSOCKET = True
NAME = 'community.general.xen_orchestra' import websocket
CALL_TIMEOUT = 100 from websocket import create_connection
"""Number of 1/10ths of a second to wait before method call times out."""
if LooseVersion(websocket.__version__) <= LooseVersion('1.0.0'):
raise ImportError
def __init__(self, module): except ImportError as e:
# from config HAS_WEBSOCKET = False
self.counter = -1
self.con = None OBJECT_NOT_FOUND = 1
self.module = module VM_STATE_ERROR = 13
self.create_connection(module['api_host'])
self.login(module['user'], module['password']) class XenOrchestra(object):
''' Host inventory parser for ansible using XenOrchestra as source. '''
@property
def pointer(self): NAME = 'community.general.xen_orchestra'
self.counter += 1 CALL_TIMEOUT = 100
return self.counter '''Number of 1/10ths of a second to wait before method call times out.'''
def create_connection(self, xoa_api_host):
validate_certs = self.module['validate_certs'] def __init__(self, module):
use_ssl = self.module['use_ssl'] # from config
proto = 'wss' if use_ssl else 'ws' self.counter = -1
self.con = None
sslopt = None if validate_certs else {'cert_reqs': ssl.CERT_NONE} self.module = module
self.conn = create_connection(
'{0}://{1}/api/'.format(proto, xoa_api_host), sslopt=sslopt) self.create_connection(module.params['api_host'])
self.login(module.params['user'], module.params['password'])
def call(self, method, params):
"""Calls a method on the XO server with the provided parameters.""" @property
id = self.pointer def pointer(self):
self.conn.send(json.dumps({ self.counter += 1
'id': id, return self.counter
'jsonrpc': '2.0',
'method': method, def create_connection(self, xoa_api_host):
'params': params validate_certs = self.module.params['validate_certs']
})) use_ssl = self.module.params['use_ssl']
proto = 'wss' if use_ssl else 'ws'
waited = 0
while waited < self.CALL_TIMEOUT: sslopt = None if validate_certs else {'cert_reqs': ssl.CERT_NONE}
response = json.loads(self.conn.recv()) self.conn = create_connection(
if 'id' in response and response['id'] == id: '{0}://{1}/api/'.format(proto, xoa_api_host), sslopt=sslopt)
return response
else: def call(self, method, params):
sleep(0.1) '''Calls a method on the XO server with the provided parameters.'''
waited += 1 id = self.pointer
self.conn.send(json.dumps({
raise AnsibleError( 'id': id,
'Method call {method} timed out after {timeout} seconds.'.format(method=method, timeout=self.CALL_TIMEOUT / 10)) 'jsonrpc': '2.0',
'method': method,
def login(self, user, password): 'params': params
answer = self.call('session.signIn', { }))
'username': user, 'password': password
}) waited = 0
while waited < self.CALL_TIMEOUT:
if 'error' in answer: response = json.loads(self.conn.recv())
raise AnsibleError( if 'id' in response and response['id'] == id:
'Could not connect: {0}'.format(answer['error'])) return response
else:
def stop_vm(self, vm_uid): sleep(0.1)
answer = self.call('vm.stop', {'id': vm_uid}) waited += 1
if 'error' in answer: raise AnsibleError(
raise AnsibleError( 'Method call {method} timed out after {timeout} seconds.'.format(method=method, timeout=self.CALL_TIMEOUT / 10))
'Could not request: {0}'.format(answer['error']))
def login(self, user, password):
return answer['result'] answer = self.call('session.signIn', {
'username': user, 'password': password
def start_vm(self, vm_uid): })
answer = self.call('vm.start', {'id': vm_uid})
if 'error' in answer:
if 'error' in answer: raise self.module.fail_json(
raise AnsibleError( 'Could not connect: {0}'.format(answer['error']))
'Could not request: {0}'.format(answer['error']))
return answer['result']
return answer['result']
def create_vm(self):
def get_object(self, name): params = {
answer = self.call('xo.getAllObjects', {'filter': {'type': name}}) 'template': self.module.params['template'],
'name_label': self.module.params['label'],
if 'error' in answer: 'bootAfterCreate': self.module.params.get('boot_after_create', False)
raise AnsibleError( }
'Could not request: {0}'.format(answer['error']))
description = self.module.params.get('description')
return answer['result'] if description:
params['name_description'] = description
def main(): answer = self.call('vm.create', params)
module_args = dict(
api_host=dict(type='str', required=True), if 'error' in answer:
user=dict(type='str', required=True), raise self.module.fail_json(
password=dict(type='str', required=True, no_log=True), 'Could not create vm: {0}'.format(answer['error']))
validate_certs=dict(type='bool', default=True),
use_ssl=dict(type='bool', default=True), return answer['result']
vm_uid=dict(type='str'),
state=dict(default='present', choices=['absent', 'stopped', 'started', 'restarted']), def restart_vm(self, vm_uid):
) answer = self.call('vm.restart', {'id': vm_uid, 'force': True })
xen_orchestra = XenOrchestra() if 'error' in answer:
module = AnsibleModule( raise self.module.fail_json(
argument_spec=module_args, 'Could not restart vm: {0}'.format(answer['error']))
required_one_of=[('api_password', 'api_token_id')],
) return answer['result']
state = module.params['state'] def stop_vm(self, vm_uid):
vm_uid = module.params['vm_uid'] answer = self.call('vm.stop', {'id': vm_uid, 'force': True})
if state == 'stopped': if 'error' in answer:
xen_orchestra.stop_vm(vm_uid) # VM is not paused, suspended or running
module.exit_json(changed=True) if answer['error']['code'] == VM_STATE_ERROR:
return False
if state == 'started': raise self.module.fail_json(
xen_orchestra.start_vm(vm_uid) 'Could not stop vm: {0}'.format(answer['error']))
module.exit_json(changed=True)
return answer['result']
if __name__ == '__main__':
main() 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():
if not HAS_WEBSOCKET:
raise AnsibleError('This module requires websocket-client 1.0.0 or higher: '
'https://github.com/websocket-client/websocket-client.')
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_ssl=dict(type='bool', default=True),
vm_uid=dict(type='str'),
template=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')),
],
)
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=True, vm_uid=result)
if __name__ == '__main__':
main()