salt/beacons/__init__.py
# -*- coding: utf-8 -*-
'''
This package contains the loader modules for the salt streams system
'''
# Import Python libs
from __future__ import absolute_import
import logging
import copy
import re
# Import Salt libs
import salt.loader
import salt.utils.event
import salt.utils.minion
from salt.ext.six.moves import map
from salt.exceptions import CommandExecutionError
log = logging.getLogger(__name__)
class Beacon(object):
'''
This class is used to evaluate and execute on the beacon system
'''
def __init__(self, opts, functions):
self.opts = opts
self.functions = functions
self.beacons = salt.loader.beacons(opts, functions)
self.interval_map = dict()
def process(self, config, grains):
'''
Process the configured beacons
The config must be a list and looks like this in yaml
.. code_block:: yaml
beacons:
inotify:
- files:
- /etc/fstab: {}
- /var/cache/foo: {}
'''
ret = []
b_config = copy.deepcopy(config)
if 'enabled' in b_config and not b_config['enabled']:
return
for mod in config:
if mod == 'enabled':
continue
# Convert beacons that are lists to a dict to make processing easier
current_beacon_config = None
if isinstance(config[mod], list):
current_beacon_config = {}
list(map(current_beacon_config.update, config[mod]))
elif isinstance(config[mod], dict):
current_beacon_config = config[mod]
if 'enabled' in current_beacon_config:
if not current_beacon_config['enabled']:
log.trace('Beacon %s disabled', mod)
continue
else:
# remove 'enabled' item before processing the beacon
if isinstance(config[mod], dict):
del config[mod]['enabled']
else:
self._remove_list_item(config[mod], 'enabled')
log.trace('Beacon processing: %s', mod)
beacon_name = None
if self._determine_beacon_config(current_beacon_config, 'beacon_module'):
beacon_name = current_beacon_config['beacon_module']
else:
beacon_name = mod
fun_str = '{0}.beacon'.format(beacon_name)
validate_str = '{0}.validate'.format(beacon_name)
if fun_str in self.beacons:
runonce = self._determine_beacon_config(current_beacon_config, 'run_once')
interval = self._determine_beacon_config(current_beacon_config, 'interval')
if interval:
b_config = self._trim_config(b_config, mod, 'interval')
if not self._process_interval(mod, interval):
log.trace('Skipping beacon %s. Interval not reached.', mod)
continue
if self._determine_beacon_config(current_beacon_config, 'disable_during_state_run'):
log.trace('Evaluting if beacon %s should be skipped due to a state run.', mod)
b_config = self._trim_config(b_config, mod, 'disable_during_state_run')
is_running = False
running_jobs = salt.utils.minion.running(self.opts)
for job in running_jobs:
if re.match('state.*', job['fun']):
is_running = True
if is_running:
close_str = '{0}.close'.format(beacon_name)
if close_str in self.beacons:
log.info('Closing beacon %s. State run in progress.', mod)
self.beacons[close_str](b_config[mod])
else:
log.info('Skipping beacon %s. State run in progress.', mod)
continue
# Update __grains__ on the beacon
self.beacons[fun_str].__globals__['__grains__'] = grains
# Run the validate function if it's available,
# otherwise there is a warning about it being missing
if validate_str in self.beacons:
valid, vcomment = self.beacons[validate_str](b_config[mod])
if not valid:
log.info('Beacon %s configuration invalid, '
'not running.\n%s', mod, vcomment)
continue
# Fire the beacon!
raw = self.beacons[fun_str](b_config[mod])
for data in raw:
tag = 'salt/beacon/{0}/{1}/'.format(self.opts['id'], mod)
if 'tag' in data:
tag += data.pop('tag')
if 'id' not in data:
data['id'] = self.opts['id']
ret.append({'tag': tag,
'data': data,
'beacon_name': beacon_name})
if runonce:
self.disable_beacon(mod)
else:
log.warning('Unable to process beacon %s', mod)
return ret
def _trim_config(self, b_config, mod, key):
'''
Take a beacon configuration and strip out the interval bits
'''
if isinstance(b_config[mod], list):
self._remove_list_item(b_config[mod], key)
elif isinstance(b_config[mod], dict):
b_config[mod].pop(key)
return b_config
def _determine_beacon_config(self, current_beacon_config, key):
'''
Process a beacon configuration to determine its interval
'''
interval = False
if isinstance(current_beacon_config, dict):
interval = current_beacon_config.get(key, False)
return interval
def _process_interval(self, mod, interval):
'''
Process beacons with intervals
Return True if a beacon should be run on this loop
'''
log.trace('Processing interval %s for beacon mod %s', interval, mod)
loop_interval = self.opts['loop_interval']
if mod in self.interval_map:
log.trace('Processing interval in map')
counter = self.interval_map[mod]
log.trace('Interval counter: %s', counter)
if counter * loop_interval >= interval:
self.interval_map[mod] = 1
return True
else:
self.interval_map[mod] += 1
else:
log.trace('Interval process inserting mod: %s', mod)
self.interval_map[mod] = 1
return False
def _get_index(self, beacon_config, label):
'''
Return the index of a labeled config item in the beacon config, -1 if the index is not found
'''
indexes = [index for index, item in enumerate(beacon_config) if label in item]
if not indexes:
return -1
else:
return indexes[0]
def _remove_list_item(self, beacon_config, label):
'''
Remove an item from a beacon config list
'''
index = self._get_index(beacon_config, label)
del beacon_config[index]
def _update_enabled(self, name, enabled_value):
'''
Update whether an individual beacon is enabled
'''
if isinstance(self.opts['beacons'][name], dict):
# Backwards compatibility
self.opts['beacons'][name]['enabled'] = enabled_value
else:
enabled_index = self._get_index(self.opts['beacons'][name], 'enabled')
if enabled_index >= 0:
self.opts['beacons'][name][enabled_index]['enabled'] = enabled_value
else:
self.opts['beacons'][name].append({'enabled': enabled_value})
def _get_beacons(self,
include_opts=True,
include_pillar=True):
'''
Return the beacons data structure
'''
beacons = {}
if include_pillar:
pillar_beacons = self.opts.get('pillar', {}).get('beacons', {})
if not isinstance(pillar_beacons, dict):
raise ValueError('Beacons must be of type dict.')
beacons.update(pillar_beacons)
if include_opts:
opts_beacons = self.opts.get('beacons', {})
if not isinstance(opts_beacons, dict):
raise ValueError('Beacons must be of type dict.')
beacons.update(opts_beacons)
return beacons
def list_beacons(self,
include_pillar=True,
include_opts=True):
'''
List the beacon items
include_pillar: Whether to include beacons that are
configured in pillar, default is True.
include_opts: Whether to include beacons that are
configured in opts, default is True.
'''
beacons = self._get_beacons(include_pillar, include_opts)
# Fire the complete event back along with the list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': True, 'beacons': beacons},
tag='/salt/minion/minion_beacons_list_complete')
return True
def list_available_beacons(self):
'''
List the available beacons
'''
_beacons = ['{0}'.format(_beacon.replace('.beacon', ''))
for _beacon in self.beacons if '.beacon' in _beacon]
# Fire the complete event back along with the list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': True, 'beacons': _beacons},
tag='/salt/minion/minion_beacons_list_available_complete')
return True
def validate_beacon(self, name, beacon_data):
'''
Return available beacon functions
'''
validate_str = '{}.validate'.format(name)
# Run the validate function if it's available,
# otherwise there is a warning about it being missing
if validate_str in self.beacons:
if 'enabled' in beacon_data:
del beacon_data['enabled']
valid, vcomment = self.beacons[validate_str](beacon_data)
else:
vcomment = 'Beacon {0} does not have a validate' \
' function, skipping validation.'.format(name)
valid = True
# Fire the complete event back along with the list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': True,
'vcomment': vcomment,
'valid': valid},
tag='/salt/minion/minion_beacon_validation_complete')
return True
def add_beacon(self, name, beacon_data):
'''
Add a beacon item
'''
data = {}
data[name] = beacon_data
if name in self._get_beacons(include_opts=False):
comment = 'Cannot update beacon item {0}, ' \
'because it is configured in pillar.'.format(name)
complete = False
else:
if name in self.opts['beacons']:
comment = 'Updating settings for beacon ' \
'item: {0}'.format(name)
else:
comment = 'Added new beacon item: {0}'.format(name)
complete = True
self.opts['beacons'].update(data)
# Fire the complete event back along with updated list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': complete, 'comment': comment,
'beacons': self.opts['beacons']},
tag='/salt/minion/minion_beacon_add_complete')
return True
def modify_beacon(self, name, beacon_data):
'''
Modify a beacon item
'''
data = {}
data[name] = beacon_data
if name in self._get_beacons(include_opts=False):
comment = 'Cannot modify beacon item {0}, ' \
'it is configured in pillar.'.format(name)
complete = False
else:
comment = 'Updating settings for beacon ' \
'item: {0}'.format(name)
complete = True
self.opts['beacons'].update(data)
# Fire the complete event back along with updated list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': complete, 'comment': comment,
'beacons': self.opts['beacons']},
tag='/salt/minion/minion_beacon_modify_complete')
return True
def delete_beacon(self, name):
'''
Delete a beacon item
'''
if name in self._get_beacons(include_opts=False):
comment = 'Cannot delete beacon item {0}, ' \
'it is configured in pillar.'.format(name)
complete = False
else:
if name in self.opts['beacons']:
del self.opts['beacons'][name]
comment = 'Deleting beacon item: {0}'.format(name)
else:
comment = 'Beacon item {0} not found.'.format(name)
complete = True
# Fire the complete event back along with updated list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': complete, 'comment': comment,
'beacons': self.opts['beacons']},
tag='/salt/minion/minion_beacon_delete_complete')
return True
def enable_beacons(self):
'''
Enable beacons
'''
self.opts['beacons']['enabled'] = True
# Fire the complete event back along with updated list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': True, 'beacons': self.opts['beacons']},
tag='/salt/minion/minion_beacons_enabled_complete')
return True
def disable_beacons(self):
'''
Enable beacons
'''
self.opts['beacons']['enabled'] = False
# Fire the complete event back along with updated list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': True, 'beacons': self.opts['beacons']},
tag='/salt/minion/minion_beacons_disabled_complete')
return True
def enable_beacon(self, name):
'''
Enable a beacon
'''
if name in self._get_beacons(include_opts=False):
comment = 'Cannot enable beacon item {0}, ' \
'it is configured in pillar.'.format(name)
complete = False
else:
self._update_enabled(name, True)
comment = 'Enabling beacon item {0}'.format(name)
complete = True
# Fire the complete event back along with updated list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': complete, 'comment': comment,
'beacons': self.opts['beacons']},
tag='/salt/minion/minion_beacon_enabled_complete')
return True
def disable_beacon(self, name):
'''
Disable a beacon
'''
if name in self._get_beacons(include_opts=False):
comment = 'Cannot disable beacon item {0}, ' \
'it is configured in pillar.'.format(name)
complete = False
else:
self._update_enabled(name, False)
comment = 'Disabling beacon item {0}'.format(name)
complete = True
# Fire the complete event back along with updated list of beacons
with salt.utils.event.get_event('minion', opts=self.opts) as evt:
evt.fire_event({'complete': complete, 'comment': comment,
'beacons': self.opts['beacons']},
tag='/salt/minion/minion_beacon_disabled_complete')
return True
def reset(self):
'''
Reset the beacons to defaults
'''
self.opts['beacons'] = {}
comment = 'Beacon Reset'
complete = True
# Fire the complete event back along with updated list of beacons
evt = salt.utils.event.get_event('minion', opts=self.opts)
evt.fire_event({'complete': complete, 'comment': comment,
'beacons': self.opts['beacons']},
tag='/salt/minion/minion_beacon_reset_complete')
return True