Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New xen orchestra module #8470

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/BOTMETA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,8 @@ files:
maintainers: dinoocch the-maldridge
$modules/xcc_:
maintainers: panyy3 renxulei
$modules/xen_orchestra_instance.py:
maintainers: shinuza
$modules/xenserver_:
maintainers: bvitnik
$modules/xenserver_facts.py:
Expand Down
326 changes: 326 additions & 0 deletions plugins/modules/xen_orchestra_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
# -*- 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 = '''
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: 9.2.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.
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
O(state=restarted)
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Muse be provided when O(state=present)
- Must 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) <[email protected]>
'''


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: stop

- 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.'''
id = self.pointer
self.conn.send(json.dumps({
'id': id,
'jsonrpc': '2.0',
'method': method,
'params': params
}))

waited = 0
while waited < self.CALL_TIMEOUT:
response = json.loads(self.conn.recv())
if 'id' in response and response['id'] == id:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to:

Suggested change
if 'id' in response and response['id'] == id:
if response.get('id') == id:

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']
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a boolean value? Because it is being passed back to the user as changed.



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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are idempotency checks (is the VM already stopped)?

Copy link
Contributor Author

@shinuza shinuza Jun 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See line 230, we set the task as changed=false if the api returns an error saying the VM is not in a paused, suspended or running state (meaning it's already stopped)

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the VM is already present and no configuration changed, this should be False for idempotency.



if __name__ == '__main__':
main()