Last active
August 24, 2020 13:36
-
-
Save frock81/16265f9c40399d96b4bebe942ca78969 to your computer and use it in GitHub Desktop.
Ansible module libvirt_domain
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# -*- coding: utf-8 -*- | |
# Copyright: (c) 2020, Fabio Rocha <frock81@yahoo.com.br> | |
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | |
ANSIBLE_METADATA = { | |
'metadata_version': '1.1', | |
'status': ['preview'], | |
'supported_by': 'community' | |
} | |
DOCUMENTATION = ''' | |
--- | |
module: libvirt_domain | |
short_description: Manages a Libvirt domain | |
version_added: "2.9" | |
description: | |
- "Manages domain resources (cpus, memory, etc), persistence, state and | |
autostart." | |
options: | |
name: | |
description: | |
- Name of the Libvirt managed domain. | |
required: true | |
type: str | |
domain_type: | |
description: | |
- Type of domain. | |
required: False | |
default: kvm | |
choices: | |
- kvm | |
- qemu | |
- xen | |
- lxc | |
type: str | |
state: | |
description: | |
- The desired state for the domain. | |
- For I(state=stopped) see I(stop_method). | |
- For I(state=restarted) see I(restart_method). | |
required: false | |
default: running | |
choices: | |
- running | |
- stopped | |
- restarted | |
- paused | |
- saved | |
type: str | |
stop_method: | |
description: | |
- "The method used to stop the domain. Shutdown and destroy | |
are analogous to Libvirt. It may succeed or not | |
depending on the guest." | |
- "Module (default) will try to shutdown and if it do not | |
succeed until a timeout it will destroy." | |
required: false | |
default: module | |
choices: | |
- module | |
- shutdown | |
- destroy | |
type: str | |
restart_method: | |
description: | |
- The method used to restart the domain. | |
- "For I(persistent=no) the valid methods are | |
I(reboot_method=reboot) or I(reboot_method=reset)." | |
- "Module (default) will try to shutdown and if it do not | |
succeed until a timeout it will destroy and start|create | |
accordingly to the desired state." | |
required: false | |
default: module | |
choices: | |
- module | |
- reboot | |
- reset | |
type: str | |
apply_changes: | |
description: | |
- "Some domain modifications are not applied until the next | |
restart of the domain. Restart calling restart (either | |
with reboot or reset Libvirt command) is not enough. If | |
this parameter is set to yes, the module will try a | |
shutdown followed by a destroy if a timeout expires." | |
required: false | |
default: yes | |
type: bool | |
poll: | |
description: | |
- Poll interval to watch changes to take effect in seconds. | |
required: false | |
default: 1 | |
type: int | |
delay: | |
description: | |
- Delay interval before polling for changes in seconds. | |
required: false | |
default: 1 | |
type: int | |
timeout: | |
description: | |
- "Timeout for operations (stop|restart|apply_changes) to complete | |
in seconds." | |
required: false | |
default: 120 | |
type: int | |
save_file: | |
description: | |
- The file where to save domain state. | |
- "Required when I(state=saved) or when one want to restore a saved | |
domain with I(state=running)." | |
required: false | |
type: 'str' | |
persistent: | |
description: | |
- The domain should be persisted or not. | |
required: false | |
default: yes | |
type: bool | |
autostart: | |
description: | |
- The domain should be started with the host or not. | |
required: false | |
default: yes | |
type: bool | |
resources: | |
description: | |
- The domain resources like cpus, memory, etc. | |
required: false | |
default: | |
uuid: "{{ name | to_uuid }}" | |
cpu: host | |
vcpus: 1 | |
maxcpus: 1 | |
maxmemory: 512 | |
memory: 512 | |
memory_unit: MiB | |
console: yes | |
type: dict | |
suboptions: | |
uuid: "{{ name | to_uuid }}" | |
cpu: host | |
vcpus: 1 | |
maxcpus: 1 | |
maxmemory: 512 | |
memory: 512 | |
memory_unit: MiB | |
console: yes | |
extends_documentation_fragment: [] | |
requirements: | |
- python >= 2.6 | |
- libvirt-python | |
author: | |
- Fabio Rocha (@frock81) | |
''' | |
EXAMPLES = ''' | |
# Make a persistent domain with default resources and start it. | |
- name: Create a domain with default options | |
libvirt_domain: | |
name: vm-foo | |
''' | |
RETURN = ''' # ''' | |
from ansible.module_utils.basic import AnsibleModule | |
import re | |
import libvirt | |
class LibvirtConnector: | |
"""Connector to communicate with Libvirt | |
Attributes: | |
_conn (libvirt.virConnect): libvirt connection | |
Methods: | |
close_connection | |
""" | |
def __init__(self, connection_uri=None): | |
self._conn = libvirt.open(connection_uri) | |
def close_connection(self): | |
"""Close Libvirt connection""" | |
self._conn.close() | |
class LibvirtDomain(): | |
"""Libvirt domain class | |
Attributes: | |
exists (bool): if the domain exists in the hypervisor | |
vir_domain (libvirt.virDomain|None): libvirt domain returned by | |
lookup. | |
""" | |
def __init__(self, libvirt_connector, domain_name): | |
try: | |
# If the domain is not found an error will be raised. | |
self.vir_domain = libvirt_connector._conn.lookupByName(domain_name) | |
self.exists = True | |
except libvirt.libvirtError as e: | |
if str(e).find('Domain not found') != -1: | |
self.vir_domain = None | |
self.exists = False | |
return | |
raise | |
class ParameterValidationError(Exception): | |
"""Parameters validatiorn errors | |
Parameters: | |
parameter (str): parameter name | |
message (str): custom message | |
""" | |
def __init__(self, parameter, message=''): | |
self.parameter = parameter | |
if not message: | |
self.message = "The parameter '{0}' could not be validated."\ | |
.format(parameter) | |
else: | |
self.message = message | |
super().__init__(self.message) | |
def validate_user_input(domain_name, **opt_args): | |
"""Validate user input | |
Parameters: | |
domain_name (str): domain name | |
Other Parameters: | |
Returns: | |
(bool, str, str): Returns true or false depdending if validation | |
succeeded or not, the attribute which failed to validade, in | |
the case of validation failure, and an error message. | |
""" | |
domain_name_validation = validate_user_input_domain_name(domain_name) | |
if not domain_name_validation[0]: | |
raise ParameterValidationError(parameter='name', | |
message=domain_name_validation[1]) | |
return (True, '', '') | |
def validate_user_input_domain_name(domain_name): | |
"""Validate domain name | |
Parameters: | |
name (str): the value of the domain name to be validated | |
Returns: | |
(bool, str): wheter the name was validated or not together with an | |
error message in the case it did not validate. | |
""" | |
# Libvirt documentation stats that the domain name must consist of | |
# alpha-numeric characters only, but it works with dashes and | |
# underscores. | |
regexp = '^[a-zA-Z0-9-_]+$' | |
if not re.search(regexp, domain_name): | |
return (False, "The domain name must contain only alphanumeric, dashes " | |
"or underscores characters, got: '{0}'".format(domain_name)) | |
return (True, '') | |
def run_module(): | |
# define available arguments/parameters a user can pass to the module | |
module_args = dict( | |
name=dict( | |
type='str', | |
required=True | |
), | |
domain_type=dict( | |
required=False, | |
default='kvm', | |
choices=[ | |
'kvm', | |
'qemu', | |
'xen', | |
'lxc' | |
], | |
type=str | |
), | |
state=dict( | |
required=False, | |
default='running', | |
choices=[ | |
'running', | |
'stopped', | |
'restarted', | |
'paused', | |
'saved' | |
], | |
type='str' | |
), | |
stop_method=dict( | |
required=False, | |
default='module', | |
choices=[ | |
'module', | |
'shutdown', | |
'destroy' | |
], | |
type='str' | |
), | |
restart_method=dict( | |
required=False, | |
default='module', | |
choices=[ | |
'module', | |
'reboot', | |
'reset' | |
], | |
type='str' | |
), | |
apply_changes=dict( | |
required=False, | |
default=True, | |
type='bool' | |
), | |
poll=dict( | |
required=False, | |
default=1, | |
type='int' | |
), | |
delay=dict( | |
required=False, | |
default=1, | |
type='int' | |
), | |
timeout=dict( | |
required=False, | |
default=240, | |
type='int' | |
), | |
save_file=dict( | |
required=False, | |
type='str' | |
), | |
persistent=dict( | |
required=False, | |
default=True, | |
type='bool' | |
), | |
autostart=dict( | |
required=False, | |
default=True, | |
type='bool' | |
), | |
resources=dict( | |
required=False, | |
default=dict( | |
uuid="", | |
cpu='host', | |
vcpus=1, | |
maxcpus=1, | |
maxmemory=512, | |
memory=512, | |
memory_unit='MiB', | |
console=True | |
), | |
type='dict', | |
suboptions=dict( | |
uuid="", | |
cpu='host', | |
vcpus=1, | |
maxcpus=1, | |
maxmemory=512, | |
memory=512, | |
memory_unit='MiB', | |
console=True | |
) | |
) | |
) | |
# the AnsibleModule object will be our abstraction working with Ansible | |
# this includes instantiation, a couple of common attr would be the | |
# args/params passed to the execution, as well as if the module | |
# supports check mode | |
module = AnsibleModule( | |
argument_spec=module_args, | |
supports_check_mode=False | |
) | |
domain_name = module.params['name'] | |
# seed the result dict in the object | |
# we primarily care about changed and state | |
# change is if this module effectively modified the target | |
# state will include any data that you want your module to pass back | |
# for consumption, for example, in a subsequent task | |
result = dict( | |
changed=False, | |
name=domain_name, | |
meta=module.params | |
) | |
try: | |
#======================================================================= | |
# Early user input validation layer. | |
#======================================================================= | |
# The part not handled by the module. | |
validate_user_input(domain_name=domain_name) | |
#======================================================================= | |
# Information gathering layer. | |
#======================================================================= | |
try: | |
libvirt_connector = LibvirtConnector() | |
except libvirt.libvirtError as e: | |
module.fail_json(msg=str(e), **result) | |
# Check if domain exists. | |
# domain_exists = check_domain_existance( | |
# libvirt_connection=libvirt_connector._conn, | |
# domain_name=domain_name | |
# ) | |
libvirt_domain = LibvirtDomain(libvirt_connector=libvirt_connector, | |
domain_name=domain_name) | |
module.exit_json(vir_domain=str(libvirt_domain.vir_domain), **result) | |
#======================================================================= | |
# Validation for transitions. | |
#======================================================================= | |
#======================================================================= | |
# Couser of action layer. | |
#======================================================================= | |
#--------------------------------------------------------------- | |
# Persistence | |
#--------------------------------------------------------------- | |
#--------------------------------------------------------------- | |
# State | |
#--------------------------------------------------------------- | |
#--------------------------------------------------------------- | |
# Resources | |
#--------------------------------------------------------------- | |
#--------------------------------------------------------------- | |
# Autostart | |
#--------------------------------------------------------------- | |
#======================================================================= | |
# Operations validation, last layer. | |
#======================================================================= | |
#======================================================================= | |
# Apply operations. | |
#======================================================================= | |
except ParameterValidationError as e: | |
module.fail_json(msg=str(e), **result) | |
finally: | |
if libvirt_connector: | |
libvirt_connector.close_connection() | |
# in the event of a successful module execution, you will want to | |
# simple AnsibleModule.exit_json(), passing the key/value results | |
module.exit_json(**result) | |
def main(): | |
run_module() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment