Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

script engine #50005

Merged
merged 9 commits into from
Oct 17, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')