Last active
August 29, 2015 14:25
-
-
Save pcn/9f2717c993c0d7e4d862 to your computer and use it in GitHub Desktop.
A saltstack "tester" that will use controlled input to create expected (?) output
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
#!/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