Skip to content

Instantly share code, notes, and snippets.

@pcn
Last active August 29, 2015 14:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pcn/9f2717c993c0d7e4d862 to your computer and use it in GitHub Desktop.
Save pcn/9f2717c993c0d7e4d862 to your computer and use it in GitHub Desktop.
A saltstack "tester" that will use controlled input to create expected (?) output
#!/usr/bin/env python
'''
This module contains the function calls to render state files into yaml.
The intent is that your repo of states will have a default grains.json. In each directory in
your states, you will will have a test/ directory in each state directory that will contain a
pillars.json and an <statename>.json one per sls file in the directory.
The test consists of calling the state.show_sls on the state file, with the provided grains
and pillars.
The test will succeed when all of the keys and values in the <statename>.json are contained
in the output of the test.
In the first iteration of this, if keys+values exist in the output of the test that do not
exist in the <statename>.json the test should succeed.
At first, the only goal will be getting output. Once output is obtained the comparison can be
worked on.
'''
from __future__ import absolute_import
# Import python libs
from __future__ import print_function
import os
import sys
import traceback
import logging
import datetime
import traceback
import multiprocessing
import json
import threading
import time
from random import randint
# These imports would normally come from salt/cli/call.py
from salt.utils import parsers
from salt.utils.verify import verify_env, verify_files
from salt.config import _expand_glob_path
import salt.cli.call
import salt.cli.caller
# Import salt libs
from salt.exceptions import SaltSystemExit, SaltClientError, SaltReqTimeoutError
import salt.defaults.exitcodes # pylint: disable=unused-import
# Custom exceptions
from salt.exceptions import (
SaltClientError,
CommandNotFoundError,
CommandExecutionError,
SaltInvocationError,
)
log = logging.getLogger(__name__)
class ConfObj(object):
def __init__(self, conf_dict):
for k,v in conf_dict.items():
setattr(self, k, v)
def __setatrr__(self, name, val):
setattr(self, name, val)
options = ConfObj({'output_file_append': False,
'saltfile': None,
'state_output': 'full',
'force_color': False,
'skip_grains': False,
'config_dir': '/tmp/salt',
'id': 'foo',
'output_indent': None,
'log_level': 'info',
'output_file': None,
'module_dirs': [],
'master': 'salt',
'log_level_logfile': None,
'local': True,
'metadata': False,
'return': '',
'no_color': False,
'pillar_root': '.',
'hard_crash': False,
'file_root': '../../',
'auth_timeout': 60,
'refresh_grains_cache': False,
'doc': False,
'grains_run': False,
'versions_report': None,
'retcode_passthrough': False,
'output': None,
'log_file': '/tmp/salt/log'})
config = {
'output_file_append': False,
'ioflo_realtime': True,
'master_alive_interval': 0,
'recon_default': 1000,
'master_port': '4506',
'whitelist_modules': [],
'ioflo_console_logdir': '',
'utils_dirs': ['/tmp/salt/cache/extmods/utils'],
'states_dirs': [],
'fileserver_backend': ['roots'],
'outputter_dirs': [],
'sls_list': [],
'module_dirs': [],
'extension_modules': '/tmp/salt/cache/extmods',
'state_auto_order': True,
'acceptance_wait_time': 10,
'__role': 'minion',
'disable_modules': [],
'backup_mode': '',
'recon_randomize': True,
'return': '',
'file_ignore_glob': None,
'auto_accept': True,
'cache_jobs': False,
'state_verbose': True,
'verify_master_pubkey_sign': False,
'password': None,
'startup_states': '',
'auth_timeout': 60,
'always_verify_signature': False,
'tcp_pull_port': 4511,
'gitfs_pubkey': '',
'fileserver_ignoresymlinks': False,
'retry_dns': 30,
'file_ignore_regex': None,
'output': 'json',
'master_shuffle': False,
'metadata': False,
'multiprocessing': True,
'file_roots': {'base': ['/srv/salt']},
'root_dir': '/',
'log_granular_levels': {},
'returner_dirs': [],
'gitfs_privkey': '',
'tcp_keepalive': True,
# 'arg': ['chatbot',
# 'grains={ "target_pillars" : ["chatbot"] }'],
'log_datefmt_logfile': '%Y-%m-%d %H:%M:%S',
'config_dir': '/tmp/salt',
'random_reauth_delay': 10,
'autosign_timeout': 120,
'gitfs_base': 'master',
'render_dirs': [],
'gitfs_user': '',
'fileserver_limit_traversal': False,
'tcp_keepalive_intvl': -1,
'conf_file': '/tmp/salt/minion',
'pillar_root': '.',
'top_file': '',
'zmq_monitor': False,
'file_recv_max_size': 100,
'pidfile': '/tmp/salt/salt-minion.pid',
'range_server': 'range:80',
'raet_mutable': False,
'grains_dirs': [],
'pillar_roots': {'base': ['/srv/pillar']},
'schedule': {},
'raet_main': False,
'fun': 'state.show_sls',
'cachedir': '/tmp/salt/cache',
'interface': '0.0.0.0',
'update_restart_services': [],
'recon_max': 10000,
'default_include': 'minion.d/*.conf',
'hard_crash': False,
'rejected_retry': False,
'file_root': '../../',
'state_events': False,
'environment': None,
'win_repo_cachefile': 'salt://win/repo/winrepo.p',
'ipc_mode': 'ipc',
'keysize': 2048,
'master_sign_key_name': 'master_sign',
'cython_enable': False,
'raet_port': 4510,
'ext_job_cache': '',
'hash_type': 'md5',
'state_output': 'full',
'force_color': False,
'modules_max_memory': -1,
'renderer': 'yaml_jinja',
'state_top': 'top.sls',
'gitfs_env_whitelist': [],
'auth_tries': 7,
'gitfs_insecure_auth': False,
'mine_interval': 60,
'grains_cache': False,
'file_recv': False,
'log_level_logfile': None,
'ipv6': False,
'master': 'salt',
'sudo_user': '',
'no_color': False,
'username': None,
'master_finger': '',
'failhard': False,
'tcp_keepalive_idle': 300,
'gitfs_passphrase': '',
'fileserver_followsymlinks': True,
'verify_env': True,
'ioflo_period': 0.1,
'ping_interval': 0,
'grains': {},
'local': True,
'tcp_keepalive_cnt': -1,
'raet_alt_port': 4511,
'skip_grains': False,
'retcode_passthrough': True,
'doc': False,
'state_aggregate': False,
'syndic_log_file': '/var/log/salt/syndic',
'update_url': False,
'grains_refresh_every': 0,
'transport': 'zeromq',
'providers': {},
'autoload_dynamic_modules': True,
'file_buffer_size': 262144,
'log_fmt_console': '[%(levelname)-8s] %(message)s',
'random_master': False,
'log_datefmt': '%H:%M:%S',
'grains_cache_expiration': 300,
'minion_floscript': '/Users/peter.norton/.pyenv/versions/salt/lib/python2.7/site-packages/salt/daemons/flo/minion.flo',
'id': 'foo',
'syndic_pidfile': '/var/run/salt-syndic.pid',
'loop_interval': 1,
'log_level': 'info',
'gitfs_env_blacklist': [],
'auth_safemode': False,
'clean_dynamic_modules': True,
'disable_returners': [],
'cache_sreqs': True,
'minion_id_caching': True,
'gitfs_root': '',
'test': False,
'gitfs_password': '',
'caller_floscript': '/Users/peter.norton/.pyenv/versions/salt/lib/python2.7/site-packages/salt/daemons/flo/caller.flo',
'caller': True,
'syndic_finger': '',
'raet_clear_remotes': True,
'file_client': 'local',
'user': 'root',
'use_master_when_local': False,
'acceptance_wait_time_max': 0,
'open_mode': False,
'permissive_pki_access': False,
'cmd_safe': True,
'zmq_filtering': False,
'refresh_grains_cache': False,
'selected_output_option': 'output_indent',
'master_type': 'standard',
'pki_dir': '/tmp/salt/pki/minion',
'grains_run': False,
'max_event_size': 1048576,
'ioflo_verbose': 0,
'sock_dir': '/var/run/salt/minion',
'tcp_pub_port': 4510,
'log_fmt_logfile': '%(asctime)s,%(msecs)03.0f [%(name)-17s][%(levelname)-8s][%(process)d] %(message)s',
'log_file': '/tmp/salt/log',
'gitfs_remotes': [],
'gitfs_mountpoint': ''}
def _handle_interrupt(exc, original_exc, hardfail=False, trace=''):
'''
if hardfailing:
If we got the original stacktrace, log it
If all cases, raise the original exception
but this is logically part the initial
stack.
else just let salt exit gracefully
'''
if hardfail:
if trace:
log.error(trace)
raise original_exc
else:
raise exc
def salt_call():
'''
Directly call state.show_sls via the same mechanism as salt-call
'''
if '' in sys.path:
sys.path.remove('')
client = None
try:
client = SaltCall()
client.run()
except KeyboardInterrupt as err:
trace = traceback.format_exc()
try:
hardcrash = client.options.hard_crash
except (AttributeError, KeyError):
hardcrash = False
_handle_interrupt(
SystemExit('\nExiting gracefully on Ctrl-c'),
err,
hardcrash, trace=trace)
class SaltCall(parsers.SaltCallOptionParser):
'''
Used to locally execute a salt command, specifically state.show_sls
'''
def run(self):
'''
Execute the salt call with provided configuratio
'''
global config
global options
if options.file_root:
# check if the argument is pointing to a file on disk
file_root = os.path.abspath(options.file_root)
config['file_roots'] = {'base': _expand_glob_path([file_root])}
config['arg'] = sys.argv[1:]
caller = LocalCaller(config)
caller.run()
class LocalCaller(object):
'''
Object to wrap the calling of local salt modules for testing
Stripping down bits of the ZeroMQCaller
'''
def __init__(self, config):
'''
Pass in the command line options
'''
self.config = config
self.serial = salt.payload.Serial(self.config)
# Handle this here so other deeper code which might
# be imported as part of the salt api doesn't do a
# nasty sys.exit() and tick off our developer users
try:
self.minion = salt.minion.SMinion(config)
except SaltClientError as exc:
raise SystemExit(str(exc))
def call(self):
'''
Call the module
'''
ret = {}
fun = self.config['fun']
ret['jid'] = '{0:%Y%m%d%H%M%S%f}'.format(datetime.datetime.now())
proc_fn = os.path.join(
salt.minion.get_proc_dir(self.config['cachedir']),
ret['jid']
)
if fun not in self.minion.functions:
sys.stderr.write(self.minion.functions.missing_fun_string(fun))
mod_name = fun.split('.')[0]
if mod_name in self.minion.function_errors:
sys.stderr.write(' Possible reasons: {0}\n'.format(self.minion.function_errors[mod_name]))
else:
sys.stderr.write('\n')
sys.exit(-1)
sdata = {
'fun': fun,
'pid': os.getpid(),
'jid': ret['jid'],
'tgt': 'salt-call'}
args, kwargs = salt.minion.load_args_and_kwargs(
self.minion.functions[fun],
salt.utils.args.parse_input(self.config['arg']),
data=sdata)
self.config['grains'] = json.load(open('grains.json'))
kwargs['pillar'] = json.load(open('pillar.json'))
func = self.minion.functions[fun]
try:
ret['return'] = func(*args, **kwargs)
except TypeError as exc:
trace = traceback.format_exc()
raise ValueError, 'Passed invalid arguments: {0}\n'.format(exc)
try:
ret['retcode'] = sys.modules[
func.__module__].__context__.get('retcode', 0)
except AttributeError:
ret['retcode'] = 1
try:
os.remove(proc_fn)
except (IOError, OSError):
pass
if hasattr(self.minion.functions[fun], '__outputter__'):
oput = self.minion.functions[fun].__outputter__
if isinstance(oput, string_types):
ret['out'] = oput
return ret
def run(self):
'''
Execute the salt call logic
'''
ret = self.call()
out = ret.get('out', 'nested')
if self.config['metadata']:
print_ret = ret
out = 'nested'
else:
print_ret = ret.get('return', {})
salt.output.display_output(
{'local': print_ret},
out,
self.config)
if __name__ == '__main__':
salt_call()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment