salt/add-engine-relaying-libvirt-events.patch
Bo Maryniuk 7966fde1ef Accepting request 626472 from home:mdinca:branches:systemsmanagement:saltstack
- Update to 2018.3.2
  See https://docs.saltstack.com/en/latest/topics/releases/2018.3.2.html
  for full changelog
- Added:
  * accounting-for-when-files-in-an-archive-contain-non-.patch
  * add-all_versions-parameter-to-include-all-installed-.patch
  * add-custom-suse-capabilities-as-grains.patch
  * add-engine-relaying-libvirt-events.patch
  * add-environment-variable-to-know-if-yum-is-invoked-f.patch
  * add-other-attribute-to-gecos-fields-to-avoid-inconsi.patch
  * align-suse-salt-master.service-limitnofiles-limit-wi.patch
  * avoid-incomprehensive-message-if-crashes.patch
  * fix-deprecation-warning-bsc-1095507.patch
  * fix-diffing-binary-files-in-file.get_diff-bsc-109839.patch
  * fix-unboundlocalerror-in-file.get_diff.patch
  * fix-zypper.list_pkgs-to-be-aligned-with-pkg-state.patch
  * prevent-zypper-from-parsing-repo-configuration-from-.patch
  * remove-old-hack-when-reporting-multiversion-packages.patch
  * show-recommendations-for-salt-ssh-cross-version-pyth.patch
- Modified:
  * activate-all-beacons-sources-config-pillar-grains.patch
  * add-saltssh-multi-version-support-across-python-inte.patch
  * avoid-excessive-syslogging-by-watchdog-cronjob-58.patch
  * do-not-override-jid-on-returners-only-sending-back-t.patch
  * enable-passing-a-unix_socket-for-mysql-returners-bsc.patch
  * fall-back-to-pymysql.patch
  * feat-add-grain-for-all-fqdns.patch
  * fix-bsc-1065792.patch
  * fix-decrease-loglevel-when-unable-to-resolve-addr.patch
  * fix-for-ec2-rate-limit-failures.patch

OBS-URL: https://build.opensuse.org/request/show/626472
OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=127
2018-07-30 11:52:13 +00:00

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