Skip to content

Commit

Permalink
Porting PR saltstack#50005 to 2019.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgreenaway committed Sep 19, 2019
1 parent 8d4d5ea commit d253b90
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 0 deletions.
137 changes: 137 additions & 0 deletions salt/engines/script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
'''
Send events based on a script's stdout
Example Config
.. code-block:: yaml
engines:
- script:
cmd: /some/script.py -a 1 -b 2
output: json
interval: 5
Script engine configs:
cmd: Script or command to execute
output: Any available saltstack deserializer
interval: How often in seconds to execute the command
'''

from __future__ import absolute_import, print_function
import logging
import shlex
import time
import subprocess

# import salt libs
import salt.utils.event
import salt.utils.process
import salt.loader
from salt.exceptions import CommandExecutionError

from salt.ext import six


log = logging.getLogger(__name__)


def _read_stdout(proc):
'''
Generator that returns stdout
'''
for line in iter(proc.stdout.readline, ""):
yield line


def _get_serializer(output):
'''
Helper to return known serializer based on
pass output argument
'''
serializers = salt.loader.serializers(__opts__)
try:
return getattr(serializers, output)
except AttributeError:
raise CommandExecutionError("Unknown serializer '%s' found for output option", output)


def start(cmd, output='json', interval=1):
'''
Parse stdout of a command and generate an event
The script engine will scrap stdout of the
given script and generate an event based on the
presence of the 'tag' key and it's value.
If there is a data obj available, that will also
be fired along with the tag.
Example:
Given the following json output from a script:
{ "tag" : "lots/of/tacos",
"data" : { "toppings" : "cilantro" }
}
This will fire the event 'lots/of/tacos'
on the event bus with the data obj as is.
:param cmd: The command to execute
:param output: How to deserialize stdout of the script
:param interval: How often to execute the script.
'''
try:
cmd = shlex.split(cmd)
except AttributeError:
cmd = shlex.split(six.text_type(cmd))
log.debug("script engine using command %s", cmd)

serializer = _get_serializer(output)

if __opts__.get('__role') == 'master':
fire_master = salt.utils.event.get_master_event(
__opts__,
__opts__['sock_dir']).fire_event
else:
fire_master = __salt__['event.send']

while True:

try:
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)

log.debug("Starting script with pid %d", proc.pid)

for raw_event in _read_stdout(proc):
log.debug(raw_event)

event = serializer.deserialize(raw_event)
tag = event.get('tag', None)
data = event.get('data', {})

if data and 'id' not in data:
data['id'] = __opts__['id']

if tag:
log.info("script engine firing event with tag %s", tag)
fire_master(tag=tag, data=data)

log.debug("Closing script with pid %d", proc.pid)
proc.stdout.close()
rc = proc.wait()
if rc:
raise subprocess.CalledProcessError(rc, cmd)

except subprocess.CalledProcessError as e:
log.error(e)
finally:
if proc.poll is None:
proc.terminate()

time.sleep(interval)
54 changes: 54 additions & 0 deletions tests/unit/engines/test_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
'''
unit tests for the script 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,
patch)

# Import Salt Libs
import salt.config
import salt.engines.script as script
from salt.exceptions import CommandExecutionError


@skipIf(NO_MOCK, NO_MOCK_REASON)
class EngineScriptTestCase(TestCase, LoaderModuleMockMixin):
'''
Test cases for salt.engine.script
'''

def setup_loader_modules(self):

opts = salt.config.DEFAULT_MASTER_OPTS
return {
script: {
'__opts__': opts
}
}

def test__get_serializer(self):
'''
Test known serializer is returned or exception is raised
if unknown serializer
'''
for serializers in ('json', 'yaml', 'msgpack'):
self.assertTrue(script._get_serializer(serializers))

with self.assertRaises(CommandExecutionError):
script._get_serializer('bad')

def test__read_stdout(self):
'''
Test we can yield stdout
'''
with patch('subprocess.Popen') as popen_mock:
popen_mock.stdout.readline.return_value = 'test'
self.assertEqual(next(script._read_stdout(popen_mock)), 'test')

0 comments on commit d253b90

Please sign in to comment.