Move the state.py module components into cmd

Than change here is to make cmd.{run,wait,script,etc} accept an argument
called stateful, and then it runs the commands using the stateful
approach debfined by @kjkuan
This commit is contained in:
Thomas S Hatch 2012-11-12 13:30:19 -07:00
parent eb512e5a1c
commit fbab7151cb
2 changed files with 229 additions and 199 deletions

View File

@ -35,9 +35,115 @@ syslog if there is no disk space:
import grp
import os
import copy
import json
import shlex
import copy
import logging
# Import salt libs
from salt.exceptions import CommandExecutionError
import salt.state
log = logging.getLogger(__name__)
def _delegate_to_state(func, name, **kws):
'''
Delegate execution to a state function with arguments(name+kws).
'''
# eg, _delegate_to_state("cmd.run", "echo hello", cwd="/")
state_name, state_func = func.split('.', 1)
# the following code fragment is copied and modified from salt's
# modules.state.single()
kws.update(dict(state=state_name, fun=state_func, name=name))
opts = copy.copy(__opts__)
st_ = salt.state.State(opts)
err = st_.verify_data(kws)
if err:
log.error(str(err))
raise Exception('Failed verifying state input!')
return st_.call(kws)
def _reinterpreted_state(state):
'''
Re-interpret the state return by salt.sate.run using our protocol.
'''
ret = state['changes']
state['changes'] = {}
state['comment'] = ''
out = ret.get('stdout')
if not out:
if ret.get('stderr'):
state['comment'] = ret['stderr']
return state
is_json = False
try:
d = json.loads(out)
if not isinstance(d, dict):
return _failout(state,
'script JSON output must be a JSON object(ie, {})!')
is_json = True
except Exception:
idx = out.rstrip().rfind('\n')
if idx != -1:
out = out[idx+1:]
d = {}
try:
for item in shlex.split(out):
k, v = item.split('=')
d[k] = v
except ValueError:
return _failout(state,
'Failed parsing script output! '
'Stdout must be JSON or a line of name=value pairs.')
changed = _is_true(d.get('changed', 'no'))
if 'comment' in d:
state['comment'] = d['comment']
del d['comment']
if changed:
for k in ret:
d.setdefault(k, ret[k])
# if stdout is the state output in json, don't show it.
# otherwise it contains the one line name=value pairs, strip it.
d['stdout'] = '' if is_json else d.get('stdout', '')[:idx]
state['changes'] = d
#FIXME: if it's not changed but there's stdout and/or stderr then those
# won't be shown as the function output. (though, they will be shown
# inside INFO logs).
return state
def _failout(state, msg):
state['comment'] = msg
state['result'] = False
return state
def _is_true(v):
if v and str(v).lower() in ('true', 'yes', '1'):
return True
elif str(v).lower() in ('false', 'no', '0'):
return False
raise ValueError('Failed parsing boolean value: {0}'.format(v))
def _no_op(name, **kws):
'''
No-op state to support state config via the stateconf renderer.
'''
return dict(name=name, result=True, changes={}, comment='')
config = _no_op
def _run_check(cmd_kwargs, onlyif, unless, cwd, user, group, shell):
@ -77,7 +183,9 @@ def wait(name,
cwd='/root',
user=None,
group=None,
shell=None):
shell=None,
stateful=False,
**kwargs):
'''
Run the given command only if the watch statement calls it
@ -122,6 +230,7 @@ def wait_script(name,
group=None,
shell=None,
env=None,
stateful=False,
**kwargs):
'''
Download a script from a remote source and execute it only if a watch
@ -166,6 +275,7 @@ def run(name,
group=None,
shell=None,
env=(),
stateful=False,
**kwargs):
'''
Run a command if certain circumstances are met
@ -194,7 +304,26 @@ def run(name,
shell
The shell to use for execution, defaults to the shell grain
stateful
The command being executed is expected to return data about executing
a state
'''
if stateful:
return _reinterpreted_state(
_delegate_to_state(
'cmd.run',
name,
onlyif,
unless,
cwd,
user,
group,
shell,
env,
**kwargs
)
)
ret = {'name': name,
'changes': {},
'result': False,
@ -258,6 +387,7 @@ def script(name,
group=None,
shell=None,
env=None,
stateful=False,
**kwargs):
'''
Download a script from a remote source and execute it. The name can be the
@ -287,7 +417,26 @@ def script(name,
shell
The shell to use for execution, defaults to the shell grain
stateful
The command being executed is expected to return data about executing
a state
'''
if stateful:
return _reinterpreted_state(
_delegate_to_state(
'cmd.script',
name,
onlyif,
unless,
cwd,
user,
group,
shell,
env,
**kwargs
)
)
ret = {'changes': {},
'comment': '',
'name': name,
@ -357,8 +506,24 @@ def mod_watch(name, **kwargs):
Execute a cmd function based on a watch call
'''
if kwargs['sfun'] == 'wait' or kwargs['sfun'] == 'run':
if kwargs['stateful']:
kwargs.pop('stateful')
return _reinterpreted_state(
_delegate_to_state(
'cmd.run',
**kwargs
)
)
return run(name, **kwargs)
elif kwargs['sfun'] == 'wait_script' or kwargs['sfun'] == 'script':
if kwargs['stateful']:
kwargs.pop('stateful')
return _reinterpreted_state(
_delegate_to_state(
'cmd.script',
**kwargs
)
)
return script(name, **kwargs)
return {'name': name,
'changes': {},
@ -367,3 +532,66 @@ def mod_watch(name, **kwargs):
kwargs['sfun']
),
'result': False}
'''
This module is similar to the built-in cmd state module except that rather
than always resulting in a changed state, the state of a command or script
execution is determined by the command or the script itself.
This allows a state to watch for changes in another state that executes
a command or script.
This is done using a simple protocol similar to the one used by Ansible's
modules. Here's how it works:
If there's nothing in the stdout of the command, then assume no changes.
Otherwise, the stdout must be either in JSON or its `last` non-empty line
must be a string of key=value pairs delimited by spaces(no spaces on the
sides of '=').
If it's JSON then it must be a JSON object(ie, {}).
If it's key=value pairs then quoting may be used to include spaces.
(Python's shlex module is used to parse the key=value string)
Two special keys or attributes are recognized in the output::
changed: bool (ie, 'yes', 'no', 'true', 'false', case-insensitive)
comment: str (ie, any string)
So, only if 'changed' is true then assume the command execution has changed
the state, and any other key values or attributes in the output will be set
as part of the changes.
If there's a comment then it will be used as the comment of the state.
Here's an example of how one might write a shell script for use by this state
function::
#!/bin/bash
#
echo "Working hard..."
# writing the state line
echo # an empty line here so the next line will be the last.
echo "changed=yes comment=\"something's changed!\" whatever=123"
And an example salt files using this module::
Run myscript:
state.run:
- name: /path/to/myscript
- cwd: /
Run only if myscript changed something:
cmd.wait:
- name: echo hello
- cwd: /
- watch:
- state: Run myscript
Note that instead of using `cmd.wait` in the example, `state.wait` can be
used, and in which case it can then be watched by some other states.
'''

View File

@ -1,198 +0,0 @@
'''
This module is similar to the built-in cmd state module except that rather
than always resulting in a changed state, the state of a command or script
execution is determined by the command or the script itself.
This allows a state to watch for changes in another state that executes
a command or script.
This is done using a simple protocol similar to the one used by Ansible's
modules. Here's how it works:
If there's nothing in the stdout of the command, then assume no changes.
Otherwise, the stdout must be either in JSON or its `last` non-empty line
must be a string of key=value pairs delimited by spaces(no spaces on the
sides of '=').
If it's JSON then it must be a JSON object(ie, {}).
If it's key=value pairs then quoting may be used to include spaces.
(Python's shlex module is used to parse the key=value string)
Two special keys or attributes are recognized in the output::
changed: bool (ie, 'yes', 'no', 'true', 'false', case-insensitive)
comment: str (ie, any string)
So, only if 'changed' is true then assume the command execution has changed
the state, and any other key values or attributes in the output will be set
as part of the changes.
If there's a comment then it will be used as the comment of the state.
Here's an example of how one might write a shell script for use by this state
function::
#!/bin/bash
#
echo "Working hard..."
# writing the state line
echo # an empty line here so the next line will be the last.
echo "changed=yes comment=\"something's changed!\" whatever=123"
And an example salt files using this module::
Run myscript:
state.run:
- name: /path/to/myscript
- cwd: /
Run only if myscript changed something:
cmd.wait:
- name: echo hello
- cwd: /
- watch:
- state: Run myscript
Note that instead of using `cmd.wait` in the example, `state.wait` can be
used, and in which case it can then be watched by some other states.
'''
# Import python libs
import json
import shlex
import copy
import logging
# Import Salt libs
import salt.state
log = logging.getLogger(__name__)
def run(name, **kws):
return _reinterpreted_state(_delegate_to_state('cmd.run', name, **kws))
def script(name, **kws):
return _reinterpreted_state(_delegate_to_state('cmd.script', name, **kws))
def wait(name, **kws):
return _reinterpreted_state(_delegate_to_state('cmd.wait', name, **kws))
def wait_script(name, **kws):
return _reinterpreted_state(_delegate_to_state('cmd.wait_script', name, **kws))
# basically just a copy & paste from the salt built-in cmd state module.
def mod_watch(name, **kwargs):
'''
Execute a cmd function based on a watch call
'''
if kwargs['sfun'] == 'wait' or kwargs['sfun'] == 'run':
return run(name, **kwargs)
elif kwargs['sfun'] == 'wait_script' or kwargs['sfun'] == 'script':
return script(name, **kwargs)
def _delegate_to_state(func, name, **kws):
'''
Delegate execution to a state function with arguments(name+kws).
'''
# eg, _delegate_to_state("cmd.run", "echo hello", cwd="/")
state_name, state_func = func.split('.', 1)
# the following code fragment is copied and modified from salt's
# modules.state.single()
kws.update(dict(state=state_name, fun=state_func, name=name))
opts = copy.copy(__opts__)
st_ = salt.state.State(opts)
err = st_.verify_data(kws)
if err:
log.error(str(err))
raise Exception('Failed verifying state input!')
return st_.call(kws)
def _reinterpreted_state(state):
'''
Re-interpret the state return by salt.sate.run using our protocol.
'''
ret = state['changes']
state['changes'] = {}
state['comment'] = ''
out = ret.get('stdout')
if not out:
if ret.get('stderr'):
state['comment'] = ret['stderr']
return state
is_json = False
try:
d = json.loads(out)
if not isinstance(d, dict):
return _failout(state,
'script JSON output must be a JSON object(ie, {})!')
is_json = True
except Exception:
idx = out.rstrip().rfind('\n')
if idx != -1:
out = out[idx+1:]
d = {}
try:
for item in shlex.split(out):
k, v = item.split('=')
d[k] = v
except ValueError:
return _failout(state,
'Failed parsing script output! '
'Stdout must be JSON or a line of name=value pairs.')
changed = _is_true(d.get('changed', 'no'))
if 'comment' in d:
state['comment'] = d['comment']
del d['comment']
if changed:
for k in ret:
d.setdefault(k, ret[k])
# if stdout is the state output in json, don't show it.
# otherwise it contains the one line name=value pairs, strip it.
d['stdout'] = '' if is_json else d.get('stdout', '')[:idx]
state['changes'] = d
#FIXME: if it's not changed but there's stdout and/or stderr then those
# won't be shown as the function output. (though, they will be shown
# inside INFO logs).
return state
def _failout(state, msg):
state['comment'] = msg
state['result'] = False
return state
def _is_true(v):
if v and str(v).lower() in ('true', 'yes', '1'):
return True
elif str(v).lower() in ('false', 'no', '0'):
return False
raise ValueError('Failed parsing boolean value: {0}'.format(v))
def _no_op(name, **kws):
'''
No-op state to support state config via the stateconf renderer.
'''
return dict(name=name, result=True, changes={}, comment='')
config = _no_op