saltstack/salt

View on GitHub
salt/beacons/watchdog.py

Summary

Maintainability
A
0 mins
Test Coverage
# -*- coding: utf-8 -*-
'''
watchdog beacon

.. versionadded:: 2019.2.0

Watch files and translate the changes into salt events

:depends:   - watchdog Python module >= 0.8.3

'''
# Import Python libs
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import collections
import logging

from salt.ext.six.moves import map

# Import third party libs
try:
    from watchdog.observers import Observer
    from watchdog.events import FileSystemEventHandler
    HAS_WATCHDOG = True
except ImportError:
    HAS_WATCHDOG = False

    class FileSystemEventHandler(object):
        """ A dummy class to make the import work """
        def __init__(self):
            pass

__virtualname__ = 'watchdog'

log = logging.getLogger(__name__)

DEFAULT_MASK = [
    'create',
    'delete',
    'modify',
    'move',
]


class Handler(FileSystemEventHandler):
    def __init__(self, queue, masks=None):
        super(Handler, self).__init__()
        self.masks = masks or DEFAULT_MASK
        self.queue = queue

    def on_created(self, event):
        self._append_if_mask(event, 'create')

    def on_modified(self, event):
        self._append_if_mask(event, 'modify')

    def on_deleted(self, event):
        self._append_if_mask(event, 'delete')

    def on_moved(self, event):
        self._append_if_mask(event, 'move')

    def _append_if_mask(self, event, mask):
        logging.debug(event)

        self._append_path_if_mask(event, mask)

    def _append_path_if_mask(self, event, mask):
        if mask in self.masks:
            self.queue.append(event)


def __virtual__():
    if HAS_WATCHDOG:
        return __virtualname__
    return False


def _get_queue(config):
    '''
    Check the context for the notifier and construct it if not present
    '''

    if 'watchdog.observer' not in __context__:
        queue = collections.deque()
        observer = Observer()
        for path in config.get('directories', {}):
            path_params = config.get('directories').get(path)
            masks = path_params.get('mask', DEFAULT_MASK)
            event_handler = Handler(queue, masks)
            observer.schedule(event_handler, path)

        observer.start()

        __context__['watchdog.observer'] = observer
        __context__['watchdog.queue'] = queue

    return __context__['watchdog.queue']


class ValidationError(Exception):
    pass


def validate(config):
    '''
    Validate the beacon configuration
    '''

    try:
        _validate(config)
        return True, 'Valid beacon configuration'
    except ValidationError as error:
        return False, str(error)


def _validate(config):
    if not isinstance(config, list):
        raise ValidationError(
            'Configuration for watchdog beacon must be a list.')

    _config = {}
    for part in config:
        _config.update(part)

    if 'directories' not in _config:
        raise ValidationError(
            'Configuration for watchdog beacon must include directories.')

    if not isinstance(_config['directories'], dict):
        raise ValidationError(
            'Configuration for watchdog beacon directories must be a '
            'dictionary.')

    for path in _config['directories']:
        _validate_path(_config['directories'][path])


def _validate_path(path_config):
    if not isinstance(path_config, dict):
        raise ValidationError(
            'Configuration for watchdog beacon directory path must be '
            'a dictionary.')

    if 'mask' in path_config:
        _validate_mask(path_config['mask'])


def _validate_mask(mask_config):
    valid_mask = [
        'create',
        'modify',
        'delete',
        'move',
    ]

    if not isinstance(mask_config, list):
        raise ValidationError(
            'Configuration for watchdog beacon mask must be list.')

    if any(mask not in valid_mask for mask in mask_config):
        raise ValidationError(
            'Configuration for watchdog beacon contains invalid mask')


def to_salt_event(event):
    return {
        'tag': __virtualname__,
        'path': event.src_path,
        'change': event.event_type,
    }


def beacon(config):
    '''
    Watch the configured directories

    Example Config

    .. code-block:: yaml

        beacons:
          watchdog:
            - directories:
                /path/to/dir:
                  mask:
                    - create
                    - modify
                    - delete
                    - move

    The mask list can contain the following events (the default mask is create,
    modify delete, and move):
    * create  - File or directory is created in watched directory
    * modify  - The watched directory is modified
    * delete  - File or directory is deleted from watched directory
    * move    - File or directory is moved or renamed in the watched directory
    '''

    _config = {}
    list(map(_config.update, config))

    queue = _get_queue(_config)

    ret = []
    while queue:
        ret.append(to_salt_event(queue.popleft()))

    return ret


def close(config):
    observer = __context__.pop('watchdog.observer', None)

    if observer:
        observer.stop()