diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/libvirt/plugins | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/libvirt/plugins')
10 files changed, 2754 insertions, 0 deletions
diff --git a/ansible_collections/community/libvirt/plugins/connection/libvirt_lxc.py b/ansible_collections/community/libvirt/plugins/connection/libvirt_lxc.py new file mode 100644 index 000000000..eca94a0f0 --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/connection/libvirt_lxc.py @@ -0,0 +1,183 @@ +# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com> +# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com> +# (c) 2013, Michael Scherer <misc@zarb.org> +# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com> +# (c) 2017 Ansible Project +# 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 + +DOCUMENTATION = ''' +--- +author: + - Michael Scherer (@mscherer) <misc@zarb.org> +name: libvirt_lxc +short_description: Run tasks in lxc containers via libvirt +description: + - Run commands or put/fetch files to an existing lxc container using libvirt. +options: + remote_addr: + description: + - Container identifier. + default: The set user as per docker's configuration + vars: + - name: ansible_host + - name: ansible_libvirt_lxc_host +''' + +import os +import os.path +import subprocess +import traceback + +from ansible import constants as C +from ansible.errors import AnsibleError +from ansible.module_utils.six.moves import shlex_quote +from ansible.module_utils.common.process import get_bin_path +from ansible.module_utils._text import to_bytes +from ansible.plugins.connection import ConnectionBase, BUFSIZE +from ansible.utils.display import Display + +display = Display() + + +class Connection(ConnectionBase): + ''' Local lxc based connections ''' + + transport = 'community.general.libvirt_lxc' + has_pipelining = True + # su currently has an undiagnosed issue with calculating the file + # checksums (so copy, for instance, doesn't work right) + # Have to look into that before re-enabling this + default_user = 'root' + has_tty = False + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + self.lxc = self._play_context.remote_addr + + self.virsh = self._search_executable('virsh') + + self._check_domain(self.lxc) + + def _search_executable(self, executable): + try: + return get_bin_path(executable) + except ValueError: + raise AnsibleError("%s command not found in PATH") % executable + + def _check_domain(self, domain): + p = subprocess.Popen([self.virsh, '-q', '-c', 'lxc:///', 'dominfo', to_bytes(domain)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.communicate() + if p.returncode: + raise AnsibleError("%s is not a lxc defined in libvirt" % domain) + + def _connect(self): + ''' connect to the lxc; nothing to do here ''' + super(Connection, self)._connect() + if not self._connected: + display.vvv("THIS IS A LOCAL LXC DIR", host=self.lxc) + self._connected = True + + def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE): + ''' run a command on the chroot. This is only needed for implementing + put_file() get_file() so that we don't have to read the whole file + into memory. + + compared to exec_command() it looses some niceties like being able to + return the process's exit code immediately. + ''' + executable = C.DEFAULT_EXECUTABLE.split()[0] if C.DEFAULT_EXECUTABLE else '/bin/sh' + local_cmd = [self.virsh, '-q', '-c', 'lxc:///', 'lxc-enter-namespace'] + + if C.DEFAULT_LIBVIRT_LXC_NOSECLABEL: + local_cmd += ['--noseclabel'] + + local_cmd += [self.lxc, '--', executable, '-c', cmd] + + display.vvv("EXEC %s" % (local_cmd,), host=self.lxc) + local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd] + p = subprocess.Popen(local_cmd, shell=False, stdin=stdin, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + return p + + def exec_command(self, cmd, in_data=None, sudoable=False): + ''' run a command on the chroot ''' + super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) + + p = self._buffered_exec_command(cmd) + + stdout, stderr = p.communicate(in_data) + return (p.returncode, stdout, stderr) + + def _prefix_login_path(self, remote_path): + ''' Make sure that we put files into a standard path + + If a path is relative, then we need to choose where to put it. + ssh chooses $HOME but we aren't guaranteed that a home dir will + exist in any given chroot. So for now we're choosing "/" instead. + This also happens to be the former default. + + Can revisit using $HOME instead if it's a problem + ''' + if not remote_path.startswith(os.path.sep): + remote_path = os.path.join(os.path.sep, remote_path) + return os.path.normpath(remote_path) + + def put_file(self, in_path, out_path): + ''' transfer a file from local to lxc ''' + super(Connection, self).put_file(in_path, out_path) + display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.lxc) + + out_path = shlex_quote(self._prefix_login_path(out_path)) + try: + with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file: + if not os.fstat(in_file.fileno()).st_size: + count = ' count=0' + else: + count = '' + try: + p = self._buffered_exec_command('dd of=%s bs=%s%s' % (out_path, BUFSIZE, count), stdin=in_file) + except OSError: + raise AnsibleError("chroot connection requires dd command in the chroot") + try: + stdout, stderr = p.communicate() + except Exception: + traceback.print_exc() + raise AnsibleError("failed to transfer file %s to %s" % (in_path, out_path)) + if p.returncode != 0: + raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr)) + except IOError: + raise AnsibleError("file or module does not exist at: %s" % in_path) + + def fetch_file(self, in_path, out_path): + ''' fetch a file from lxc to local ''' + super(Connection, self).fetch_file(in_path, out_path) + display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.lxc) + + in_path = shlex_quote(self._prefix_login_path(in_path)) + try: + p = self._buffered_exec_command('dd if=%s bs=%s' % (in_path, BUFSIZE)) + except OSError: + raise AnsibleError("chroot connection requires dd command in the chroot") + + with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb+') as out_file: + try: + chunk = p.stdout.read(BUFSIZE) + while chunk: + out_file.write(chunk) + chunk = p.stdout.read(BUFSIZE) + except Exception: + traceback.print_exc() + raise AnsibleError("failed to transfer file %s to %s" % (in_path, out_path)) + stdout, stderr = p.communicate() + if p.returncode != 0: + raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr)) + + def close(self): + ''' terminate the connection; nothing to do here ''' + super(Connection, self).close() + self._connected = False diff --git a/ansible_collections/community/libvirt/plugins/connection/libvirt_qemu.py b/ansible_collections/community/libvirt/plugins/connection/libvirt_qemu.py new file mode 100644 index 000000000..220c02283 --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/connection/libvirt_qemu.py @@ -0,0 +1,361 @@ +# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com> +# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com> +# (c) 2013, Michael Scherer <misc@zarb.org> +# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com> +# (c) 2017 Ansible Project +# 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 + +DOCUMENTATION = """ +--- +author: + - Jesse Pretorius (@odyssey4me) <jesse@odyssey4.me> +name: libvirt_qemu +short_description: Run tasks on libvirt/qemu virtual machines +description: + - Run commands or put/fetch files to libvirt/qemu virtual machines using the qemu agent API. +notes: + - Currently DOES NOT work with selinux set to enforcing in the VM. + - Requires the qemu-agent installed in the VM. + - Requires access to the qemu-ga commands guest-exec, guest-exec-status, guest-file-close, guest-file-open, guest-file-read, guest-file-write. +extends_documentation_fragment: + - community.libvirt.requirements +version_added: "2.10.0" +options: + remote_addr: + description: Virtual machine name. + default: inventory_hostname + vars: + - name: ansible_host + executable: + description: + - Shell to use for execution inside container. + - Set this to 'cmd' or 'powershell' for Windows VMs. + default: /bin/sh + vars: + - name: ansible_shell_type + virt_uri: + description: Libvirt URI to connect to to access the virtual machine. + default: qemu:///system + vars: + - name: ansible_libvirt_uri +""" + +import base64 +import json +import shlex +import time +import traceback + +try: + import libvirt + import libvirt_qemu +except ImportError as imp_exc: + LIBVIRT_IMPORT_ERROR = imp_exc +else: + LIBVIRT_IMPORT_ERROR = None + +from ansible import constants as C +from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound +from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.six import raise_from +from ansible.plugins.connection import ConnectionBase, BUFSIZE +from ansible.plugins.shell.powershell import _parse_clixml +from ansible.utils.display import Display +from functools import partial +from os.path import exists, getsize + +display = Display() + + +REQUIRED_CAPABILITIES = [ + {'enabled': True, 'name': 'guest-exec', 'success-response': True}, + {'enabled': True, 'name': 'guest-exec-status', 'success-response': True}, + {'enabled': True, 'name': 'guest-file-close', 'success-response': True}, + {'enabled': True, 'name': 'guest-file-open', 'success-response': True}, + {'enabled': True, 'name': 'guest-file-read', 'success-response': True}, + {'enabled': True, 'name': 'guest-file-write', 'success-response': True} +] + + +class Connection(ConnectionBase): + ''' Local libvirt qemu based connections ''' + + transport = 'community.libvirt.libvirt_qemu' + # TODO(odyssey4me): + # Figure out why pipelining does not work and fix it + has_pipelining = False + has_tty = False + + def __init__(self, play_context, new_stdin, *args, **kwargs): + if LIBVIRT_IMPORT_ERROR: + raise_from( + AnsibleError('libvirt python bindings must be installed to use this plugin'), + LIBVIRT_IMPORT_ERROR) + + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + + self._host = self._play_context.remote_addr + + # Windows operates differently from a POSIX connection/shell plugin, + # we need to set various properties to ensure SSH on Windows continues + # to work + # Ensure that any Windows hosts in your inventory have one of the + # following set, in order to trigger this code: + # ansible_shell_type: cmd + # ansible_shell_type: powershell + if getattr(self._shell, "_IS_WINDOWS", False): + self.has_native_async = True + self.always_pipeline_modules = True + self.module_implementation_preferences = ('.ps1', '.exe', '') + self.allow_executable = False + + def _connect(self): + ''' connect to the virtual machine; nothing to do here ''' + super(Connection, self)._connect() + if not self._connected: + + self._virt_uri = self.get_option('virt_uri') + + self._display.vvv(u"CONNECT TO {0}".format(self._virt_uri), host=self._host) + try: + self.conn = libvirt.open(self._virt_uri) + except libvirt.libvirtError as err: + raise AnsibleConnectionFailure(to_native(err)) + + self._display.vvv(u"FIND DOMAIN {0}".format(self._host), host=self._host) + try: + self.domain = self.conn.lookupByName(self._host) + except libvirt.libvirtError as err: + raise AnsibleConnectionFailure(to_native(err)) + + request_cap = json.dumps({'execute': 'guest-info'}) + response_cap = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_cap, 5, 0)) + self.capabilities = response_cap['return']['supported_commands'] + self._display.vvvvv(u"GUEST CAPABILITIES: {0}".format(self.capabilities), host=self._host) + missing_caps = [] + for cap in REQUIRED_CAPABILITIES: + if cap not in self.capabilities: + missing_caps.append(cap['name']) + if len(missing_caps) > 0: + self._display.vvv(u"REQUIRED CAPABILITIES MISSING: {0}".format(missing_caps), host=self._host) + raise AnsibleConnectionFailure('Domain does not have required capabilities') + + display.vvv(u"ESTABLISH {0} CONNECTION".format(self.transport), host=self._host) + self._connected = True + + def exec_command(self, cmd, in_data=None, sudoable=True): + """ execute a command on the virtual machine host """ + super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) + + self._display.vvv(u"EXEC {0}".format(cmd), host=self._host) + + cmd_args_list = shlex.split(to_native(cmd, errors='surrogate_or_strict')) + + if getattr(self._shell, "_IS_WINDOWS", False): + # Become method 'runas' is done in the wrapper that is executed, + # need to disable sudoable so the bare_run is not waiting for a + # prompt that will not occur + sudoable = False + + # Generate powershell commands + cmd_args_list = self._shell._encode_script(cmd, as_list=True, strict_mode=False, preserve_rc=False) + + # TODO(odyssey4me): + # Implement buffering much like the other connection plugins + # Implement 'env' for the environment settings + # Implement 'input-data' for whatever it might be useful for + request_exec = { + 'execute': 'guest-exec', + 'arguments': { + 'path': cmd_args_list[0], + 'capture-output': True, + 'arg': cmd_args_list[1:] + } + } + request_exec_json = json.dumps(request_exec) + + display.vvv(u"GA send: {0}".format(request_exec_json), host=self._host) + + # TODO(odyssey4me): + # Add timeout parameter + result_exec = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_exec_json, 5, 0)) + + display.vvv(u"GA return: {0}".format(result_exec), host=self._host) + + command_start = time.clock_gettime(time.CLOCK_MONOTONIC) + + request_status = { + 'execute': 'guest-exec-status', + 'arguments': { + 'pid': result_exec['return']['pid'] + } + } + request_status_json = json.dumps(request_status) + + display.vvv(u"GA send: {0}".format(request_status_json), host=self._host) + + # TODO(odyssey4me): + # Work out a better way to wait until the command has exited + result_status = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_status_json, 5, 0)) + + display.vvv(u"GA return: {0}".format(result_status), host=self._host) + + while not result_status['return']['exited']: + # Wait for 5% of the time already elapsed + sleep_time = (time.clock_gettime(time.CLOCK_MONOTONIC) - command_start) * (5 / 100) + if sleep_time < 0.0002: + sleep_time = 0.0002 + elif sleep_time > 1: + sleep_time = 1 + time.sleep(sleep_time) + result_status = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_status_json, 5, 0)) + + display.vvv(u"GA return: {0}".format(result_status), host=self._host) + + if result_status['return'].get('out-data'): + stdout = base64.b64decode(result_status['return']['out-data']) + else: + stdout = b'' + + if result_status['return'].get('err-data'): + stderr = base64.b64decode(result_status['return']['err-data']) + else: + stderr = b'' + + # Decode xml from windows + if getattr(self._shell, "_IS_WINDOWS", False) and stdout.startswith(b"#< CLIXML"): + stdout = _parse_clixml(stdout) + + display.vvv(u"GA stdout: {0}".format(to_text(stdout)), host=self._host) + display.vvv(u"GA stderr: {0}".format(to_text(stderr)), host=self._host) + + return result_status['return']['exitcode'], stdout, stderr + + def put_file(self, in_path, out_path): + ''' transfer a file from local to domain ''' + super(Connection, self).put_file(in_path, out_path) + display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._host) + + if not exists(to_bytes(in_path, errors='surrogate_or_strict')): + raise AnsibleFileNotFound( + "file or module does not exist: %s" % in_path) + + request_handle = { + 'execute': 'guest-file-open', + 'arguments': { + 'path': out_path, + 'mode': 'wb+' + } + } + request_handle_json = json.dumps(request_handle) + + display.vvv(u"GA send: {0}".format(request_handle_json), host=self._host) + + result_handle = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_handle_json, 5, 0)) + + display.vvv(u"GA return: {0}".format(result_handle), host=self._host) + + # TODO(odyssey4me): + # Handle exception for file/path IOError + with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file: + for chunk in iter(partial(in_file.read, BUFSIZE), b''): + try: + request_write = { + 'execute': 'guest-file-write', + 'arguments': { + 'handle': result_handle['return'], + 'buf-b64': base64.b64encode(chunk).decode() + } + } + request_write_json = json.dumps(request_write) + + display.vvvvv(u"GA send: {0}".format(request_write_json), host=self._host) + + result_write = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_write_json, 5, 0)) + + display.vvvvv(u"GA return: {0}".format(result_write), host=self._host) + + except Exception: + traceback.print_exc() + raise AnsibleError("failed to transfer file %s to %s" % (in_path, out_path)) + + request_close = { + 'execute': 'guest-file-close', + 'arguments': { + 'handle': result_handle['return'] + } + } + request_close_json = json.dumps(request_close) + + display.vvv(u"GA send: {0}".format(request_close_json), host=self._host) + + result_close = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_close_json, 5, 0)) + + display.vvv(u"GA return: {0}".format(result_close), host=self._host) + + def fetch_file(self, in_path, out_path): + ''' fetch a file from domain to local ''' + super(Connection, self).fetch_file(in_path, out_path) + display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._host) + + request_handle = { + 'execute': 'guest-file-open', + 'arguments': { + 'path': in_path, + 'mode': 'r' + } + } + request_handle_json = json.dumps(request_handle) + + display.vvv(u"GA send: {0}".format(request_handle_json), host=self._host) + + result_handle = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_handle_json, 5, 0)) + + display.vvv(u"GA return: {0}".format(result_handle), host=self._host) + + request_read = { + 'execute': 'guest-file-read', + 'arguments': { + 'handle': result_handle['return'], + 'count': BUFSIZE + } + } + request_read_json = json.dumps(request_read) + + display.vvv(u"GA send: {0}".format(request_read_json), host=self._host) + + with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb+') as out_file: + try: + result_read = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_read_json, 5, 0)) + display.vvvvv(u"GA return: {0}".format(result_read), host=self._host) + out_file.write(base64.b64decode(result_read['return']['buf-b64'])) + while not result_read['return']['eof']: + result_read = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_read_json, 5, 0)) + display.vvvvv(u"GA return: {0}".format(result_read), host=self._host) + out_file.write(base64.b64decode(result_read['return']['buf-b64'])) + + except Exception: + traceback.print_exc() + raise AnsibleError("failed to transfer file %s to %s" % (in_path, out_path)) + + request_close = { + 'execute': 'guest-file-close', + 'arguments': { + 'handle': result_handle['return'] + } + } + request_close_json = json.dumps(request_close) + + display.vvv(u"GA send: {0}".format(request_close_json), host=self._host) + + result_close = json.loads(libvirt_qemu.qemuAgentCommand(self.domain, request_close_json, 5, 0)) + + display.vvv(u"GA return: {0}".format(result_close), host=self._host) + + def close(self): + ''' terminate the connection; nothing to do here ''' + super(Connection, self).close() + self._connected = False diff --git a/ansible_collections/community/libvirt/plugins/doc_fragments/__init__.py b/ansible_collections/community/libvirt/plugins/doc_fragments/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/doc_fragments/__init__.py diff --git a/ansible_collections/community/libvirt/plugins/doc_fragments/requirements.py b/ansible_collections/community/libvirt/plugins/doc_fragments/requirements.py new file mode 100644 index 000000000..a181ed68b --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/doc_fragments/requirements.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +class ModuleDocFragment(object): + DOCUMENTATION = r""" +options: {} +requirements: + - python >= 2.6 + - libvirt python bindings + """ diff --git a/ansible_collections/community/libvirt/plugins/doc_fragments/virt.py b/ansible_collections/community/libvirt/plugins/doc_fragments/virt.py new file mode 100644 index 000000000..a607299bf --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/doc_fragments/virt.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +class ModuleDocFragment(object): + OPTIONS_GUEST = r""" +options: + name: + description: + - name of the guest VM being managed. Note that VM must be previously + defined with xml. + - This option is required unless I(command) is C(list_vms) or C(info). + type: str + aliases: + - guest + """ + + OPTIONS_STATE = r""" +options: + state: + description: + - Note that there may be some lag for state requests like C(shutdown) + since these refer only to VM states. After starting a guest, it may not + be immediately accessible. + state and command are mutually exclusive except when command=list_vms. In + this case all VMs in specified state will be listed. + choices: [ destroyed, paused, running, shutdown ] + type: str + """ + + OPTIONS_COMMAND = r""" +options: + command: + description: + - In addition to state management, various non-idempotent commands are available. + choices: [ create, define, destroy, freemem, get_xml, info, list_vms, nodeinfo, pause, shutdown, start, status, stop, undefine, unpause, virttype ] + type: str + """ + + OPTIONS_AUTOSTART = r""" +options: + autostart: + description: + - Start VM at host startup. + type: bool + """ + + OPTIONS_URI = r""" +options: + uri: + description: + - Libvirt connection uri. + default: qemu:///system + type: str + """ + + OPTIONS_XML = r""" +options: + xml: + description: + - XML document used with the define command. + - Must be raw XML content using C(lookup). XML cannot be reference to a file. + type: str + """ diff --git a/ansible_collections/community/libvirt/plugins/inventory/__init__.py b/ansible_collections/community/libvirt/plugins/inventory/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/inventory/__init__.py diff --git a/ansible_collections/community/libvirt/plugins/inventory/libvirt.py b/ansible_collections/community/libvirt/plugins/inventory/libvirt.py new file mode 100644 index 000000000..903870cc0 --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/inventory/libvirt.py @@ -0,0 +1,212 @@ +# 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 + +DOCUMENTATION = r''' +name: libvirt +extends_documentation_fragment: + - constructed + - community.libvirt.requirements +short_description: Libvirt inventory source +description: + - Get libvirt guests in an inventory source. +author: + - Dave Olsthoorn (@daveol) <dave@bewaar.me> +version_added: "2.10.0" +options: + plugin: + description: Token that ensures this is a source file for the 'libvirt' plugin. + required: True + choices: ['libvirt', 'community.libvirt.libvirt'] + uri: + description: Libvirt Connection URI + required: True + type: str + inventory_hostname: + description: | + What to register as the inventory hostname. + If set to 'uuid' the uuid of the server will be used and a + group will be created for the server name. + If set to 'name' the name of the server will be used unless + there are more than one server with the same name in which + case the 'uuid' logic will be used. + Default is to do 'name'. + type: string + choices: + - name + - uuid + default: "name" +''' + +EXAMPLES = r''' +# Connect to lxc host +plugin: community.libvirt.libvirt +uri: 'lxc:///' + +# Connect to qemu +plugin: community.libvirt.libvirt +uri: 'qemu:///system' +''' + +from ansible.plugins.inventory import BaseInventoryPlugin, Constructable +from ansible.errors import AnsibleError +from ansible.module_utils.six import raise_from + +try: + import libvirt +except ImportError as imp_exc: + LIBVIRT_IMPORT_ERROR = imp_exc +else: + LIBVIRT_IMPORT_ERROR = None + +VIRDOMAINSTATE = ["nostate", "running", "blocked", "paused", "shutdown", "shutoff", "crashed", "pmsuspended", "last"] + + +class InventoryModule(BaseInventoryPlugin, Constructable): + NAME = 'community.libvirt.libvirt' + + def parse(self, inventory, loader, path, cache=True): + if LIBVIRT_IMPORT_ERROR: + raise_from( + AnsibleError('libvirt python bindings must be installed to use this plugin'), + LIBVIRT_IMPORT_ERROR) + + super(InventoryModule, self).parse( + inventory, + loader, + path, + cache=cache + ) + + config_data = self._read_config_data(path) + + # set _options from config data + self._consume_options(config_data) + + uri = self.get_option('uri') + if not uri: + raise AnsibleError("hypervisor uri not given") + + connection = libvirt.open(uri) + if not connection: + raise AnsibleError("hypervisor connection failure") + + # TODO(daveol) + # make using connection plugins optional + connection_plugin = dict({ + 'LXC': 'community.libvirt.libvirt_lxc', + 'QEMU': 'community.libvirt.libvirt_qemu' + }).get(connection.getType()) + + for server in connection.listAllDomains(): + inventory_hostname = dict({ + 'uuid': server.UUIDString(), + 'name': server.name() + }).get( + self.get_option('inventory_hostname') + ) + + inventory_hostname_alias = dict({ + 'name': server.UUIDString(), + 'uuid': server.name() + }).get( + self.get_option('inventory_hostname') + ) + + # TODO(daveol): Fix "Invalid characters were found in group names" + # This warning is generated because of uuid's + self.inventory.add_host(inventory_hostname) + self.inventory.add_group(inventory_hostname_alias) + self.inventory.add_child(inventory_hostname_alias, inventory_hostname) + + if connection_plugin is not None: + self.inventory.set_variable( + inventory_hostname, + 'ansible_libvirt_uri', + uri + ) + self.inventory.set_variable( + inventory_hostname, + 'ansible_connection', + connection_plugin + ) + + try: + domain = connection.lookupByUUIDString(server.UUIDString()) + except libvirt.libvirtError as e: + self.inventory.set_variable( + inventory_hostname, + 'ERROR', + str(e) + ) + else: + _domain_state, _domain_maxmem, _domain_mem, _domain_cpus, _domain_cput = domain.info() + domain_info = {"state_number": _domain_state, + "state": VIRDOMAINSTATE[_domain_state], + "maxMem_kb": _domain_maxmem, + "memory_kb": _domain_mem, + "nrVirtCpu": _domain_cpus, + "cpuTime_ns": _domain_cput} + self.inventory.set_variable( + inventory_hostname, + 'info', + domain_info + ) + + self.inventory.set_variable( + inventory_hostname, + 'xml_desc', + domain.XMLDesc() + ) + + # This needs the guest powered on, 'qemu-guest-agent' installed and the org.qemu.guest_agent.0 channel configured. + try: + # type==0 returns all types (users, os, timezone, hostname, filesystem, disks, interfaces) + domain_guestInfo = domain.guestInfo(types=0) + except libvirt.libvirtError as e: + domain_guestInfo = {"error": str(e)} + finally: + self.inventory.set_variable( + inventory_hostname, + 'guest_info', + domain_guestInfo + ) + + # This needs the guest powered on, 'qemu-guest-agent' installed and the org.qemu.guest_agent.0 channel configured. + try: + domain_interfaceAddresses = domain.interfaceAddresses(source=libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT) + except libvirt.libvirtError as e: + domain_interfaceAddresses = {"error": str(e)} + finally: + self.inventory.set_variable( + inventory_hostname, + 'interface_addresses', + domain_interfaceAddresses + ) + + # Get variables for compose + variables = self.inventory.hosts[inventory_hostname].get_vars() + + # Set composed variables + self._set_composite_vars( + self.get_option('compose'), + variables, + inventory_hostname, + self.get_option('strict'), + ) + + # Add host to composed groups + self._add_host_to_composed_groups( + self.get_option('groups'), + variables, + inventory_hostname, + self.get_option('strict'), + ) + + # Add host to keyed groups + self._add_host_to_keyed_groups( + self.get_option('keyed_groups'), + variables, + inventory_hostname, + self.get_option('strict'), + ) diff --git a/ansible_collections/community/libvirt/plugins/modules/virt.py b/ansible_collections/community/libvirt/plugins/modules/virt.py new file mode 100644 index 000000000..bbc2add06 --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/modules/virt.py @@ -0,0 +1,563 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2007, 2012 Red Hat, Inc +# Michael DeHaan <michael.dehaan@gmail.com> +# Seth Vidal <skvidal@fedoraproject.org> +# 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 + + +DOCUMENTATION = ''' +--- +module: virt +short_description: Manages virtual machines supported by libvirt +description: + - Manages virtual machines supported by I(libvirt). +extends_documentation_fragment: + - community.libvirt.virt.options_uri + - community.libvirt.virt.options_xml + - community.libvirt.virt.options_guest + - community.libvirt.virt.options_autostart + - community.libvirt.virt.options_state + - community.libvirt.virt.options_command + - community.libvirt.requirements +author: + - Ansible Core Team + - Michael DeHaan + - Seth Vidal (@skvidal) +''' + +EXAMPLES = ''' +# a playbook task line: +- name: Start a VM + community.libvirt.virt: + name: alpha + state: running + +# /usr/bin/ansible invocations +# ansible host -m virt -a "name=alpha command=status" +# ansible host -m virt -a "name=alpha command=get_xml" +# ansible host -m virt -a "name=alpha command=create uri=lxc:///" + +# defining and launching an LXC guest +- name: Define a VM + community.libvirt.virt: + command: define + xml: "{{ lookup('template', 'container-template.xml.j2') }}" + uri: 'lxc:///' +- name: start vm + community.libvirt.virt: + name: foo + state: running + uri: 'lxc:///' + +# setting autostart on a qemu VM (default uri) +- name: Set autostart for a VM + community.libvirt.virt: + name: foo + autostart: yes + +# Defining a VM and making is autostart with host. VM will be off after this task +- name: Define vm from xml and set autostart + community.libvirt.virt: + command: define + xml: "{{ lookup('template', 'vm_template.xml.j2') }}" + autostart: yes + +# Listing VMs +- name: List all VMs + community.libvirt.virt: + command: list_vms + register: all_vms + +- name: List only running VMs + community.libvirt.virt: + command: list_vms + state: running + register: running_vms +''' + +RETURN = ''' +# for list_vms command +list_vms: + description: The list of vms defined on the remote system. + type: list + returned: success + sample: [ + "build.example.org", + "dev.example.org" + ] +# for status command +status: + description: The status of the VM, among running, crashed, paused and shutdown. + type: str + sample: "success" + returned: success +''' + +import traceback + +try: + import libvirt + from libvirt import libvirtError +except ImportError: + HAS_VIRT = False +else: + HAS_VIRT = True + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + + +VIRT_FAILED = 1 +VIRT_SUCCESS = 0 +VIRT_UNAVAILABLE = 2 + +ALL_COMMANDS = [] +VM_COMMANDS = ['create', 'define', 'destroy', 'get_xml', 'pause', 'shutdown', 'status', 'start', 'stop', 'undefine', 'unpause'] +HOST_COMMANDS = ['freemem', 'info', 'list_vms', 'nodeinfo', 'virttype'] +ALL_COMMANDS.extend(VM_COMMANDS) +ALL_COMMANDS.extend(HOST_COMMANDS) + +VIRT_STATE_NAME_MAP = { + 0: 'running', + 1: 'running', + 2: 'running', + 3: 'paused', + 4: 'shutdown', + 5: 'shutdown', + 6: 'crashed', +} + + +class VMNotFound(Exception): + pass + + +class LibvirtConnection(object): + + def __init__(self, uri, module): + + self.module = module + + cmd = "uname -r" + rc, stdout, stderr = self.module.run_command(cmd) + + if "xen" in stdout: + conn = libvirt.open(None) + elif "esx" in uri: + auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], [], None] + conn = libvirt.openAuth(uri, auth) + else: + conn = libvirt.open(uri) + + if not conn: + raise Exception("hypervisor connection failure") + + self.conn = conn + + def find_vm(self, vmid): + """ + Extra bonus feature: vmid = -1 returns a list of everything + """ + + vms = self.conn.listAllDomains() + + if vmid == -1: + return vms + + for vm in vms: + if vm.name() == vmid: + return vm + + raise VMNotFound("virtual machine %s not found" % vmid) + + def shutdown(self, vmid): + return self.find_vm(vmid).shutdown() + + def pause(self, vmid): + return self.suspend(vmid) + + def unpause(self, vmid): + return self.resume(vmid) + + def suspend(self, vmid): + return self.find_vm(vmid).suspend() + + def resume(self, vmid): + return self.find_vm(vmid).resume() + + def create(self, vmid): + return self.find_vm(vmid).create() + + def destroy(self, vmid): + return self.find_vm(vmid).destroy() + + def undefine(self, vmid): + return self.find_vm(vmid).undefine() + + def get_status2(self, vm): + state = vm.info()[0] + return VIRT_STATE_NAME_MAP.get(state, "unknown") + + def get_status(self, vmid): + state = self.find_vm(vmid).info()[0] + return VIRT_STATE_NAME_MAP.get(state, "unknown") + + def nodeinfo(self): + return self.conn.getInfo() + + def get_type(self): + return self.conn.getType() + + def get_xml(self, vmid): + vm = self.conn.lookupByName(vmid) + return vm.XMLDesc(0) + + def get_maxVcpus(self, vmid): + vm = self.conn.lookupByName(vmid) + return vm.maxVcpus() + + def get_maxMemory(self, vmid): + vm = self.conn.lookupByName(vmid) + return vm.maxMemory() + + def getFreeMemory(self): + return self.conn.getFreeMemory() + + def get_autostart(self, vmid): + vm = self.conn.lookupByName(vmid) + return vm.autostart() + + def set_autostart(self, vmid, val): + vm = self.conn.lookupByName(vmid) + return vm.setAutostart(val) + + def define_from_xml(self, xml): + return self.conn.defineXML(xml) + + +class Virt(object): + + def __init__(self, uri, module): + self.module = module + self.uri = uri + + def __get_conn(self): + self.conn = LibvirtConnection(self.uri, self.module) + return self.conn + + def get_vm(self, vmid): + self.__get_conn() + return self.conn.find_vm(vmid) + + def state(self): + vms = self.list_vms() + state = [] + for vm in vms: + state_blurb = self.conn.get_status(vm) + state.append("%s %s" % (vm, state_blurb)) + return state + + def info(self): + vms = self.list_vms() + info = dict() + for vm in vms: + data = self.conn.find_vm(vm).info() + # libvirt returns maxMem, memory, and cpuTime as long()'s, which + # xmlrpclib tries to convert to regular int's during serialization. + # This throws exceptions, so convert them to strings here and + # assume the other end of the xmlrpc connection can figure things + # out or doesn't care. + info[vm] = dict( + state=VIRT_STATE_NAME_MAP.get(data[0], "unknown"), + maxMem=str(data[1]), + memory=str(data[2]), + nrVirtCpu=data[3], + cpuTime=str(data[4]), + autostart=self.conn.get_autostart(vm), + ) + + return info + + def nodeinfo(self): + self.__get_conn() + data = self.conn.nodeinfo() + info = dict( + cpumodel=str(data[0]), + phymemory=str(data[1]), + cpus=str(data[2]), + cpumhz=str(data[3]), + numanodes=str(data[4]), + sockets=str(data[5]), + cpucores=str(data[6]), + cputhreads=str(data[7]) + ) + return info + + def list_vms(self, state=None): + self.conn = self.__get_conn() + vms = self.conn.find_vm(-1) + results = [] + for x in vms: + try: + if state: + vmstate = self.conn.get_status2(x) + if vmstate == state: + results.append(x.name()) + else: + results.append(x.name()) + except Exception: + pass + return results + + def virttype(self): + return self.__get_conn().get_type() + + def autostart(self, vmid, as_flag): + self.conn = self.__get_conn() + # Change autostart flag only if needed + if self.conn.get_autostart(vmid) != as_flag: + self.conn.set_autostart(vmid, as_flag) + return True + + return False + + def freemem(self): + self.conn = self.__get_conn() + return self.conn.getFreeMemory() + + def shutdown(self, vmid): + """ Make the machine with the given vmid stop running. Whatever that takes. """ + self.__get_conn() + self.conn.shutdown(vmid) + return 0 + + def pause(self, vmid): + """ Pause the machine with the given vmid. """ + + self.__get_conn() + return self.conn.suspend(vmid) + + def unpause(self, vmid): + """ Unpause the machine with the given vmid. """ + + self.__get_conn() + return self.conn.resume(vmid) + + def create(self, vmid): + """ Start the machine via the given vmid """ + + self.__get_conn() + return self.conn.create(vmid) + + def start(self, vmid): + """ Start the machine via the given id/name """ + + self.__get_conn() + return self.conn.create(vmid) + + def destroy(self, vmid): + """ Pull the virtual power from the virtual domain, giving it virtually no time to virtually shut down. """ + self.__get_conn() + return self.conn.destroy(vmid) + + def undefine(self, vmid): + """ Stop a domain, and then wipe it from the face of the earth. (delete disk/config file) """ + + self.__get_conn() + return self.conn.undefine(vmid) + + def status(self, vmid): + """ + Return a state suitable for server consumption. Aka, codes.py values, not XM output. + """ + self.__get_conn() + return self.conn.get_status(vmid) + + def get_xml(self, vmid): + """ + Receive a Vm id as input + Return an xml describing vm config returned by a libvirt call + """ + + self.__get_conn() + return self.conn.get_xml(vmid) + + def get_maxVcpus(self, vmid): + """ + Gets the max number of VCPUs on a guest + """ + + self.__get_conn() + return self.conn.get_maxVcpus(vmid) + + def get_max_memory(self, vmid): + """ + Gets the max memory on a guest + """ + + self.__get_conn() + return self.conn.get_MaxMemory(vmid) + + def define(self, xml): + """ + Define a guest with the given xml + """ + self.__get_conn() + return self.conn.define_from_xml(xml) + + +def core(module): + + state = module.params.get('state', None) + autostart = module.params.get('autostart', None) + guest = module.params.get('name', None) + command = module.params.get('command', None) + uri = module.params.get('uri', None) + xml = module.params.get('xml', None) + + v = Virt(uri, module) + res = dict() + + if state and command == 'list_vms': + res = v.list_vms(state=state) + if not isinstance(res, dict): + res = {command: res} + return VIRT_SUCCESS, res + + if autostart is not None and command != 'define': + if not guest: + module.fail_json(msg="autostart requires 1 argument: name") + try: + v.get_vm(guest) + except VMNotFound: + module.fail_json(msg="domain %s not found" % guest) + res['changed'] = v.autostart(guest, autostart) + if not command and not state: + return VIRT_SUCCESS, res + + if state: + if not guest: + module.fail_json(msg="state change requires a guest specified") + + if state == 'running': + if v.status(guest) == 'paused': + res['changed'] = True + res['msg'] = v.unpause(guest) + elif v.status(guest) != 'running': + res['changed'] = True + res['msg'] = v.start(guest) + elif state == 'shutdown': + if v.status(guest) != 'shutdown': + res['changed'] = True + res['msg'] = v.shutdown(guest) + elif state == 'destroyed': + if v.status(guest) != 'shutdown': + res['changed'] = True + res['msg'] = v.destroy(guest) + elif state == 'paused': + if v.status(guest) == 'running': + res['changed'] = True + res['msg'] = v.pause(guest) + else: + module.fail_json(msg="unexpected state") + + return VIRT_SUCCESS, res + + if command: + if command in VM_COMMANDS: + if command == 'define': + if not xml: + module.fail_json(msg="define requires xml argument") + if guest: + # there might be a mismatch between quest 'name' in the module and in the xml + module.warn("'xml' is given - ignoring 'name'") + try: + domain_name = re.search('<name>(.*)</name>', xml).groups()[0] + except AttributeError: + module.fail_json(msg="Could not find domain 'name' in xml") + + # From libvirt docs (https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainDefineXML): + # -- A previous definition for this domain would be overridden if it already exists. + # + # In real world testing with libvirt versions 1.2.17-13, 2.0.0-10 and 3.9.0-14 + # on qemu and lxc domains results in: + # operation failed: domain '<name>' already exists with <uuid> + # + # In case a domain would be indeed overwritten, we should protect idempotency: + try: + existing_domain_xml = v.get_vm(domain_name).XMLDesc( + libvirt.VIR_DOMAIN_XML_INACTIVE + ) + except VMNotFound: + existing_domain_xml = None + try: + domain = v.define(xml) + if existing_domain_xml: + # if we are here, then libvirt redefined existing domain as the doc promised + if existing_domain_xml != domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE): + res = {'changed': True, 'change_reason': 'config changed'} + else: + res = {'changed': True, 'created': domain.name()} + except libvirtError as e: + if e.get_error_code() != 9: # 9 means 'domain already exists' error + module.fail_json(msg='libvirtError: %s' % e.get_error_message()) + if autostart is not None and v.autostart(domain_name, autostart): + res = {'changed': True, 'change_reason': 'autostart'} + + elif not guest: + module.fail_json(msg="%s requires 1 argument: guest" % command) + else: + res = getattr(v, command)(guest) + if not isinstance(res, dict): + res = {command: res} + + return VIRT_SUCCESS, res + + elif hasattr(v, command): + res = getattr(v, command)() + if not isinstance(res, dict): + res = {command: res} + return VIRT_SUCCESS, res + + else: + module.fail_json(msg="Command %s not recognized" % command) + + module.fail_json(msg="expected state or command parameter to be specified") + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type='str', aliases=['guest']), + state=dict(type='str', choices=['destroyed', 'paused', 'running', 'shutdown']), + autostart=dict(type='bool'), + command=dict(type='str', choices=ALL_COMMANDS), + uri=dict(type='str', default='qemu:///system'), + xml=dict(type='str'), + ), + ) + + if not HAS_VIRT: + module.fail_json(msg='The `libvirt` module is not importable. Check the requirements.') + + rc = VIRT_SUCCESS + try: + rc, result = core(module) + except Exception as e: + module.fail_json(msg=to_native(e), exception=traceback.format_exc()) + + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=result) + else: + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/libvirt/plugins/modules/virt_net.py b/ansible_collections/community/libvirt/plugins/modules/virt_net.py new file mode 100644 index 000000000..7492cac79 --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/modules/virt_net.py @@ -0,0 +1,647 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Maciej Delmanowski <drybjed@gmail.com> +# 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 + + +DOCUMENTATION = ''' +--- +module: virt_net +author: "Maciej Delmanowski (@drybjed)" +short_description: Manage libvirt network configuration +description: + - Manage I(libvirt) networks. +options: + name: + aliases: ['network'] + description: + - Name of the network being managed. Note that network must be previously + defined with xml. + type: str + state: + choices: [ "active", "inactive", "present", "absent" ] + description: + - Specify which state you want a network to be in. + If 'active', network will be started. + If 'present', ensure that network is present but do not change its + state; if it is missing, you need to specify xml argument. + If 'inactive', network will be stopped. + If 'undefined' or 'absent', network will be removed from I(libvirt) configuration. + type: str + command: + choices: [ "define", "create", "start", "stop", "destroy", + "undefine", "get_xml", "list_nets", "facts", + "info", "status", "modify"] + description: + - In addition to state management, various non-idempotent commands are available. + See examples. + Modify was added in Ansible version 2.1. + type: str + autostart: + type: bool + description: + - Specify if a given network should be started automatically on system boot. +extends_documentation_fragment: + - community.libvirt.virt.options_uri + - community.libvirt.virt.options_xml + - community.libvirt.requirements +requirements: + - "python-lxml" +''' + +EXAMPLES = ''' +- name: Define a new network + community.libvirt.virt_net: + command: define + name: br_nat + xml: '{{ lookup("template", "network/bridge.xml.j2") }}' + +- name: Start a network + community.libvirt.virt_net: + command: create + name: br_nat + +- name: List available networks + community.libvirt.virt_net: + command: list_nets + +- name: Get XML data of a specified network + community.libvirt.virt_net: + command: get_xml + name: br_nat + +- name: Stop a network + community.libvirt.virt_net: + command: destroy + name: br_nat + +- name: Undefine a network + community.libvirt.virt_net: + command: undefine + name: br_nat + +# Gather facts about networks +# Facts will be available as 'ansible_libvirt_networks' +- name: Gather facts about networks + community.libvirt.virt_net: + command: facts + +- name: Gather information about network managed by 'libvirt' remotely using uri + community.libvirt.virt_net: + command: info + uri: '{{ item }}' + with_items: '{{ libvirt_uris }}' + register: networks + +- name: Ensure that a network is active (needs to be defined and built first) + community.libvirt.virt_net: + state: active + name: br_nat + +- name: Ensure that a network is inactive + community.libvirt.virt_net: + state: inactive + name: br_nat + +- name: Ensure that a given network will be started at boot + community.libvirt.virt_net: + autostart: yes + name: br_nat + +- name: Disable autostart for a given network + community.libvirt.virt_net: + autostart: no + name: br_nat + +- name: Add a new host in the dhcp pool + community.libvirt.virt_net: + name: br_nat + command: modify + xml: "<host mac='FC:C2:33:00:6c:3c' name='my_vm' ip='192.168.122.30'/>" +''' + +try: + import libvirt +except ImportError: + HAS_VIRT = False +else: + HAS_VIRT = True + +try: + from lxml import etree +except ImportError: + HAS_XML = False +else: + HAS_XML = True + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + + +VIRT_FAILED = 1 +VIRT_SUCCESS = 0 +VIRT_UNAVAILABLE = 2 + +ALL_COMMANDS = [] +ENTRY_COMMANDS = ['create', 'status', 'start', 'stop', + 'undefine', 'destroy', 'get_xml', 'define', + 'modify'] +HOST_COMMANDS = ['list_nets', 'facts', 'info'] +ALL_COMMANDS.extend(ENTRY_COMMANDS) +ALL_COMMANDS.extend(HOST_COMMANDS) + +ENTRY_STATE_ACTIVE_MAP = { + 0: "inactive", + 1: "active" +} + +ENTRY_STATE_AUTOSTART_MAP = { + 0: "no", + 1: "yes" +} + +ENTRY_STATE_PERSISTENT_MAP = { + 0: "no", + 1: "yes" +} + + +class EntryNotFound(Exception): + pass + + +class LibvirtConnection(object): + + def __init__(self, uri, module): + + self.module = module + + conn = libvirt.open(uri) + + if not conn: + raise Exception("hypervisor connection failure") + + self.conn = conn + + def find_entry(self, entryid): + if entryid == -1: # Get active entries + names = self.conn.listNetworks() + self.conn.listDefinedNetworks() + return [self.conn.networkLookupByName(n) for n in names] + + try: + return self.conn.networkLookupByName(entryid) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_NETWORK: + raise EntryNotFound("network %s not found" % entryid) + raise + + def create(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).create() + else: + try: + state = self.find_entry(entryid).isActive() + except Exception: + return self.module.exit_json(changed=True) + if not state: + return self.module.exit_json(changed=True) + + def modify(self, entryid, xml): + network = self.find_entry(entryid) + # identify what type of entry is given in the xml + new_data = etree.fromstring(xml) + old_data = etree.fromstring(network.XMLDesc(0)) + if new_data.tag == 'host': + mac_addr = new_data.get('mac') + hosts = old_data.xpath('/network/ip/dhcp/host') + # find the one mac we're looking for + host = None + for h in hosts: + if h.get('mac') == mac_addr: + host = h + break + if host is None: + # add the host + if not self.module.check_mode: + if network.isActive(): + res = network.update(libvirt.VIR_NETWORK_UPDATE_COMMAND_ADD_LAST, + libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST, + -1, xml, libvirt.VIR_NETWORK_UPDATE_AFFECT_LIVE | libvirt.VIR_NETWORK_UPDATE_AFFECT_CONFIG) + else: + res = network.update(libvirt.VIR_NETWORK_UPDATE_COMMAND_ADD_LAST, + libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST, + -1, xml, libvirt.VIR_NETWORK_UPDATE_AFFECT_CONFIG) + else: + # pretend there was a change + res = 0 + if res == 0: + return True + else: + # change the host + if host.get('name') == new_data.get('name') and host.get('ip') == new_data.get('ip'): + return False + else: + if not self.module.check_mode: + if network.isActive(): + res = network.update(libvirt.VIR_NETWORK_UPDATE_COMMAND_MODIFY, + libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST, + -1, xml, libvirt.VIR_NETWORK_UPDATE_AFFECT_LIVE | libvirt.VIR_NETWORK_UPDATE_AFFECT_CONFIG) + else: + res = network.update(libvirt.VIR_NETWORK_UPDATE_COMMAND_MODIFY, + libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST, + -1, xml, libvirt.VIR_NETWORK_UPDATE_AFFECT_CONFIG) + else: + # pretend there was a change + res = 0 + if res == 0: + return True + # command, section, parentIndex, xml, flags=0 + self.module.fail_json(msg='updating this is not supported yet %s' % to_native(xml)) + + def destroy(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).destroy() + else: + if self.find_entry(entryid).isActive(): + return self.module.exit_json(changed=True) + + def undefine(self, entryid): + entry = None + try: + entry = self.find_entry(entryid) + found = True + except EntryNotFound: + found = False + + if found: + return self.find_entry(entryid).undefine() + + if self.module.check_mode: + return self.module.exit_json(changed=found) + + def get_status2(self, entry): + state = entry.isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state, "unknown") + + def get_status(self, entryid): + if not self.module.check_mode: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state, "unknown") + else: + try: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state, "unknown") + except Exception: + return ENTRY_STATE_ACTIVE_MAP.get("inactive", "unknown") + + def get_uuid(self, entryid): + return self.find_entry(entryid).UUIDString() + + def get_xml(self, entryid): + return self.find_entry(entryid).XMLDesc(0) + + def get_forward(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/network/forward')[0].get('mode') + except Exception: + raise ValueError('Forward mode not specified') + return result + + def get_domain(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/network/domain')[0].get('name') + except Exception: + raise ValueError('Domain not specified') + return result + + def get_macaddress(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/network/mac')[0].get('address') + except Exception: + raise ValueError('MAC address not specified') + return result + + def get_autostart(self, entryid): + state = self.find_entry(entryid).autostart() + return ENTRY_STATE_AUTOSTART_MAP.get(state, "unknown") + + def get_autostart2(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).autostart() + else: + try: + return self.find_entry(entryid).autostart() + except Exception: + return self.module.exit_json(changed=True) + + def set_autostart(self, entryid, val): + if not self.module.check_mode: + return self.find_entry(entryid).setAutostart(val) + else: + try: + state = self.find_entry(entryid).autostart() + except Exception: + return self.module.exit_json(changed=True) + if bool(state) != val: + return self.module.exit_json(changed=True) + + def get_bridge(self, entryid): + return self.find_entry(entryid).bridgeName() + + def get_persistent(self, entryid): + state = self.find_entry(entryid).isPersistent() + return ENTRY_STATE_PERSISTENT_MAP.get(state, "unknown") + + def get_dhcp_leases(self, entryid): + network = self.find_entry(entryid) + return network.DHCPLeases() + + def define_from_xml(self, entryid, xml): + if not self.module.check_mode: + return self.conn.networkDefineXML(xml) + else: + try: + self.find_entry(entryid) + except Exception: + return self.module.exit_json(changed=True) + + +class VirtNetwork(object): + + def __init__(self, uri, module): + self.module = module + self.uri = uri + self.conn = LibvirtConnection(self.uri, self.module) + + def get_net(self, entryid): + return self.conn.find_entry(entryid) + + def list_nets(self, state=None): + results = [] + for entry in self.conn.find_entry(-1): + if state: + if state == self.conn.get_status2(entry): + results.append(entry.name()) + else: + results.append(entry.name()) + return results + + def state(self): + results = [] + for entry in self.list_nets(): + state_blurb = self.conn.get_status(entry) + results.append("%s %s" % (entry, state_blurb)) + return results + + def autostart(self, entryid): + return self.conn.set_autostart(entryid, True) + + def get_autostart(self, entryid): + return self.conn.get_autostart2(entryid) + + def set_autostart(self, entryid, state): + return self.conn.set_autostart(entryid, state) + + def create(self, entryid): + if self.conn.get_status(entryid) == "active": + return + try: + return self.conn.create(entryid) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NETWORK_EXIST: + return None + raise + + def modify(self, entryid, xml): + return self.conn.modify(entryid, xml) + + def start(self, entryid): + return self.create(entryid) + + def stop(self, entryid): + if self.conn.get_status(entryid) == "active": + return self.conn.destroy(entryid) + + def destroy(self, entryid): + return self.stop(entryid) + + def undefine(self, entryid): + return self.conn.undefine(entryid) + + def status(self, entryid): + return self.conn.get_status(entryid) + + def get_xml(self, entryid): + return self.conn.get_xml(entryid) + + def define(self, entryid, xml): + return self.conn.define_from_xml(entryid, xml) + + def info(self): + return self.facts(facts_mode='info') + + def facts(self, name=None, facts_mode='facts'): + results = dict() + if name: + entries = [name] + else: + entries = self.list_nets() + for entry in entries: + results[entry] = dict() + results[entry]["autostart"] = self.conn.get_autostart(entry) + results[entry]["persistent"] = self.conn.get_persistent(entry) + results[entry]["state"] = self.conn.get_status(entry) + results[entry]["bridge"] = self.conn.get_bridge(entry) + results[entry]["uuid"] = self.conn.get_uuid(entry) + try: + results[entry]["dhcp_leases"] = self.conn.get_dhcp_leases(entry) + # not supported on RHEL 6 + except AttributeError: + pass + + try: + results[entry]["forward_mode"] = self.conn.get_forward(entry) + except ValueError: + pass + + try: + results[entry]["domain"] = self.conn.get_domain(entry) + except ValueError: + pass + + try: + results[entry]["macaddress"] = self.conn.get_macaddress(entry) + except ValueError: + pass + + facts = dict() + if facts_mode == 'facts': + facts["ansible_facts"] = dict() + facts["ansible_facts"]["ansible_libvirt_networks"] = results + elif facts_mode == 'info': + facts['networks'] = results + return facts + + +def core(module): + + state = module.params.get('state', None) + name = module.params.get('name', None) + command = module.params.get('command', None) + uri = module.params.get('uri', None) + xml = module.params.get('xml', None) + autostart = module.params.get('autostart', None) + + v = VirtNetwork(uri, module) + res = {} + + if state and command == 'list_nets': + res = v.list_nets(state=state) + if not isinstance(res, dict): + res = {command: res} + return VIRT_SUCCESS, res + + if state: + if not name: + module.fail_json(msg="state change requires a specified name") + + res['changed'] = False + if state in ['active']: + if v.status(name) != 'active': + res['changed'] = True + res['msg'] = v.start(name) + elif state in ['present']: + try: + v.get_net(name) + except EntryNotFound: + if not xml: + module.fail_json(msg="network '" + name + "' not present, but xml not specified") + v.define(name, xml) + res = {'changed': True, 'created': name} + elif state in ['inactive']: + entries = v.list_nets() + if name in entries: + if v.status(name) != 'inactive': + res['changed'] = True + res['msg'] = v.destroy(name) + elif state in ['undefined', 'absent']: + entries = v.list_nets() + if name in entries: + if v.status(name) != 'inactive': + v.destroy(name) + res['changed'] = True + res['msg'] = v.undefine(name) + else: + module.fail_json(msg="unexpected state") + + return VIRT_SUCCESS, res + + if command: + if command in ENTRY_COMMANDS: + if not name: + module.fail_json(msg="%s requires 1 argument: name" % command) + if command in ('define', 'modify'): + if not xml: + module.fail_json(msg=command + " requires xml argument") + try: + v.get_net(name) + except EntryNotFound: + v.define(name, xml) + res = {'changed': True, 'created': name} + else: + if command == 'modify': + mod = v.modify(name, xml) + res = {'changed': mod, 'modified': name} + return VIRT_SUCCESS, res + res = getattr(v, command)(name) + if not isinstance(res, dict): + res = {command: res} + return VIRT_SUCCESS, res + + elif hasattr(v, command): + if command == 'facts' and name: + res = v.facts(name) + else: + res = getattr(v, command)() + if not isinstance(res, dict): + res = {command: res} + return VIRT_SUCCESS, res + + else: + module.fail_json(msg="Command %s not recognized" % command) + + if autostart is not None: + if not name: + module.fail_json(msg="state change requires a specified name") + + res['changed'] = False + if autostart: + if not v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, True) + else: + if v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, False) + + return VIRT_SUCCESS, res + + module.fail_json(msg="expected state or command parameter to be specified") + + +def main(): + + module = AnsibleModule( + argument_spec=dict( + name=dict(aliases=['network']), + state=dict(choices=['active', 'inactive', 'present', 'absent']), + command=dict(choices=ALL_COMMANDS), + uri=dict(default='qemu:///system'), + xml=dict(), + autostart=dict(type='bool') + ), + supports_check_mode=True, + required_if=[ + ('command', 'create', ['name']), + ('command', 'status', ['name']), + ('command', 'start', ['name']), + ('command', 'stop', ['name']), + ('command', 'undefine', ['name']), + ('command', 'destroy', ['name']), + ('command', 'get_xml', ['name']), + ('command', 'define', ['name']), + ('command', 'modify', ['name']), + ] + ) + + if not HAS_VIRT: + module.fail_json( + msg='The `libvirt` module is not importable. Check the requirements.' + ) + + if not HAS_XML: + module.fail_json( + msg='The `lxml` module is not importable. Check the requirements.' + ) + + rc = VIRT_SUCCESS + try: + rc, result = core(module) + except Exception as e: + module.fail_json(msg=str(e)) + + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=result) + else: + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/libvirt/plugins/modules/virt_pool.py b/ansible_collections/community/libvirt/plugins/modules/virt_pool.py new file mode 100644 index 000000000..70145c617 --- /dev/null +++ b/ansible_collections/community/libvirt/plugins/modules/virt_pool.py @@ -0,0 +1,707 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Maciej Delmanowski <drybjed@gmail.com> +# 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 + + +DOCUMENTATION = ''' +--- +module: virt_pool +author: "Maciej Delmanowski (@drybjed)" +short_description: Manage libvirt storage pools +description: + - Manage I(libvirt) storage pools. +options: + name: + aliases: [ "pool" ] + description: + - Name of the storage pool being managed. Note that pool must be previously + defined with xml. + type: str + state: + choices: [ "active", "inactive", "present", "absent", "undefined", "deleted" ] + description: + - Specify which state you want a storage pool to be in. + If 'active', pool will be started. + If 'present', ensure that pool is present but do not change its + state; if it is missing, you need to specify xml argument. + If 'inactive', pool will be stopped. + If 'undefined' or 'absent', pool will be removed from I(libvirt) configuration. + If 'deleted', pool contents will be deleted and then pool undefined. + type: str + command: + choices: [ "define", "build", "create", "start", "stop", "destroy", + "delete", "undefine", "get_xml", "list_pools", "facts", + "info", "status", "refresh" ] + description: + - In addition to state management, various non-idempotent commands are available. + See examples. + type: str + autostart: + type: bool + description: + - Specify if a given storage pool should be started automatically on system boot. + mode: + choices: [ 'new', 'repair', 'resize', 'no_overwrite', 'overwrite', 'normal', 'zeroed' ] + description: + - Pass additional parameters to 'build' or 'delete' commands. + type: str +extends_documentation_fragment: + - community.libvirt.virt.options_uri + - community.libvirt.virt.options_xml + - community.libvirt.requirements +requirements: + - "python-lxml" +''' + +EXAMPLES = ''' +- name: Define a new storage pool + community.libvirt.virt_pool: + command: define + name: vms + xml: '{{ lookup("template", "pool/dir.xml.j2") }}' + +- name: Build a storage pool if it does not exist + community.libvirt.virt_pool: + command: build + name: vms + +- name: Start a storage pool + community.libvirt.virt_pool: + command: create + name: vms + +- name: List available pools + community.libvirt.virt_pool: + command: list_pools + +- name: Get XML data of a specified pool + community.libvirt.virt_pool: + command: get_xml + name: vms + +- name: Stop a storage pool + community.libvirt.virt_pool: + command: destroy + name: vms + +- name: Delete a storage pool (destroys contents) + community.libvirt.virt_pool: + command: delete + name: vms + +- name: Undefine a storage pool + community.libvirt.virt_pool: + command: undefine + name: vms + +# Gather facts about storage pools +# Facts will be available as 'ansible_libvirt_pools' +- name: Gather facts about storage pools + community.libvirt.virt_pool: + command: facts + +- name: Gather information about pools managed by 'libvirt' remotely using uri + community.libvirt.virt_pool: + command: info + uri: '{{ item }}' + with_items: '{{ libvirt_uris }}' + register: storage_pools + +- name: Ensure that a pool is active (needs to be defined and built first) + community.libvirt.virt_pool: + state: active + name: vms + +- name: Ensure that a pool is inactive + community.libvirt.virt_pool: + state: inactive + name: vms + +- name: Ensure that a given pool will be started at boot + community.libvirt.virt_pool: + autostart: yes + name: vms + +- name: Disable autostart for a given pool + community.libvirt.virt_pool: + autostart: no + name: vms +''' + +try: + import libvirt +except ImportError: + HAS_VIRT = False +else: + HAS_VIRT = True + +try: + from lxml import etree +except ImportError: + HAS_XML = False +else: + HAS_XML = True + +from ansible.module_utils.basic import AnsibleModule + + +VIRT_FAILED = 1 +VIRT_SUCCESS = 0 +VIRT_UNAVAILABLE = 2 + +ALL_COMMANDS = [] +ENTRY_COMMANDS = ['create', 'status', 'start', 'stop', 'build', 'delete', + 'undefine', 'destroy', 'get_xml', 'define', 'refresh'] +HOST_COMMANDS = ['list_pools', 'facts', 'info'] +ALL_COMMANDS.extend(ENTRY_COMMANDS) +ALL_COMMANDS.extend(HOST_COMMANDS) + +ENTRY_STATE_ACTIVE_MAP = { + 0: "inactive", + 1: "active" +} + +ENTRY_STATE_AUTOSTART_MAP = { + 0: "no", + 1: "yes" +} + +ENTRY_STATE_PERSISTENT_MAP = { + 0: "no", + 1: "yes" +} + +ENTRY_STATE_INFO_MAP = { + 0: "inactive", + 1: "building", + 2: "running", + 3: "degraded", + 4: "inaccessible" +} + +ENTRY_BUILD_FLAGS_MAP = { + "new": 0, + "repair": 1, + "resize": 2, + "no_overwrite": 4, + "overwrite": 8 +} + +ENTRY_DELETE_FLAGS_MAP = { + "normal": 0, + "zeroed": 1 +} + +ALL_MODES = [] +ALL_MODES.extend(ENTRY_BUILD_FLAGS_MAP.keys()) +ALL_MODES.extend(ENTRY_DELETE_FLAGS_MAP.keys()) + + +class EntryNotFound(Exception): + pass + + +class LibvirtConnection(object): + + def __init__(self, uri, module): + + self.module = module + + conn = libvirt.open(uri) + + if not conn: + raise Exception("hypervisor connection failure") + + self.conn = conn + + def find_entry(self, entryid): + # entryid = -1 returns a list of everything + + results = [] + + # Get active entries + for name in self.conn.listStoragePools(): + entry = self.conn.storagePoolLookupByName(name) + results.append(entry) + + # Get inactive entries + for name in self.conn.listDefinedStoragePools(): + entry = self.conn.storagePoolLookupByName(name) + results.append(entry) + + if entryid == -1: + return results + + for entry in results: + if entry.name() == entryid: + return entry + + raise EntryNotFound("storage pool %s not found" % entryid) + + def create(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).create() + else: + try: + state = self.find_entry(entryid).isActive() + except Exception: + return self.module.exit_json(changed=True) + if not state: + return self.module.exit_json(changed=True) + + def destroy(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).destroy() + else: + if self.find_entry(entryid).isActive(): + return self.module.exit_json(changed=True) + + def undefine(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).undefine() + else: + if not self.find_entry(entryid): + return self.module.exit_json(changed=True) + + def get_status2(self, entry): + state = entry.isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state, "unknown") + + def get_status(self, entryid): + if not self.module.check_mode: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state, "unknown") + else: + try: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state, "unknown") + except Exception: + return ENTRY_STATE_ACTIVE_MAP.get("inactive", "unknown") + + def get_uuid(self, entryid): + return self.find_entry(entryid).UUIDString() + + def get_xml(self, entryid): + return self.find_entry(entryid).XMLDesc(0) + + def get_info(self, entryid): + return self.find_entry(entryid).info() + + def get_volume_count(self, entryid): + return self.find_entry(entryid).numOfVolumes() + + def get_volume_names(self, entryid): + return self.find_entry(entryid).listVolumes() + + def get_devices(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + if xml.xpath('/pool/source/device'): + result = [] + for device in xml.xpath('/pool/source/device'): + result.append(device.get('path')) + try: + return result + except Exception: + raise ValueError('No devices specified') + + def get_format(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/pool/source/format')[0].get('type') + except Exception: + raise ValueError('Format not specified') + return result + + def get_host(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/pool/source/host')[0].get('name') + except Exception: + raise ValueError('Host not specified') + return result + + def get_source_path(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/pool/source/dir')[0].get('path') + except Exception: + raise ValueError('Source path not specified') + return result + + def get_path(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/pool/target/path')[0].text + except Exception: + raise ValueError('Target path not specified') + return result + + def get_type(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + return xml.get('type') + + def build(self, entryid, flags): + if not self.module.check_mode: + return self.find_entry(entryid).build(flags) + else: + try: + state = self.find_entry(entryid) + except Exception: + return self.module.exit_json(changed=True) + if not state: + return self.module.exit_json(changed=True) + + def delete(self, entryid, flags): + if not self.module.check_mode: + return self.find_entry(entryid).delete(flags) + else: + try: + state = self.find_entry(entryid) + except Exception: + return self.module.exit_json(changed=True) + if state: + return self.module.exit_json(changed=True) + + def get_autostart(self, entryid): + state = self.find_entry(entryid).autostart() + return ENTRY_STATE_AUTOSTART_MAP.get(state, "unknown") + + def get_autostart2(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).autostart() + else: + try: + return self.find_entry(entryid).autostart() + except Exception: + return self.module.exit_json(changed=True) + + def set_autostart(self, entryid, val): + if not self.module.check_mode: + return self.find_entry(entryid).setAutostart(val) + else: + try: + state = self.find_entry(entryid).autostart() + except Exception: + return self.module.exit_json(changed=True) + if bool(state) != val: + return self.module.exit_json(changed=True) + + def refresh(self, entryid): + return self.find_entry(entryid).refresh() + + def get_persistent(self, entryid): + state = self.find_entry(entryid).isPersistent() + return ENTRY_STATE_PERSISTENT_MAP.get(state, "unknown") + + def define_from_xml(self, entryid, xml): + if not self.module.check_mode: + return self.conn.storagePoolDefineXML(xml) + else: + try: + self.find_entry(entryid) + except Exception: + return self.module.exit_json(changed=True) + + +class VirtStoragePool(object): + + def __init__(self, uri, module): + self.module = module + self.uri = uri + self.conn = LibvirtConnection(self.uri, self.module) + + def get_pool(self, entryid): + return self.conn.find_entry(entryid) + + def list_pools(self, state=None): + results = [] + for entry in self.conn.find_entry(-1): + if state: + if state == self.conn.get_status2(entry): + results.append(entry.name()) + else: + results.append(entry.name()) + return results + + def state(self): + results = [] + for entry in self.list_pools(): + state_blurb = self.conn.get_status(entry) + results.append("%s %s" % (entry, state_blurb)) + return results + + def autostart(self, entryid): + return self.conn.set_autostart(entryid, True) + + def get_autostart(self, entryid): + return self.conn.get_autostart2(entryid) + + def set_autostart(self, entryid, state): + return self.conn.set_autostart(entryid, state) + + def create(self, entryid): + return self.conn.create(entryid) + + def start(self, entryid): + return self.conn.create(entryid) + + def stop(self, entryid): + return self.conn.destroy(entryid) + + def destroy(self, entryid): + return self.conn.destroy(entryid) + + def undefine(self, entryid): + return self.conn.undefine(entryid) + + def status(self, entryid): + return self.conn.get_status(entryid) + + def get_xml(self, entryid): + return self.conn.get_xml(entryid) + + def define(self, entryid, xml): + return self.conn.define_from_xml(entryid, xml) + + def build(self, entryid, flags): + return self.conn.build(entryid, ENTRY_BUILD_FLAGS_MAP.get(flags, 0)) + + def delete(self, entryid, flags): + return self.conn.delete(entryid, ENTRY_DELETE_FLAGS_MAP.get(flags, 0)) + + def refresh(self, entryid): + return self.conn.refresh(entryid) + + def info(self): + return self.facts(facts_mode='info') + + def facts(self, facts_mode='facts'): + results = dict() + for entry in self.list_pools(): + results[entry] = dict() + if self.conn.find_entry(entry): + data = self.conn.get_info(entry) + # libvirt returns maxMem, memory, and cpuTime as long()'s, which + # xmlrpclib tries to convert to regular int's during serialization. + # This throws exceptions, so convert them to strings here and + # assume the other end of the xmlrpc connection can figure things + # out or doesn't care. + results[entry] = { + "status": ENTRY_STATE_INFO_MAP.get(data[0], "unknown"), + "size_total": str(data[1]), + "size_used": str(data[2]), + "size_available": str(data[3]), + } + results[entry]["autostart"] = self.conn.get_autostart(entry) + results[entry]["persistent"] = self.conn.get_persistent(entry) + results[entry]["state"] = self.conn.get_status(entry) + results[entry]["type"] = self.conn.get_type(entry) + results[entry]["uuid"] = self.conn.get_uuid(entry) + if self.conn.find_entry(entry).isActive(): + results[entry]["volume_count"] = self.conn.get_volume_count(entry) + results[entry]["volumes"] = list() + for volume in self.conn.get_volume_names(entry): + results[entry]["volumes"].append(volume) + else: + results[entry]["volume_count"] = -1 + + try: + results[entry]["path"] = self.conn.get_path(entry) + except ValueError: + pass + + try: + results[entry]["host"] = self.conn.get_host(entry) + except ValueError: + pass + + try: + results[entry]["source_path"] = self.conn.get_source_path(entry) + except ValueError: + pass + + try: + results[entry]["format"] = self.conn.get_format(entry) + except ValueError: + pass + + try: + devices = self.conn.get_devices(entry) + results[entry]["devices"] = devices + except ValueError: + pass + + else: + results[entry]["state"] = self.conn.get_status(entry) + + facts = dict() + if facts_mode == 'facts': + facts["ansible_facts"] = dict() + facts["ansible_facts"]["ansible_libvirt_pools"] = results + elif facts_mode == 'info': + facts['pools'] = results + return facts + + +def core(module): + + state = module.params.get('state', None) + name = module.params.get('name', None) + command = module.params.get('command', None) + uri = module.params.get('uri', None) + xml = module.params.get('xml', None) + autostart = module.params.get('autostart', None) + mode = module.params.get('mode', None) + + v = VirtStoragePool(uri, module) + res = {} + + if state and command == 'list_pools': + res = v.list_pools(state=state) + if not isinstance(res, dict): + res = {command: res} + return VIRT_SUCCESS, res + + if state: + if not name: + module.fail_json(msg="state change requires a specified name") + + res['changed'] = False + if state in ['active']: + if v.status(name) != 'active': + res['changed'] = True + res['msg'] = v.start(name) + elif state in ['present']: + try: + v.get_pool(name) + except EntryNotFound: + if not xml: + module.fail_json(msg="storage pool '" + name + "' not present, but xml not specified") + v.define(name, xml) + res = {'changed': True, 'created': name} + elif state in ['inactive']: + entries = v.list_pools() + if name in entries: + if v.status(name) != 'inactive': + res['changed'] = True + res['msg'] = v.destroy(name) + elif state in ['undefined', 'absent']: + entries = v.list_pools() + if name in entries: + if v.status(name) != 'inactive': + v.destroy(name) + res['changed'] = True + res['msg'] = v.undefine(name) + elif state in ['deleted']: + entries = v.list_pools() + if name in entries: + if v.status(name) != 'inactive': + v.destroy(name) + v.delete(name, mode) + res['changed'] = True + res['msg'] = v.undefine(name) + else: + module.fail_json(msg="unexpected state") + + return VIRT_SUCCESS, res + + if command: + if command in ENTRY_COMMANDS: + if not name: + module.fail_json(msg="%s requires 1 argument: name" % command) + if command == 'define': + if not xml: + module.fail_json(msg="define requires xml argument") + try: + v.get_pool(name) + except EntryNotFound: + v.define(name, xml) + res = {'changed': True, 'created': name} + return VIRT_SUCCESS, res + elif command == 'build': + res = v.build(name, mode) + if not isinstance(res, dict): + res = {'changed': True, command: res} + return VIRT_SUCCESS, res + elif command == 'delete': + res = v.delete(name, mode) + if not isinstance(res, dict): + res = {'changed': True, command: res} + return VIRT_SUCCESS, res + res = getattr(v, command)(name) + if not isinstance(res, dict): + res = {command: res} + return VIRT_SUCCESS, res + + elif hasattr(v, command): + res = getattr(v, command)() + if not isinstance(res, dict): + res = {command: res} + return VIRT_SUCCESS, res + + else: + module.fail_json(msg="Command %s not recognized" % command) + + if autostart is not None: + if not name: + module.fail_json(msg="state change requires a specified name") + + res['changed'] = False + if autostart: + if not v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, True) + else: + if v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, False) + + return VIRT_SUCCESS, res + + module.fail_json(msg="expected state or command parameter to be specified") + + +def main(): + + module = AnsibleModule( + argument_spec=dict( + name=dict(aliases=['pool']), + state=dict(choices=['active', 'inactive', 'present', 'absent', 'undefined', 'deleted']), + command=dict(choices=ALL_COMMANDS), + uri=dict(default='qemu:///system'), + xml=dict(), + autostart=dict(type='bool'), + mode=dict(choices=ALL_MODES), + ), + supports_check_mode=True + ) + + if not HAS_VIRT: + module.fail_json( + msg='The `libvirt` module is not importable. Check the requirements.' + ) + + if not HAS_XML: + module.fail_json( + msg='The `lxml` module is not importable. Check the requirements.' + ) + + rc = VIRT_SUCCESS + try: + rc, result = core(module) + except Exception as e: + module.fail_json(msg=str(e)) + + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=result) + else: + module.exit_json(**result) + + +if __name__ == '__main__': + main() |