forked from saltstack/salt
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Porting PR saltstack#50005 to 2019.2.1
- Loading branch information
1 parent
8d4d5ea
commit d253b90
Showing
2 changed files
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |