<commit_before># -*- coding: utf-8 -*-

'''
An engine that listens for libvirt events and resends them to the salt event bus.

Example configuration:

.. code-block:: yaml

    engines:
        - libvirt_events:
            uri: lxc:///
            tag_prefix: libvirt
            filters:
                - domain/lifecycle
                - domain/reboot
                - pool

The default URI is ``qemu:///system`` and the default tag prefix is
``salt/engines/libvirt_events``.

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.

: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(tag, all_data)
    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()
        },
        '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<commit_msg>Fix libvirt_events engine running on master<commit_after># -*- coding: utf-8 -*-

'''
An engine that listens for libvirt events and resends them to the salt event bus.

Example configuration:

.. code-block:: yaml

    engines:
        - libvirt_events:
            uri: lxc:///
            tag_prefix: libvirt
            filters:
                - domain/lifecycle
                - domain/reboot
                - pool

The default URI is ``qemu:///system`` and the default tag prefix is
``salt/engines/libvirt_events``.

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()
        },
        '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
{'fraction_non_alphanumeric': 0.11584315803417672, 'fraction_numerical': 0.004305205987548019, 'mean_word_length': 2.71165191740413, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1530998', 'n_tokens_mistral': 4605, 'n_tokens_neox': 4458, 'n_words': 1410}