summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/libvirt/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/libvirt/plugins
parentInitial commit. (diff)
downloadansible-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')
-rw-r--r--ansible_collections/community/libvirt/plugins/connection/libvirt_lxc.py183
-rw-r--r--ansible_collections/community/libvirt/plugins/connection/libvirt_qemu.py361
-rw-r--r--ansible_collections/community/libvirt/plugins/doc_fragments/__init__.py0
-rw-r--r--ansible_collections/community/libvirt/plugins/doc_fragments/requirements.py14
-rw-r--r--ansible_collections/community/libvirt/plugins/doc_fragments/virt.py67
-rw-r--r--ansible_collections/community/libvirt/plugins/inventory/__init__.py0
-rw-r--r--ansible_collections/community/libvirt/plugins/inventory/libvirt.py212
-rw-r--r--ansible_collections/community/libvirt/plugins/modules/virt.py563
-rw-r--r--ansible_collections/community/libvirt/plugins/modules/virt_net.py647
-rw-r--r--ansible_collections/community/libvirt/plugins/modules/virt_pool.py707
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()