Salt Stack's Reactor system provides the ability to trigger actions in response to events. This tutorial sets up a vagrant box and gives you a brief overview on reactors.
-
Download & install Vagrant
2.0.0
and VirtualBox5.0.0
or above. -
Ensure vagrant-salt plugin is not installed as it may cause issues.
vagrant plugin uninstall vagrant-salt
-
Create a workspace and clone
salt-reactors-demo
repository.mkdir ~/saltspace && cd ~/saltspace git clone [email protected]:rehmanz/salt-reactors-demo.git export WORKSPACE=~/saltspace/salt-reactors-demo/
-
Use Vagrant to create and provision Salt master and two minions.
cd ${WORKSPACE} vagrant up --provider virtualbox
-
Validate provisioning via the ping test from Salt master
vagrant ssh master sudo salt '*' test.ping
You should see the following output.
minion2: True minion1: True
Salt Stack has internal and external reactors.
Internal reactors are automatically triggered by Salt. Let's explore the structure.
-
Login into Salt master to explore.
cd ${WORKSPACE} vagrant ssh master
-
Salt reactors allow you to define specific event tags and associate them with one or more reactions. This can be seen in Salt master
/etc/salt/master
config file underreactor
section.reactor: - 'salt/demo/minion1/full_logs': - salt://reactor/cleanup_logs.sls - 'salt/demo/minion1/restart/ssh': - salt://reactor/restart_ssh_service.sls
The above example shows
salt/demo/minion/full_logs
tag, which triggers thecleanup_logs.sls
formula.The
cleanup_logs.sls
formula simply removes the log file.cleanup log files: local.cmd.run: - tgt: 'minion1' - arg: - rm /tmp/log.txt
Let's see the internal reactors in action.
-
Start monitoring events on Salt master
sudo salt-run state.event pretty=True
-
In a new shell, login to
minion1
cd ${WORKSPACE} vagrant ssh minion1
-
Create a log file on
minion1
and manually trigger the event.echo "This is a test" > /tmp/log.txt sudo salt-call event.send 'salt/demo/minion1/full_logs'
You will notice
/tmp/log.txt
file was removed due tosalt/demo/minion/full_logs
event sent to Salt master byminion1
. -
You can go back to the Salt master window and view the event output and reaction.
# This is the event triggered by minion1 salt/demo/minion1/full_logs { "_stamp": "2017-09-24T23:30:37.659325", "cmd": "_minion_event", "data": { "__pub_fun": "event.send", "__pub_jid": "20170924233038250552", "__pub_pid": 16518, "__pub_tgt": "salt-call" }, "id": "minion1", "tag": "salt/demo/minion1/full_logs" } # Salt Reactor ran this formula due to the event above salt/job/20170924233037754615/ret/minion1 { "_stamp": "2017-09-24T23:30:37.794277", "cmd": "_return", "fun": "cmd.run", "fun_args": [ "rm /tmp/log.txt" ], "id": "minion1", "jid": "20170924233037754615", "retcode": 0, "return": "", "success": true }
External reactors are custom scripts (i.e. Bash, Python, Golang) that listen for specific tags from Salt event bus.
-
Let's view an external reactor by logging into the Salt master first.
cd ${WORKSPACE} vagrant ssh master
-
ui_reactor.py
script in/srv/salt/formulas/base/reactor
directory provides a basic structure.#!/usr/bin/env python import os import time import logging import argparse from Queue import Queue from threading import Thread from salt.utils.event import LocalClientEvent LOGGER = logging.getLogger() MAX_TIMEOUT_VALUE=60*5 def __parse_record(event): payload = event.get('data', {}) return payload.get('record', {}) def get_event_payload(tag_id): for event in client.iter_events(tag=tag_id): payload = __parse_record(event) return payload def process_events(): while True: if not q.empty(): LOGGER.debug("#TODO: Implement correlation logic") LOGGER.debug("Waiting 20 seconds for all events to be registered") time.sleep(20) LOGGER.debug("Fetching all events from the event queue") while not q.empty(): msg_payload = q.get() LOGGER.debug("msg_payload=%s" %msg_payload) timeout = time.time() + MAX_TIMEOUT_VALUE while time.time() < timeout: try: LOGGER.debug("#TODO: Implement complex reaction") except Exception as e: LOGGER.error("Failed to complete the reaction: %s" %(e)) # Send failure notification break LOGGER.debug("#TODO: Implement validation & notification") if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser() """ Required Parameters Definition """ parser.add_argument("environment", help="demo") args = parser.parse_args() target_env = args.environment tag_id = 'salt/%s/ui/slave/dead' %(target_env) client = LocalClientEvent("/var/run/salt/master") # Setup file handler fh= logging.FileHandler("/var/log/%s.log" %target_env) fh.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') fh.setFormatter(formatter) LOGGER.addHandler(fh) LOGGER.debug("##") LOGGER.debug("# Setting up UI Reactor for %s" %target_env) LOGGER.debug("##") q = Queue() worker = Thread(target=process_events) worker.setDaemon(True) worker.start() while True: event_payload = get_event_payload(tag_id) q.put(event_payload) LOGGER.info("Received an event=%s" %event_payload)
-
Now start the reactor process and monitor the log file for activity.
sudo /srv/salt/formulas/base/reactor/ui_reactor.py demo & sudo tail -f /var/log/demo.log
-
In new shells, login to
minion1
andminion2
.cd ${WORKSPACE} vagrant ssh minion1
cd ${WORKSPACE} vagrant ssh minion2
-
Now trigger the dead event from both minions while monitoring log file activity on Salt Master.
On
minion1
execute.sudo salt-call event.send 'salt/demo/ui/slave/dead' '{ 'record': { 'environment' : 'demo', 'node_type' : 'ui', 'node_id' : 'minion1' } }'
On
minion2
execute.sudo salt-call event.send 'salt/demo/ui/slave/dead' '{ 'record': { 'environment' : 'demo', 'node_type' : 'ui', 'node_id' : 'minion2' } }'
You will notice our reactor waited for both the events to be registered, before invoking the recovery logic. To learn more about reactors, Salt Stack Reactor System is a great resource!