895 lines
29 KiB
Diff
895 lines
29 KiB
Diff
|
From 5c41a5b8c9925bf788946e334cb3912ca9b09190 Mon Sep 17 00:00:00 2001
|
||
|
From: =?UTF-8?q?C=C3=A9dric=20Bosdonnat?= <cbosdonnat@suse.com>
|
||
|
Date: Fri, 9 Mar 2018 15:46:12 +0100
|
||
|
Subject: [PATCH] Add engine relaying libvirt events
|
||
|
|
||
|
Libvirt API offers clients to register callbacks for various events.
|
||
|
libvirt_events engine will listen on a libvirt URI (local or remote)
|
||
|
for events and send them to the salt event bus.
|
||
|
|
||
|
Special thanks to @isbm for the code cleanup help
|
||
|
---
|
||
|
salt/engines/libvirt_events.py | 702 ++++++++++++++++++++++
|
||
|
tests/unit/engines/test_libvirt_events.py | 159 +++++
|
||
|
2 files changed, 861 insertions(+)
|
||
|
create mode 100644 salt/engines/libvirt_events.py
|
||
|
create mode 100644 tests/unit/engines/test_libvirt_events.py
|
||
|
|
||
|
diff --git a/salt/engines/libvirt_events.py b/salt/engines/libvirt_events.py
|
||
|
new file mode 100644
|
||
|
index 0000000000..a1c9d09067
|
||
|
--- /dev/null
|
||
|
+++ b/salt/engines/libvirt_events.py
|
||
|
@@ -0,0 +1,702 @@
|
||
|
+# -*- coding: utf-8 -*-
|
||
|
+
|
||
|
+'''
|
||
|
+An engine that listens for libvirt events and resends them to the salt event bus.
|
||
|
+
|
||
|
+The minimal configuration is the following and will listen to all events on the
|
||
|
+local hypervisor and send them with a tag starting with ``salt/engines/libvirt_events``:
|
||
|
+
|
||
|
+.. code-block:: yaml
|
||
|
+
|
||
|
+ engines:
|
||
|
+ - libvirt_events
|
||
|
+
|
||
|
+Note that the automatically-picked libvirt connection will depend on the value
|
||
|
+of ``uri_default`` in ``/etc/libvirt/libvirt.conf``. To force using another
|
||
|
+connection like the local LXC libvirt driver, set the ``uri`` property as in the
|
||
|
+following example configuration.
|
||
|
+
|
||
|
+.. code-block:: yaml
|
||
|
+
|
||
|
+ engines:
|
||
|
+ - libvirt_events:
|
||
|
+ uri: lxc:///
|
||
|
+ tag_prefix: libvirt
|
||
|
+ filters:
|
||
|
+ - domain/lifecycle
|
||
|
+ - domain/reboot
|
||
|
+ - pool
|
||
|
+
|
||
|
+Filters is a list of event types to relay to the event bus. Items in this list
|
||
|
+can be either one of the main types (``domain``, ``network``, ``pool``,
|
||
|
+``nodedev``, ``secret``), ``all`` or a more precise filter. These can be done
|
||
|
+with values like <main_type>/<subtype>. The possible values are in the
|
||
|
+CALLBACK_DEFS constant. If the filters list contains ``all``, all
|
||
|
+events will be relayed.
|
||
|
+
|
||
|
+Be aware that the list of events increases with libvirt versions, for example
|
||
|
+network events have been added in libvirt 1.2.1.
|
||
|
+
|
||
|
+Running the engine on non-root
|
||
|
+------------------------------
|
||
|
+
|
||
|
+Running this engine as non-root requires a special attention, which is surely
|
||
|
+the case for the master running as user `salt`. The engine is likely to fail
|
||
|
+to connect to libvirt with an error like this one:
|
||
|
+
|
||
|
+ [ERROR ] authentication unavailable: no polkit agent available to authenticate action 'org.libvirt.unix.monitor'
|
||
|
+
|
||
|
+
|
||
|
+To fix this, the user running the engine, for example the salt-master, needs
|
||
|
+to have the rights to connect to libvirt in the machine polkit config.
|
||
|
+A polkit rule like the following one will allow `salt` user to connect to libvirt:
|
||
|
+
|
||
|
+.. code-block:: javascript
|
||
|
+
|
||
|
+ polkit.addRule(function(action, subject) {
|
||
|
+ if (action.id.indexOf("org.libvirt") == 0 &&
|
||
|
+ subject.user == "salt") {
|
||
|
+ return polkit.Result.YES;
|
||
|
+ }
|
||
|
+ });
|
||
|
+
|
||
|
+:depends: libvirt 1.0.0+ python binding
|
||
|
+
|
||
|
+.. versionadded:: Fluorine
|
||
|
+'''
|
||
|
+
|
||
|
+from __future__ import absolute_import, unicode_literals, print_function
|
||
|
+import logging
|
||
|
+
|
||
|
+# Import salt libs
|
||
|
+import salt.utils.event
|
||
|
+
|
||
|
+# pylint: disable=no-name-in-module,import-error
|
||
|
+from salt.ext.six.moves.urllib.parse import urlparse
|
||
|
+# pylint: enable=no-name-in-module,import-error
|
||
|
+
|
||
|
+log = logging.getLogger(__name__)
|
||
|
+
|
||
|
+
|
||
|
+try:
|
||
|
+ import libvirt
|
||
|
+except ImportError:
|
||
|
+ libvirt = None # pylint: disable=invalid-name
|
||
|
+
|
||
|
+
|
||
|
+def __virtual__():
|
||
|
+ '''
|
||
|
+ Only load if libvirt python binding is present
|
||
|
+ '''
|
||
|
+ if libvirt is None:
|
||
|
+ msg = 'libvirt module not found'
|
||
|
+ elif libvirt.getVersion() < 1000000:
|
||
|
+ msg = 'libvirt >= 1.0.0 required'
|
||
|
+ else:
|
||
|
+ msg = ''
|
||
|
+ return not bool(msg), msg
|
||
|
+
|
||
|
+
|
||
|
+REGISTER_FUNCTIONS = {
|
||
|
+ 'domain': 'domainEventRegisterAny',
|
||
|
+ 'network': 'networkEventRegisterAny',
|
||
|
+ 'pool': 'storagePoolEventRegisterAny',
|
||
|
+ 'nodedev': 'nodeDeviceEventRegisterAny',
|
||
|
+ 'secret': 'secretEventRegisterAny'
|
||
|
+}
|
||
|
+
|
||
|
+# Handle either BLOCK_JOB or BLOCK_JOB_2, but prefer the latter
|
||
|
+if hasattr(libvirt, 'VIR_DOMAIN_EVENT_ID_BLOCK_JOB_2'):
|
||
|
+ BLOCK_JOB_ID = 'VIR_DOMAIN_EVENT_ID_BLOCK_JOB_2'
|
||
|
+else:
|
||
|
+ BLOCK_JOB_ID = 'VIR_DOMAIN_EVENT_ID_BLOCK_JOB'
|
||
|
+
|
||
|
+CALLBACK_DEFS = {
|
||
|
+ 'domain': (('lifecycle', None),
|
||
|
+ ('reboot', None),
|
||
|
+ ('rtc_change', None),
|
||
|
+ ('watchdog', None),
|
||
|
+ ('graphics', None),
|
||
|
+ ('io_error', 'VIR_DOMAIN_EVENT_ID_IO_ERROR_REASON'),
|
||
|
+ ('control_error', None),
|
||
|
+ ('disk_change', None),
|
||
|
+ ('tray_change', None),
|
||
|
+ ('pmwakeup', None),
|
||
|
+ ('pmsuspend', None),
|
||
|
+ ('balloon_change', None),
|
||
|
+ ('pmsuspend_disk', None),
|
||
|
+ ('device_removed', None),
|
||
|
+ ('block_job', BLOCK_JOB_ID),
|
||
|
+ ('tunable', None),
|
||
|
+ ('agent_lifecycle', None),
|
||
|
+ ('device_added', None),
|
||
|
+ ('migration_iteration', None),
|
||
|
+ ('job_completed', None),
|
||
|
+ ('device_removal_failed', None),
|
||
|
+ ('metadata_change', None),
|
||
|
+ ('block_threshold', None)),
|
||
|
+ 'network': (('lifecycle', None),),
|
||
|
+ 'pool': (('lifecycle', None),
|
||
|
+ ('refresh', None)),
|
||
|
+ 'nodedev': (('lifecycle', None),
|
||
|
+ ('update', None)),
|
||
|
+ 'secret': (('lifecycle', None),
|
||
|
+ ('value_changed', None))
|
||
|
+}
|
||
|
+
|
||
|
+
|
||
|
+def _compute_subprefix(attr):
|
||
|
+ '''
|
||
|
+ Get the part before the first '_' or the end of attr including
|
||
|
+ the potential '_'
|
||
|
+ '''
|
||
|
+ return ''.join((attr.split('_')[0], '_' if len(attr.split('_')) > 1 else ''))
|
||
|
+
|
||
|
+
|
||
|
+def _get_libvirt_enum_string(prefix, value):
|
||
|
+ '''
|
||
|
+ Convert the libvirt enum integer value into a human readable string.
|
||
|
+
|
||
|
+ :param prefix: start of the libvirt attribute to look for.
|
||
|
+ :param value: integer to convert to string
|
||
|
+ '''
|
||
|
+ attributes = [attr[len(prefix):] for attr in libvirt.__dict__ if attr.startswith(prefix)]
|
||
|
+
|
||
|
+ # Filter out the values starting with a common base as they match another enum
|
||
|
+ prefixes = [_compute_subprefix(p) for p in attributes]
|
||
|
+ counts = {p: prefixes.count(p) for p in prefixes}
|
||
|
+ sub_prefixes = [p for p, count in counts.items() if count > 1]
|
||
|
+ filtered = [attr for attr in attributes if _compute_subprefix(attr) not in sub_prefixes]
|
||
|
+
|
||
|
+ for candidate in filtered:
|
||
|
+ if value == getattr(libvirt, ''.join((prefix, candidate))):
|
||
|
+ name = candidate.lower().replace('_', ' ')
|
||
|
+ return name
|
||
|
+ return 'unknown'
|
||
|
+
|
||
|
+
|
||
|
+def _get_domain_event_detail(event, detail):
|
||
|
+ '''
|
||
|
+ Convert event and detail numeric values into a tuple of human readable strings
|
||
|
+ '''
|
||
|
+ event_name = _get_libvirt_enum_string('VIR_DOMAIN_EVENT_', event)
|
||
|
+ if event_name == 'unknown':
|
||
|
+ return event_name, 'unknown'
|
||
|
+
|
||
|
+ prefix = 'VIR_DOMAIN_EVENT_{0}_'.format(event_name.upper())
|
||
|
+ detail_name = _get_libvirt_enum_string(prefix, detail)
|
||
|
+
|
||
|
+ return event_name, detail_name
|
||
|
+
|
||
|
+
|
||
|
+def _salt_send_event(opaque, conn, data):
|
||
|
+ '''
|
||
|
+ Convenience function adding common data to the event and sending it
|
||
|
+ on the salt event bus.
|
||
|
+
|
||
|
+ :param opaque: the opaque data that is passed to the callback.
|
||
|
+ This is a dict with 'prefix', 'object' and 'event' keys.
|
||
|
+ :param conn: libvirt connection
|
||
|
+ :param data: additional event data dict to send
|
||
|
+ '''
|
||
|
+ tag_prefix = opaque['prefix']
|
||
|
+ object_type = opaque['object']
|
||
|
+ event_type = opaque['event']
|
||
|
+
|
||
|
+ # Prepare the connection URI to fit in the tag
|
||
|
+ # qemu+ssh://user@host:1234/system -> qemu+ssh/user@host:1234/system
|
||
|
+ uri = urlparse(conn.getURI())
|
||
|
+ uri_tag = [uri.scheme]
|
||
|
+ if uri.netloc:
|
||
|
+ uri_tag.append(uri.netloc)
|
||
|
+ path = uri.path.strip('/')
|
||
|
+ if path:
|
||
|
+ uri_tag.append(path)
|
||
|
+ uri_str = "/".join(uri_tag)
|
||
|
+
|
||
|
+ # Append some common data
|
||
|
+ all_data = {
|
||
|
+ 'uri': conn.getURI()
|
||
|
+ }
|
||
|
+ all_data.update(data)
|
||
|
+
|
||
|
+ tag = '/'.join((tag_prefix, uri_str, object_type, event_type))
|
||
|
+
|
||
|
+ # Actually send the event in salt
|
||
|
+ if __opts__.get('__role') == 'master':
|
||
|
+ salt.utils.event.get_master_event(
|
||
|
+ __opts__,
|
||
|
+ __opts__['sock_dir']).fire_event(all_data, tag)
|
||
|
+ else:
|
||
|
+ __salt__['event.send'](tag, all_data)
|
||
|
+
|
||
|
+
|
||
|
+def _salt_send_domain_event(opaque, conn, domain, event, event_data):
|
||
|
+ '''
|
||
|
+ Helper function send a salt event for a libvirt domain.
|
||
|
+
|
||
|
+ :param opaque: the opaque data that is passed to the callback.
|
||
|
+ This is a dict with 'prefix', 'object' and 'event' keys.
|
||
|
+ :param conn: libvirt connection
|
||
|
+ :param domain: name of the domain related to the event
|
||
|
+ :param event: name of the event
|
||
|
+ :param event_data: additional event data dict to send
|
||
|
+ '''
|
||
|
+ data = {
|
||
|
+ 'domain': {
|
||
|
+ 'name': domain.name(),
|
||
|
+ 'id': domain.ID(),
|
||
|
+ 'uuid': domain.UUIDString()
|
||
|
+ },
|
||
|
+ 'event': event
|
||
|
+ }
|
||
|
+ data.update(event_data)
|
||
|
+ _salt_send_event(opaque, conn, data)
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_lifecycle_cb(conn, domain, event, detail, opaque):
|
||
|
+ '''
|
||
|
+ Domain lifecycle events handler
|
||
|
+ '''
|
||
|
+ event_str, detail_str = _get_domain_event_detail(event, detail)
|
||
|
+
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'event': event_str,
|
||
|
+ 'detail': detail_str
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_reboot_cb(conn, domain, opaque):
|
||
|
+ '''
|
||
|
+ Domain reboot events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {})
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_rtc_change_cb(conn, domain, utcoffset, opaque):
|
||
|
+ '''
|
||
|
+ Domain RTC change events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'utcoffset': utcoffset
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_watchdog_cb(conn, domain, action, opaque):
|
||
|
+ '''
|
||
|
+ Domain watchdog events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'action': _get_libvirt_enum_string('VIR_DOMAIN_EVENT_WATCHDOG_', action)
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_io_error_cb(conn, domain, srcpath, devalias, action, reason, opaque):
|
||
|
+ '''
|
||
|
+ Domain I/O Error events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'srcPath': srcpath,
|
||
|
+ 'dev': devalias,
|
||
|
+ 'action': _get_libvirt_enum_string('VIR_DOMAIN_EVENT_IO_ERROR_', action),
|
||
|
+ 'reason': reason
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_graphics_cb(conn, domain, phase, local, remote, auth, subject, opaque):
|
||
|
+ '''
|
||
|
+ Domain graphics events handler
|
||
|
+ '''
|
||
|
+ prefix = 'VIR_DOMAIN_EVENT_GRAPHICS_'
|
||
|
+
|
||
|
+ def get_address(addr):
|
||
|
+ '''
|
||
|
+ transform address structure into event data piece
|
||
|
+ '''
|
||
|
+ data = {'family': _get_libvirt_enum_string('{0}_ADDRESS_'.format(prefix), addr['family']),
|
||
|
+ 'node': addr['node'],
|
||
|
+ 'service': addr['service']}
|
||
|
+ return addr
|
||
|
+
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'phase': _get_libvirt_enum_string(prefix, phase),
|
||
|
+ 'local': get_address(local),
|
||
|
+ 'remote': get_address(remote),
|
||
|
+ 'authScheme': auth,
|
||
|
+ 'subject': [{'type': item[0], 'name': item[1]} for item in subject]
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_control_error_cb(conn, domain, opaque):
|
||
|
+ '''
|
||
|
+ Domain control error events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {})
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_disk_change_cb(conn, domain, old_src, new_src, dev, reason, opaque):
|
||
|
+ '''
|
||
|
+ Domain disk change events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'oldSrcPath': old_src,
|
||
|
+ 'newSrcPath': new_src,
|
||
|
+ 'dev': dev,
|
||
|
+ 'reason': _get_libvirt_enum_string('VIR_DOMAIN_EVENT_DISK_', reason)
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_tray_change_cb(conn, domain, dev, reason, opaque):
|
||
|
+ '''
|
||
|
+ Domain tray change events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'dev': dev,
|
||
|
+ 'reason': _get_libvirt_enum_string('VIR_DOMAIN_EVENT_TRAY_CHANGE_', reason)
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_pmwakeup_cb(conn, domain, reason, opaque):
|
||
|
+ '''
|
||
|
+ Domain wakeup events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'reason': 'unknown' # currently unused
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_pmsuspend_cb(conn, domain, reason, opaque):
|
||
|
+ '''
|
||
|
+ Domain suspend events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'reason': 'unknown' # currently unused
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_balloon_change_cb(conn, domain, actual, opaque):
|
||
|
+ '''
|
||
|
+ Domain balloon change events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'actual': actual
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_pmsuspend_disk_cb(conn, domain, reason, opaque):
|
||
|
+ '''
|
||
|
+ Domain disk suspend events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'reason': 'unknown' # currently unused
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_block_job_cb(conn, domain, disk, job_type, status, opaque):
|
||
|
+ '''
|
||
|
+ Domain block job events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'disk': disk,
|
||
|
+ 'type': _get_libvirt_enum_string('VIR_DOMAIN_BLOCK_JOB_TYPE_', job_type),
|
||
|
+ 'status': _get_libvirt_enum_string('VIR_DOMAIN_BLOCK_JOB_', status)
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_device_removed_cb(conn, domain, dev, opaque):
|
||
|
+ '''
|
||
|
+ Domain device removal events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'dev': dev
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_tunable_cb(conn, domain, params, opaque):
|
||
|
+ '''
|
||
|
+ Domain tunable events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'params': params
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+# pylint: disable=invalid-name
|
||
|
+def _domain_event_agent_lifecycle_cb(conn, domain, state, reason, opaque):
|
||
|
+ '''
|
||
|
+ Domain agent lifecycle events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'state': _get_libvirt_enum_string('VIR_CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_STATE_', state),
|
||
|
+ 'reason': _get_libvirt_enum_string('VIR_CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_', reason)
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_device_added_cb(conn, domain, dev, opaque):
|
||
|
+ '''
|
||
|
+ Domain device addition events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'dev': dev
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+# pylint: disable=invalid-name
|
||
|
+def _domain_event_migration_iteration_cb(conn, domain, iteration, opaque):
|
||
|
+ '''
|
||
|
+ Domain migration iteration events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'iteration': iteration
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_job_completed_cb(conn, domain, params, opaque):
|
||
|
+ '''
|
||
|
+ Domain job completion events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'params': params
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_device_removal_failed_cb(conn, domain, dev, opaque):
|
||
|
+ '''
|
||
|
+ Domain device removal failure events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'dev': dev
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_metadata_change_cb(conn, domain, mtype, nsuri, opaque):
|
||
|
+ '''
|
||
|
+ Domain metadata change events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'type': _get_libvirt_enum_string('VIR_DOMAIN_METADATA_', mtype),
|
||
|
+ 'nsuri': nsuri
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _domain_event_block_threshold_cb(conn, domain, dev, path, threshold, excess, opaque):
|
||
|
+ '''
|
||
|
+ Domain block threshold events handler
|
||
|
+ '''
|
||
|
+ _salt_send_domain_event(opaque, conn, domain, opaque['event'], {
|
||
|
+ 'dev': dev,
|
||
|
+ 'path': path,
|
||
|
+ 'threshold': threshold,
|
||
|
+ 'excess': excess
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _network_event_lifecycle_cb(conn, net, event, detail, opaque):
|
||
|
+ '''
|
||
|
+ Network lifecycle events handler
|
||
|
+ '''
|
||
|
+
|
||
|
+ _salt_send_event(opaque, conn, {
|
||
|
+ 'network': {
|
||
|
+ 'name': net.name(),
|
||
|
+ 'uuid': net.UUIDString()
|
||
|
+ },
|
||
|
+ 'event': _get_libvirt_enum_string('VIR_NETWORK_EVENT_', event),
|
||
|
+ 'detail': 'unknown' # currently unused
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _pool_event_lifecycle_cb(conn, pool, event, detail, opaque):
|
||
|
+ '''
|
||
|
+ Storage pool lifecycle events handler
|
||
|
+ '''
|
||
|
+ _salt_send_event(opaque, conn, {
|
||
|
+ 'pool': {
|
||
|
+ 'name': pool.name(),
|
||
|
+ 'uuid': pool.UUIDString()
|
||
|
+ },
|
||
|
+ 'event': _get_libvirt_enum_string('VIR_STORAGE_POOL_EVENT_', event),
|
||
|
+ 'detail': 'unknown' # currently unused
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _pool_event_refresh_cb(conn, pool, opaque):
|
||
|
+ '''
|
||
|
+ Storage pool refresh events handler
|
||
|
+ '''
|
||
|
+ _salt_send_event(opaque, conn, {
|
||
|
+ 'pool': {
|
||
|
+ 'name': pool.name(),
|
||
|
+ 'uuid': pool.UUIDString()
|
||
|
+ },
|
||
|
+ 'event': opaque['event']
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _nodedev_event_lifecycle_cb(conn, dev, event, detail, opaque):
|
||
|
+ '''
|
||
|
+ Node device lifecycle events handler
|
||
|
+ '''
|
||
|
+ _salt_send_event(opaque, conn, {
|
||
|
+ 'nodedev': {
|
||
|
+ 'name': dev.name()
|
||
|
+ },
|
||
|
+ 'event': _get_libvirt_enum_string('VIR_NODE_DEVICE_EVENT_', event),
|
||
|
+ 'detail': 'unknown' # currently unused
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _nodedev_event_update_cb(conn, dev, opaque):
|
||
|
+ '''
|
||
|
+ Node device update events handler
|
||
|
+ '''
|
||
|
+ _salt_send_event(opaque, conn, {
|
||
|
+ 'nodedev': {
|
||
|
+ 'name': dev.name()
|
||
|
+ },
|
||
|
+ 'event': opaque['event']
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _secret_event_lifecycle_cb(conn, secret, event, detail, opaque):
|
||
|
+ '''
|
||
|
+ Secret lifecycle events handler
|
||
|
+ '''
|
||
|
+ _salt_send_event(opaque, conn, {
|
||
|
+ 'secret': {
|
||
|
+ 'uuid': secret.UUIDString()
|
||
|
+ },
|
||
|
+ 'event': _get_libvirt_enum_string('VIR_SECRET_EVENT_', event),
|
||
|
+ 'detail': 'unknown' # currently unused
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _secret_event_value_changed_cb(conn, secret, opaque):
|
||
|
+ '''
|
||
|
+ Secret value change events handler
|
||
|
+ '''
|
||
|
+ _salt_send_event(opaque, conn, {
|
||
|
+ 'secret': {
|
||
|
+ 'uuid': secret.UUIDString()
|
||
|
+ },
|
||
|
+ 'event': opaque['event']
|
||
|
+ })
|
||
|
+
|
||
|
+
|
||
|
+def _cleanup(cnx):
|
||
|
+ '''
|
||
|
+ Close the libvirt connection
|
||
|
+
|
||
|
+ :param cnx: libvirt connection
|
||
|
+ '''
|
||
|
+ log.debug('Closing libvirt connection: %s', cnx.getURI())
|
||
|
+ cnx.close()
|
||
|
+
|
||
|
+
|
||
|
+def _callbacks_cleanup(cnx, callback_ids):
|
||
|
+ '''
|
||
|
+ Unregister all the registered callbacks
|
||
|
+
|
||
|
+ :param cnx: libvirt connection
|
||
|
+ :param callback_ids: dictionary mapping a libvirt object type to an ID list
|
||
|
+ of callbacks to deregister
|
||
|
+ '''
|
||
|
+ for obj, ids in callback_ids.items():
|
||
|
+ register_name = REGISTER_FUNCTIONS[obj]
|
||
|
+ deregister_name = register_name.replace('Reg', 'Dereg')
|
||
|
+ deregister = getattr(cnx, deregister_name)
|
||
|
+ for callback_id in ids:
|
||
|
+ deregister(callback_id)
|
||
|
+
|
||
|
+
|
||
|
+def _register_callback(cnx, tag_prefix, obj, event, real_id):
|
||
|
+ '''
|
||
|
+ Helper function registering a callback
|
||
|
+
|
||
|
+ :param cnx: libvirt connection
|
||
|
+ :param tag_prefix: salt event tag prefix to use
|
||
|
+ :param obj: the libvirt object name for the event. Needs to
|
||
|
+ be one of the REGISTER_FUNCTIONS keys.
|
||
|
+ :param event: the event type name.
|
||
|
+ :param real_id: the libvirt name of an alternative event id to use or None
|
||
|
+
|
||
|
+ :rtype integer value needed to deregister the callback
|
||
|
+ '''
|
||
|
+ libvirt_name = real_id
|
||
|
+ if real_id is None:
|
||
|
+ libvirt_name = 'VIR_{0}_EVENT_ID_{1}'.format(obj, event).upper()
|
||
|
+
|
||
|
+ if not hasattr(libvirt, libvirt_name):
|
||
|
+ log.warning('Skipping "%s/%s" events: libvirt too old', obj, event)
|
||
|
+ return None
|
||
|
+
|
||
|
+ libvirt_id = getattr(libvirt, libvirt_name)
|
||
|
+ callback_name = "_{0}_event_{1}_cb".format(obj, event)
|
||
|
+ callback = globals().get(callback_name, None)
|
||
|
+ if callback is None:
|
||
|
+ log.error('Missing function %s in engine', callback_name)
|
||
|
+ return None
|
||
|
+
|
||
|
+ register = getattr(cnx, REGISTER_FUNCTIONS[obj])
|
||
|
+ return register(None, libvirt_id, callback,
|
||
|
+ {'prefix': tag_prefix,
|
||
|
+ 'object': obj,
|
||
|
+ 'event': event})
|
||
|
+
|
||
|
+
|
||
|
+def _append_callback_id(ids, obj, callback_id):
|
||
|
+ '''
|
||
|
+ Helper function adding a callback ID to the IDs dict.
|
||
|
+ The callback ids dict maps an object to event callback ids.
|
||
|
+
|
||
|
+ :param ids: dict of callback IDs to update
|
||
|
+ :param obj: one of the keys of REGISTER_FUNCTIONS
|
||
|
+ :param callback_id: the result of _register_callback
|
||
|
+ '''
|
||
|
+ if obj not in ids:
|
||
|
+ ids[obj] = []
|
||
|
+ ids[obj].append(callback_id)
|
||
|
+
|
||
|
+
|
||
|
+def start(uri=None,
|
||
|
+ tag_prefix='salt/engines/libvirt_events',
|
||
|
+ filters=None):
|
||
|
+ '''
|
||
|
+ Listen to libvirt events and forward them to salt.
|
||
|
+
|
||
|
+ :param uri: libvirt URI to listen on.
|
||
|
+ Defaults to None to pick the first available local hypervisor
|
||
|
+ :param tag_prefix: the begining of the salt event tag to use.
|
||
|
+ Defaults to 'salt/engines/libvirt_events'
|
||
|
+ :param filters: the list of event of listen on. Defaults to 'all'
|
||
|
+ '''
|
||
|
+ if filters is None:
|
||
|
+ filters = ['all']
|
||
|
+ try:
|
||
|
+ libvirt.virEventRegisterDefaultImpl()
|
||
|
+
|
||
|
+ cnx = libvirt.openReadOnly(uri)
|
||
|
+ log.debug('Opened libvirt uri: %s', cnx.getURI())
|
||
|
+
|
||
|
+ callback_ids = {}
|
||
|
+ all_filters = "all" in filters
|
||
|
+
|
||
|
+ for obj, event_defs in CALLBACK_DEFS.items():
|
||
|
+ for event, real_id in event_defs:
|
||
|
+ event_filter = "/".join((obj, event))
|
||
|
+ if event_filter not in filters and obj not in filters and not all_filters:
|
||
|
+ continue
|
||
|
+ registered_id = _register_callback(cnx, tag_prefix,
|
||
|
+ obj, event, real_id)
|
||
|
+ if registered_id:
|
||
|
+ _append_callback_id(callback_ids, obj, registered_id)
|
||
|
+
|
||
|
+ exit_loop = False
|
||
|
+ while not exit_loop:
|
||
|
+ exit_loop = libvirt.virEventRunDefaultImpl() < 0
|
||
|
+
|
||
|
+ except Exception as err: # pylint: disable=broad-except
|
||
|
+ log.exception(err)
|
||
|
+ finally:
|
||
|
+ _callbacks_cleanup(cnx, callback_ids)
|
||
|
+ _cleanup(cnx)
|
||
|
diff --git a/tests/unit/engines/test_libvirt_events.py b/tests/unit/engines/test_libvirt_events.py
|
||
|
new file mode 100644
|
||
|
index 0000000000..6608aaf648
|
||
|
--- /dev/null
|
||
|
+++ b/tests/unit/engines/test_libvirt_events.py
|
||
|
@@ -0,0 +1,159 @@
|
||
|
+# -*- coding: utf-8 -*-
|
||
|
+'''
|
||
|
+unit tests for the libvirt_events engine
|
||
|
+'''
|
||
|
+# Import Python libs
|
||
|
+from __future__ import absolute_import, print_function, unicode_literals
|
||
|
+
|
||
|
+# Import Salt Testing Libs
|
||
|
+from tests.support.mixins import LoaderModuleMockMixin
|
||
|
+from tests.support.unit import skipIf, TestCase
|
||
|
+from tests.support.mock import (
|
||
|
+ NO_MOCK,
|
||
|
+ NO_MOCK_REASON,
|
||
|
+ MagicMock,
|
||
|
+ patch)
|
||
|
+
|
||
|
+# Import Salt Libs
|
||
|
+import salt.engines.libvirt_events as libvirt_events
|
||
|
+
|
||
|
+
|
||
|
+# pylint: disable=protected-access,attribute-defined-outside-init,invalid-name,unused-argument,no-self-use
|
||
|
+
|
||
|
+
|
||
|
+@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||
|
+class EngineLibvirtEventTestCase(TestCase, LoaderModuleMockMixin):
|
||
|
+ '''
|
||
|
+ Test cases for salt.engine.libvirt_events
|
||
|
+ '''
|
||
|
+
|
||
|
+ def setup_loader_modules(self):
|
||
|
+ patcher = patch('salt.engines.libvirt_events.libvirt')
|
||
|
+ self.mock_libvirt = patcher.start()
|
||
|
+ self.mock_libvirt.getVersion.return_value = 2000000
|
||
|
+ self.mock_libvirt.virEventRunDefaultImpl.return_value = -1 # Don't loop for ever
|
||
|
+ self.mock_libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE = 0
|
||
|
+ self.mock_libvirt.VIR_DOMAIN_EVENT_ID_REBOOT = 1
|
||
|
+ self.addCleanup(patcher.stop)
|
||
|
+ self.addCleanup(delattr, self, 'mock_libvirt')
|
||
|
+ return {libvirt_events: {}}
|
||
|
+
|
||
|
+ @patch('salt.engines.libvirt_events.libvirt',
|
||
|
+ VIR_PREFIX_NONE=0,
|
||
|
+ VIR_PREFIX_ONE=1,
|
||
|
+ VIR_PREFIX_TWO=2,
|
||
|
+ VIR_PREFIX_SUB_FOO=0,
|
||
|
+ VIR_PREFIX_SUB_BAR=1,
|
||
|
+ VIR_PREFIX_SUB_FOOBAR=2)
|
||
|
+ def test_get_libvirt_enum_string_subprefix(self, libvirt_mock):
|
||
|
+ '''
|
||
|
+ Make sure the libvirt enum value to string works reliably with
|
||
|
+ elements with a sub prefix, eg VIR_PREFIX_SUB_* in this case.
|
||
|
+ '''
|
||
|
+ # Test case with a sub prefix
|
||
|
+
|
||
|
+ assert libvirt_events._get_libvirt_enum_string('VIR_PREFIX_', 2) == 'two'
|
||
|
+
|
||
|
+ @patch('salt.engines.libvirt_events.libvirt',
|
||
|
+ VIR_PREFIX_FOO=0,
|
||
|
+ VIR_PREFIX_FOO_BAR=1,
|
||
|
+ VIR_PREFIX_BAR_FOO=2)
|
||
|
+ def test_get_libvirt_enum_string_underscores(self, libvirt_mock):
|
||
|
+ '''
|
||
|
+ Make sure the libvirt enum value to string works reliably and items
|
||
|
+ with an underscore aren't confused with sub prefixes.
|
||
|
+ '''
|
||
|
+ assert libvirt_events._get_libvirt_enum_string('VIR_PREFIX_', 1) == 'foo bar'
|
||
|
+
|
||
|
+ @patch('salt.engines.libvirt_events.libvirt',
|
||
|
+ VIR_DOMAIN_EVENT_DEFINED=0,
|
||
|
+ VIR_DOMAIN_EVENT_UNDEFINED=1,
|
||
|
+ VIR_DOMAIN_EVENT_DEFINED_ADDED=0,
|
||
|
+ VIR_DOMAIN_EVENT_DEFINED_UPDATED=1)
|
||
|
+ def test_get_domain_event_detail(self, mock_libvirt):
|
||
|
+ '''
|
||
|
+ Test get_domain_event_detail function
|
||
|
+ '''
|
||
|
+ assert libvirt_events._get_domain_event_detail(1, 2) == ('undefined', 'unknown')
|
||
|
+ assert libvirt_events._get_domain_event_detail(0, 1) == ('defined', 'updated')
|
||
|
+ assert libvirt_events._get_domain_event_detail(4, 2) == ('unknown', 'unknown')
|
||
|
+
|
||
|
+ @patch('salt.engines.libvirt_events.libvirt', VIR_NETWORK_EVENT_ID_LIFECYCLE=1000)
|
||
|
+ def test_event_register(self, mock_libvirt):
|
||
|
+ '''
|
||
|
+ Test that the libvirt_events engine actually registers events catch them and cleans
|
||
|
+ before leaving the place.
|
||
|
+ '''
|
||
|
+ mock_cnx = MagicMock()
|
||
|
+ mock_libvirt.openReadOnly.return_value = mock_cnx
|
||
|
+
|
||
|
+ mock_cnx.networkEventRegisterAny.return_value = 10000
|
||
|
+
|
||
|
+ libvirt_events.start('test:///', 'test/prefix')
|
||
|
+
|
||
|
+ # Check that the connection has been opened
|
||
|
+ mock_libvirt.openReadOnly.assert_called_once_with('test:///')
|
||
|
+
|
||
|
+ # Check that the connection has been closed
|
||
|
+ mock_cnx.close.assert_called_once()
|
||
|
+
|
||
|
+ # Check events registration and deregistration
|
||
|
+ mock_cnx.domainEventRegisterAny.assert_any_call(
|
||
|
+ None, mock_libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE,
|
||
|
+ libvirt_events._domain_event_lifecycle_cb,
|
||
|
+ {'prefix': 'test/prefix', 'object': 'domain', 'event': 'lifecycle'})
|
||
|
+ mock_cnx.networkEventRegisterAny.assert_any_call(
|
||
|
+ None, mock_libvirt.VIR_NETWORK_EVENT_ID_LIFECYCLE,
|
||
|
+ libvirt_events._network_event_lifecycle_cb,
|
||
|
+ {'prefix': 'test/prefix', 'object': 'network', 'event': 'lifecycle'})
|
||
|
+
|
||
|
+ # Check that the deregister events are called with the result of register
|
||
|
+ mock_cnx.networkEventDeregisterAny.assert_called_with(
|
||
|
+ mock_cnx.networkEventRegisterAny.return_value)
|
||
|
+
|
||
|
+ # Check that the default 'all' filter actually worked
|
||
|
+ counts = {obj: len(callback_def) for obj, callback_def in libvirt_events.CALLBACK_DEFS.items()}
|
||
|
+ for obj, count in counts.items():
|
||
|
+ register = libvirt_events.REGISTER_FUNCTIONS[obj]
|
||
|
+ assert getattr(mock_cnx, register).call_count == count
|
||
|
+
|
||
|
+ def test_event_skipped(self):
|
||
|
+ '''
|
||
|
+ Test that events are skipped if their ID isn't defined in the libvirt
|
||
|
+ module (older libvirt)
|
||
|
+ '''
|
||
|
+ self.mock_libvirt.mock_add_spec([
|
||
|
+ 'openReadOnly',
|
||
|
+ 'virEventRegisterDefaultImpl',
|
||
|
+ 'virEventRunDefaultImpl',
|
||
|
+ 'VIR_DOMAIN_EVENT_ID_LIFECYCLE'], spec_set=True)
|
||
|
+
|
||
|
+ libvirt_events.start('test:///', 'test/prefix')
|
||
|
+
|
||
|
+ # Check events registration and deregistration
|
||
|
+ mock_cnx = self.mock_libvirt.openReadOnly.return_value
|
||
|
+
|
||
|
+ mock_cnx.domainEventRegisterAny.assert_any_call(
|
||
|
+ None, self.mock_libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE,
|
||
|
+ libvirt_events._domain_event_lifecycle_cb,
|
||
|
+ {'prefix': 'test/prefix', 'object': 'domain', 'event': 'lifecycle'})
|
||
|
+
|
||
|
+ # Network events should have been skipped
|
||
|
+ mock_cnx.networkEventRegisterAny.assert_not_called()
|
||
|
+
|
||
|
+ def test_event_filtered(self):
|
||
|
+ '''
|
||
|
+ Test that events are skipped if their ID isn't defined in the libvirt
|
||
|
+ module (older libvirt)
|
||
|
+ '''
|
||
|
+ libvirt_events.start('test', 'test/prefix', 'domain/lifecycle')
|
||
|
+
|
||
|
+ # Check events registration and deregistration
|
||
|
+ mock_cnx = self.mock_libvirt.openReadOnly.return_value
|
||
|
+
|
||
|
+ mock_cnx.domainEventRegisterAny.assert_any_call(
|
||
|
+ None, 0, libvirt_events._domain_event_lifecycle_cb,
|
||
|
+ {'prefix': 'test/prefix', 'object': 'domain', 'event': 'lifecycle'})
|
||
|
+
|
||
|
+ # Network events should have been filtered out
|
||
|
+ mock_cnx.networkEventRegisterAny.assert_not_called()
|
||
|
--
|
||
|
2.17.1
|
||
|
|
||
|
|