Merge branch '2014.7' into merge-forward

Conflicts:
	salt/modules/bsd_shadow.py
	salt/modules/freebsdjail.py
	salt/modules/yumpkg.py
	salt/modules/zfs.py
	salt/modules/zypper.py
	salt/netapi/rest_tornado/saltnado.py
	salt/states/dockerio.py
This commit is contained in:
Colton Myers 2014-11-18 12:46:29 -07:00
commit 716a7e3331
32 changed files with 2217 additions and 475 deletions

1
.gitignore vendored
View file

@ -19,6 +19,7 @@ MANIFEST
bin/
include/
lib/
lib64
pip/
share/
tests/integration/tmp/

View file

@ -237,6 +237,18 @@ distro the minion is running, in case they differ from the example below.
- name: atd
- enable: True
An alternatvie to using the :program:`atd` daemon is to fork and disown the
process.
.. code-block:: yaml
restart_minion:
cmd.run:
- name: |
nohup /bin/sh -c 'sleep 10 && salt-call --local service.restart salt-minion'
- python_shell: True
- order: last
Windows
*******

View file

@ -6,4 +6,70 @@ rest_tornado
.. automodule:: salt.netapi.rest_tornado.saltnado_websockets
.. ............................................................................
REST URI Reference
==================
.. py:currentmodule:: salt.netapi.rest_tornado.saltnado
.. contents::
:local:
``/``
-----
.. autoclass:: SaltAPIHandler
:members: GET, POST
``/login``
----------
.. autoclass:: Login
:members: GET, POST
``/logout``
-----------
.. autoclass:: Logout
:members: POST
``/minions``
------------
.. autoclass:: Minions
:members: GET, POST
``/jobs``
---------
.. autoclass:: Jobs
:members: GET
``/run``
--------
.. autoclass:: Run
:members: POST
``/events``
-----------
.. autoclass:: Events
:members: GET
``/ws``
-------
.. autoclass:: WebsocketEndpoint
:members: GET
``/hook``
---------
.. autoclass:: Webhook
:members: POST
``/stats``
----------
.. autoclass:: Stats
:members: GET

View file

@ -17,12 +17,26 @@ Authentication events
``reject``.
:var pub: The minion public key.
Start events
============
.. salt:event:: salt/minion/<MID>/start
Fired every time a minion connects to the Salt master.
:var id: The minion ID.
Key events
==========
.. salt:event:: salt/key
Fired when accepting and rejecting minions keys on the Salt master.
:var id: The minion ID.
:var act: The new status of the minion key: ``accept``, ``pend``,
``reject``.
Job events
==========

View file

@ -2138,7 +2138,7 @@ def queue_instances(instances):
salt.utils.cloud.cache_node(node, __active_provider_name__, __opts__)
def create_attach_volumes(name, kwargs, call=None):
def create_attach_volumes(name, kwargs, call=None, wait_to_finish=True):
'''
Create and attach volumes to created node
'''
@ -2178,7 +2178,7 @@ def create_attach_volumes(name, kwargs, call=None):
volume_dict['iops'] = volume['iops']
if 'volume_id' not in volume_dict:
created_volume = create_volume(volume_dict, call='function')
created_volume = create_volume(volume_dict, call='function', wait_to_finish=wait_to_finish)
created = True
for item in created_volume:
if 'volumeId' in item:

View file

@ -575,22 +575,23 @@ def _get_file_from_s3(metadata, saltenv, bucket_name, path, cached_file_path):
path=urllib.quote(path),
local_file=cached_file_path
)
for header in ret['headers']:
name, value = header.split(':', 1)
name = name.strip()
value = value.strip()
if name == 'Last-Modified':
s3_file_mtime = datetime.datetime.strptime(
value, '%a, %d %b %Y %H:%M:%S %Z')
elif name == 'Content-Length':
s3_file_size = int(value)
if (cached_file_size == s3_file_size and
cached_file_mtime > s3_file_mtime):
log.info(
'{0} - {1} : {2} skipped download since cached file size '
'equal to and mtime after s3 values'.format(
bucket_name, saltenv, path))
return
if ret is not None:
for header in ret['headers']:
name, value = header.split(':', 1)
name = name.strip()
value = value.strip()
if name == 'Last-Modified':
s3_file_mtime = datetime.datetime.strptime(
value, '%a, %d %b %Y %H:%M:%S %Z')
elif name == 'Content-Length':
s3_file_size = int(value)
if (cached_file_size == s3_file_size and
cached_file_mtime > s3_file_mtime):
log.info(
'{0} - {1} : {2} skipped download since cached file size '
'equal to and mtime after s3 values'.format(
bucket_name, saltenv, path))
return
# ... or get the file from S3
s3.query(

View file

@ -17,6 +17,10 @@ import logging
# Import salt libs
import salt.utils.validate.net
from salt.exceptions import CommandExecutionError
try:
from shlex import quote as _cmd_quote
except ImportError:
from pipes import quote as _cmd_quote
log = logging.getLogger(__name__)
HAS_PYBLUEZ = False
@ -254,10 +258,10 @@ def pair(address, key):
)
addy = address_()
cmd = 'echo "{0}" | bluez-simple-agent {1} {2}'.format(
addy['device'], address, key
cmd = 'echo {0} | bluez-simple-agent {1} {2}'.format(
_cmd_quote(addy['device']), _cmd_quote(address), _cmd_quote(key)
)
out = __salt__['cmd.run'](cmd).splitlines()
out = __salt__['cmd.run'](cmd, python_shell=True).splitlines()
return out

View file

@ -9,6 +9,11 @@ try:
except ImportError:
pass
try:
from shlex import quote as _cmd_quote
except ImportError:
from pipes import quote as _cmd_quote
# Define the module's virtual name
__virtualname__ = 'shadow'
@ -59,7 +64,7 @@ def info(name):
if cmd:
cmd += '| cut -f6,7 -d:'
try:
change, expire = __salt__['cmd.run_all'](cmd)['stdout'].split(':')
change, expire = __salt__['cmd.run_all'](cmd, python_shell=True)['stdout'].split(':')
except ValueError:
pass
else:

View file

@ -1489,7 +1489,7 @@ def build_bond(iface, **settings):
__salt__['cmd.run'](
'sed -i -e "/^options\\s{0}.*/d" /etc/modprobe.conf'.format(iface)
)
__salt__['cmd.run']('cat {0} >> /etc/modprobe.conf'.format(path))
__salt__['file.append']('/etc/modprobe.conf', path)
# Load kernel module
__salt__['kmod.load']('bonding')

View file

@ -7,6 +7,10 @@ from __future__ import absolute_import
# Import python libs
import glob
import re
try:
from shlex import quote as _cmd_quote
except ImportError:
from pipes import quote as _cmd_quote
# Import salt libs
from .systemd import _sd_booted
@ -245,16 +249,16 @@ def enable(name, **kwargs):
'''
osmajor = _osrel()[0]
if osmajor < '6':
cmd = 'update-rc.d -f {0} defaults 99'.format(name)
cmd = 'update-rc.d -f {0} defaults 99'.format(_cmd_quote(name))
else:
cmd = 'update-rc.d {0} enable'.format(name)
cmd = 'update-rc.d {0} enable'.format(_cmd_quote(name))
try:
if int(osmajor) >= 6:
cmd = 'insserv {0} && '.format(name) + cmd
cmd = 'insserv {0} && '.format(_cmd_quote(name)) + cmd
except ValueError:
if osmajor == 'testing/unstable' or osmajor == 'unstable':
cmd = 'insserv {0} && '.format(name) + cmd
return not __salt__['cmd.retcode'](cmd)
cmd = 'insserv {0} && '.format(_cmd_quote(name)) + cmd
return not __salt__['cmd.retcode'](cmd, python_shell=True)
def disable(name, **kwargs):

View file

@ -8,6 +8,7 @@ from __future__ import absolute_import
import os
import subprocess
import shlex
import re
# Import salt libs
import salt.utils
@ -75,8 +76,12 @@ def is_enabled():
salt '*' jail.is_enabled <jail name>
'''
cmd = 'service -e | grep jail'
return not __salt__['cmd.retcode'](cmd)
cmd = 'service -e'
services = __salt__['cmd.run'](cmd, python_shell=False)
for service in services.split('\\n'):
if re.search('jail', service):
return True
return False
def get_enabled():
@ -214,8 +219,12 @@ def status(jail):
salt '*' jail.status <jail name>
'''
cmd = 'jls | grep {0}'.format(jail)
return not __salt__['cmd.retcode'](cmd)
cmd = 'jls'
found_jails = __salt__['cmd.run'](cmd, python_shell=False)
for found_jail in found_jails.split('\\n'):
if re.search(jail, found_jail):
return True
return False
def sysctl():

View file

@ -39,7 +39,7 @@ def get_sys():
cmd = 'grep XKBLAYOUT /etc/default/keyboard | grep -vE "^#"'
elif 'Gentoo' in __grains__['os_family']:
cmd = 'grep "^keymap" /etc/conf.d/keymaps | grep -vE "^#"'
out = __salt__['cmd.run'](cmd).split('=')
out = __salt__['cmd.run'](cmd, python_shell=True).split('=')
ret = out[1].replace('"', '')
return ret
@ -82,7 +82,7 @@ def get_x():
salt '*' keyboard.get_x
'''
cmd = 'setxkbmap -query | grep layout'
out = __salt__['cmd.run'](cmd).split(':')
out = __salt__['cmd.run'](cmd, python_shell=True).split(':')
return out[1].strip()

View file

@ -70,7 +70,8 @@ def _list_gids():
Return a list of gids in use
'''
cmd = __salt__['cmd.run']('dscacheutil -q group | grep gid:',
output_loglevel='quiet')
output_loglevel='quiet',
python_shell=True)
data_list = cmd.split()
for item in data_list:
if item == 'gid:':

View file

@ -60,7 +60,7 @@ def get_zone():
return os.readlink('/etc/localtime').lstrip('/usr/share/zoneinfo/')
elif 'Solaris' in __grains__['os_family']:
cmd = 'grep "TZ=" /etc/TIMEZONE'
out = __salt__['cmd.run'](cmd).split('=')
out = __salt__['cmd.run'](cmd, python_shell=True).split('=')
ret = out[1].replace('"', '')
return ret
@ -217,7 +217,8 @@ def get_hwclock():
elif 'Debian' in __grains__['os_family']:
#Original way to look up hwclock on Debian-based systems
cmd = 'grep "UTC=" /etc/default/rcS | grep -vE "^#"'
out = __salt__['cmd.run'](cmd, ignore_retcode=True).split('=')
out = __salt__['cmd.run'](
cmd, ignore_retcode=True, python_shell=True).split('=')
if len(out) > 1:
if out[1] == 'yes':
return 'UTC'
@ -229,7 +230,7 @@ def get_hwclock():
return __salt__['cmd.run'](cmd)
elif 'Gentoo' in __grains__['os_family']:
cmd = 'grep "^clock=" /etc/conf.d/hwclock | grep -vE "^#"'
out = __salt__['cmd.run'](cmd).split('=')
out = __salt__['cmd.run'](cmd, python_shell=True).split('=')
return out[1].replace('"', '')
elif 'Solaris' in __grains__['os_family']:
if os.path.isfile('/etc/rtc_config'):

View file

@ -22,6 +22,10 @@ import re
import six
from distutils.version import LooseVersion as _LooseVersion
from six.moves import range
try:
from shlex import quote as _cmd_quote
except ImportError:
from pipes import quote as _cmd_quote
# Import salt libs
import salt.utils
@ -136,8 +140,8 @@ def _repoquery(repoquery_args, query_format=__QUERYFORMAT):
Runs a repoquery command and returns a list of namedtuples
'''
_check_repoquery()
cmd = 'repoquery --plugins --queryformat="{0}" {1}'.format(
query_format, repoquery_args
cmd = 'repoquery --plugins --queryformat={0} {1}'.format(
_cmd_quote(query_format), repoquery_args
)
call = __salt__['cmd.run_all'](cmd, output_loglevel='trace')
if call['retcode'] != 0:
@ -230,9 +234,10 @@ def _rpm_pkginfo(name):
# with "none"
queryformat = __QUERYFORMAT.replace('%{REPOID}', 'none')
output = __salt__['cmd.run_stdout'](
'rpm -qp --queryformat {0!r} {1}'.format(queryformat, name),
'rpm -qp --queryformat {0!r} {1}'.format(_cmd_quote(queryformat), name),
output_loglevel='trace',
ignore_retcode=True
ignore_retcode=True,
python_shell=True
)
return _parse_pkginfo(output)
@ -628,8 +633,10 @@ def refresh_db(**kwargs):
}
branch_arg = _get_branch_option(**kwargs)
cmd = 'yum -q clean expire-cache && yum -q check-update {0}'.format(branch_arg)
ret = __salt__['cmd.retcode'](cmd, ignore_retcode=True)
clean_cmd = 'yum -q clean expire-cache'
__salt__['cmd.run'](clean_cmd)
update_cmd = 'yum -q check-update {0}'.format(branch_arg)
ret = __salt__['cmd.retcode'](update_cmd, ignore_retcode=True)
return retcodes.get(ret, False)
@ -950,7 +957,7 @@ def install(name=None,
gpgcheck='--nogpgcheck' if skip_verify else '',
pkg=' '.join(targets),
)
__salt__['cmd.run'](cmd, output_loglevel='trace')
__salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=True)
if downgrade:
cmd = 'yum -y {repo} {exclude} {branch} {gpgcheck} downgrade {pkg}'.format(
@ -960,7 +967,7 @@ def install(name=None,
gpgcheck='--nogpgcheck' if skip_verify else '',
pkg=' '.join(downgrade),
)
__salt__['cmd.run'](cmd, output_loglevel='trace')
__salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=True)
if to_reinstall:
cmd = 'yum -y {repo} {exclude} {branch} {gpgcheck} reinstall {pkg}'.format(
@ -970,7 +977,7 @@ def install(name=None,
gpgcheck='--nogpgcheck' if skip_verify else '',
pkg=' '.join(six.itervalues(to_reinstall)),
)
__salt__['cmd.run'](cmd, output_loglevel='trace')
__salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=True)
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
@ -1039,7 +1046,7 @@ def upgrade(refresh=True, fromrepo=None, skip_verify=False, **kwargs):
branch=branch_arg,
gpgcheck='--nogpgcheck' if skip_verify else '')
__salt__['cmd.run'](cmd, output_loglevel='trace')
__salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=True)
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
ret = salt.utils.compare_dicts(old, new)
@ -1084,8 +1091,9 @@ def remove(name=None, pkgs=None, **kwargs): # pylint: disable=W0613
targets = [x for x in pkg_params if x in old]
if not targets:
return {}
cmd = 'yum -q -y remove "{0}"'.format('" "'.join(targets))
__salt__['cmd.run'](cmd, output_loglevel='trace')
quoted_targets = [_cmd_quote(target) for target in targets]
cmd = 'yum -q -y remove {0}'.format(' '.join(quoted_targets))
__salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=True)
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
ret = salt.utils.compare_dicts(old, new)
@ -1283,9 +1291,11 @@ def unhold(name=None, pkgs=None, sources=None, **kwargs): # pylint: disable=W06
ret[target]['comment'] = ('Package {0} is set to be unheld.'
.format(target))
else:
_targets = ' '.join('"' + item + '"' for item in search_locks)
cmd = 'yum -q versionlock delete {0}'.format(_targets)
out = __salt__['cmd.run_all'](cmd)
quoted_targets = [_cmd_quote(item) for item in search_locks]
cmd = 'yum -q versionlock delete {0}'.format(
' '.join(quoted_targets)
)
out = __salt__['cmd.run_all'](cmd, python_shell=True)
if out['retcode'] == 0:
ret[target].update(result=True)
@ -1432,18 +1442,18 @@ def group_info(name):
}
cmd_template = 'repoquery --plugins --group --grouppkgs={0} --list {1!r}'
cmd = cmd_template.format('all', name)
out = __salt__['cmd.run_stdout'](cmd, output_loglevel='trace')
cmd = cmd_template.format('all', _cmd_quote(name))
out = __salt__['cmd.run_stdout'](cmd, output_loglevel='trace', python_shell=True)
all_pkgs = set(out.splitlines())
if not all_pkgs:
raise CommandExecutionError('Group {0!r} not found'.format(name))
for pkgtype in ('mandatory', 'optional', 'default'):
cmd = cmd_template.format(pkgtype, name)
cmd = cmd_template.format(pkgtype, _cmd_quote(name))
packages = set(
__salt__['cmd.run_stdout'](
cmd, output_loglevel='trace'
cmd, output_loglevel='trace', python_shell=True
).splitlines()
)
ret['{0} packages'.format(pkgtype)].extend(sorted(packages))
@ -1454,8 +1464,10 @@ def group_info(name):
# considered to be conditional packages.
ret['conditional packages'] = sorted(all_pkgs)
cmd = 'repoquery --plugins --group --info {0!r}'.format(name)
out = __salt__['cmd.run_stdout'](cmd, output_loglevel='trace')
cmd = 'repoquery --plugins --group --info {0!r}'.format(_cmd_quote(name))
out = __salt__['cmd.run_stdout'](
cmd, output_loglevel='trace', python_shell=True
)
if out:
ret['description'] = '\n'.join(out.splitlines()[1:]).strip()
@ -1839,10 +1851,16 @@ def owner(*paths):
if not paths:
return ''
ret = {}
cmd = 'rpm -qf --queryformat "%{{NAME}}" {0!r}'
for path in paths:
ret[path] = __salt__['cmd.run_stdout'](cmd.format(path),
output_loglevel='trace')
cmd = 'rpm -qf --queryformat {0} {1!r}'.format(
_cmd_quote('%{{NAME}}'),
path
)
ret[path] = __salt__['cmd.run_stdout'](
cmd.format(path),
output_loglevel='trace',
python_shell=True
)
if 'not owned' in ret[path].lower():
ret[path] = ''
if len(ret) == 1:

View file

@ -15,6 +15,11 @@ import six
import six.moves.configparser # pylint: disable=E0611
import urlparse
from xml.dom import minidom as dom
from contextlib import contextmanager as _contextmanager
try:
from shlex import quote as _cmd_quote
except ImportError:
from pipes import quote as _cmd_quote
# Import salt libs
import salt.utils
@ -124,7 +129,11 @@ def latest_version(*names, **kwargs):
# Split call to zypper into batches of 500 packages
while restpackages:
cmd = 'zypper info -t package {0}'.format(' '.join(restpackages[:500]))
output = __salt__['cmd.run_stdout'](cmd, output_loglevel='trace')
output = __salt__['cmd.run_stdout'](
cmd,
output_loglevel='trace',
python_shell=True
)
outputs.extend(re.split('Information for package \\S+:\n', output))
restpackages = restpackages[500:]
for package in outputs:
@ -211,9 +220,14 @@ def list_pkgs(versions_as_list=False, **kwargs):
__salt__['pkg_resource.stringify'](ret)
return ret
cmd = 'rpm -qa --queryformat "%{NAME}_|-%{VERSION}_|-%{RELEASE}\\n"'
pkg_fmt = '%{NAME}_|-%{VERSION}_|-%{RELEASE}\\n'
cmd = 'rpm -qa --queryformat {0}'.format(_cmd_quote(pkg_fmt))
ret = {}
out = __salt__['cmd.run'](cmd, output_loglevel='trace')
out = __salt__['cmd.run'](
cmd,
output_loglevel='trace',
python_shell=True
)
for line in out.splitlines():
name, pkgver, rel = line.split('_|-')
if rel:
@ -601,13 +615,18 @@ def install(name=None,
while targets:
# Quotes needed around package targets because of the possibility of
# output redirection characters "<" or ">" in zypper command.
quoted_targets = [_cmd_quote(target) for target in targets[:500]]
cmd = (
'zypper --non-interactive install --name '
'--auto-agree-with-licenses {0}"{1}"'
.format(fromrepoopt, '" "'.join(targets[:500]))
)
'zypper --non-interactive install --name '
'--auto-agree-with-licenses {0}{1}'
.format(fromrepoopt, ' '.join(quoted_targets))
)
targets = targets[500:]
out = __salt__['cmd.run'](cmd, output_loglevel='trace')
out = __salt__['cmd.run'](
cmd,
output_loglevel='trace',
python_shell=True
)
for line in out.splitlines():
match = re.match(
"^The selected package '([^']+)'.+has lower version",
@ -622,7 +641,7 @@ def install(name=None,
'--auto-agree-with-licenses --force {0}{1}'
.format(fromrepoopt, ' '.join(downgrades[:500]))
)
__salt__['cmd.run'](cmd, output_loglevel='trace')
__salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=True)
downgrades = downgrades[500:]
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
@ -687,7 +706,7 @@ def _uninstall(action='remove', name=None, pkgs=None):
'zypper --non-interactive remove {0} {1}'
.format(purge_arg, ' '.join(targets[:500]))
)
__salt__['cmd.run'](cmd, output_loglevel='trace')
__salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=True)
targets = targets[500:]
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()

View file

@ -826,9 +826,9 @@ class Minions(LowDataAdapter):
return:
- jid: '20130603122505459265'
minions: [ms-4, ms-3, ms-2, ms-1, ms-0]
minions: [ms-4, ms-3, ms-2, ms-1, ms-0]
_links:
jobs:
jobs:
- href: /jobs/20130603122505459265
'''
job_data = list(self.exec_lowstate(client='local_async',
@ -1456,61 +1456,61 @@ class Events(object):
data: {'tag': '20130802115730568475', 'data': {'jid': '20130802115730568475', 'return': True, 'retcode': 0, 'success': True, 'cmd': '_return', 'fun': 'test.ping', 'id': 'ms-1'}}
The event stream can be easily consumed via JavaScript:
The event stream can be easily consumed via JavaScript:
.. code-block:: javascript
.. code-block:: javascript
# Note, you must be authenticated!
var source = new EventSource('/events');
source.onopen = function() { console.debug('opening') };
source.onerror = function(e) { console.debug('error!', e) };
source.onmessage = function(e) { console.debug(e.data) };
# Note, you must be authenticated!
var source = new EventSource('/events');
source.onopen = function() { console.debug('opening') };
source.onerror = function(e) { console.debug('error!', e) };
source.onmessage = function(e) { console.debug(e.data) };
Or using CORS:
Or using CORS:
.. code-block:: javascript
.. code-block:: javascript
var source = new EventSource('/events', {withCredentials: true});
var source = new EventSource('/events', {withCredentials: true});
Some browser clients lack CORS support for the ``EventSource()`` API. Such
clients may instead pass the :mailheader:`X-Auth-Token` value as an URL
parameter:
Some browser clients lack CORS support for the ``EventSource()`` API. Such
clients may instead pass the :mailheader:`X-Auth-Token` value as an URL
parameter:
.. code-block:: bash
.. code-block:: bash
curl -NsS localhost:8000/events/6d1b722e
curl -NsS localhost:8000/events/6d1b722e
It is also possible to consume the stream via the shell.
It is also possible to consume the stream via the shell.
Records are separated by blank lines; the ``data:`` and ``tag:``
prefixes will need to be removed manually before attempting to
unserialize the JSON.
Records are separated by blank lines; the ``data:`` and ``tag:``
prefixes will need to be removed manually before attempting to
unserialize the JSON.
curl's ``-N`` flag turns off input buffering which is required to
process the stream incrementally.
curl's ``-N`` flag turns off input buffering which is required to
process the stream incrementally.
Here is a basic example of printing each event as it comes in:
Here is a basic example of printing each event as it comes in:
.. code-block:: bash
.. code-block:: bash
curl -NsS localhost:8000/events |\
while IFS= read -r line ; do
echo $line
done
curl -NsS localhost:8000/events |\
while IFS= read -r line ; do
echo $line
done
Here is an example of using awk to filter events based on tag:
Here is an example of using awk to filter events based on tag:
.. code-block:: bash
.. code-block:: bash
curl -NsS localhost:8000/events |\
awk '
BEGIN { RS=""; FS="\\n" }
$1 ~ /^tag: salt\/job\/[0-9]+\/new$/ { print $0 }
'
tag: salt/job/20140112010149808995/new
data: {"tag": "salt/job/20140112010149808995/new", "data": {"tgt_type": "glob", "jid": "20140112010149808995", "tgt": "jerry", "_stamp": "2014-01-12_01:01:49.809617", "user": "shouse", "arg": [], "fun": "test.ping", "minions": ["jerry"]}}
tag: 20140112010149808995
data: {"tag": "20140112010149808995", "data": {"fun_args": [], "jid": "20140112010149808995", "return": true, "retcode": 0, "success": true, "cmd": "_return", "_stamp": "2014-01-12_01:01:49.819316", "fun": "test.ping", "id": "jerry"}}
curl -NsS localhost:8000/events |\
awk '
BEGIN { RS=""; FS="\\n" }
$1 ~ /^tag: salt\/job\/[0-9]+\/new$/ { print $0 }
'
tag: salt/job/20140112010149808995/new
data: {"tag": "salt/job/20140112010149808995/new", "data": {"tgt_type": "glob", "jid": "20140112010149808995", "tgt": "jerry", "_stamp": "2014-01-12_01:01:49.809617", "user": "shouse", "arg": [], "fun": "test.ping", "minions": ["jerry"]}}
tag: 20140112010149808995
data: {"tag": "20140112010149808995", "data": {"fun_args": [], "jid": "20140112010149808995", "return": true, "retcode": 0, "success": true, "cmd": "_return", "_stamp": "2014-01-12_01:01:49.819316", "fun": "test.ping", "id": "jerry"}}
'''
# Pulling the session token from an URL param is a workaround for
# browsers not supporting CORS in the EventSource API.

View file

@ -41,18 +41,9 @@ def start():
mod_opts = __opts__.get(__virtualname__, {})
if mod_opts.get('websockets', False):
from . import saltnado_websockets
if 'num_processes' not in mod_opts:
mod_opts['num_processes'] = 1
token_pattern = r"([0-9A-Fa-f]{0})".format(len(getattr(hashlib, __opts__.get('hash_type', 'md5'))().hexdigest()))
all_events_pattern = r"/all_events/{0}".format(token_pattern)
formatted_events_pattern = r"/formatted_events/{0}".format(token_pattern)
logger.debug("All events URL pattern is {0}".format(all_events_pattern))
paths = [
(r"/", saltnado.SaltAPIHandler),
(r"/login", saltnado.SaltAuthHandler),
@ -67,6 +58,12 @@ def start():
# if you have enabled websockets, add them!
if mod_opts.get('websockets', False):
from . import saltnado_websockets
token_pattern = r"([0-9A-Fa-f]{0})".format(len(getattr(hashlib, __opts__.get('hash_type', 'md5'))().hexdigest()))
all_events_pattern = r"/all_events/{0}".format(token_pattern)
formatted_events_pattern = r"/formatted_events/{0}".format(token_pattern)
logger.debug("All events URL pattern is {0}".format(all_events_pattern))
paths += [
# Matches /all_events/[0-9A-Fa-f]{n}
# Where n is the length of hexdigest

File diff suppressed because it is too large Load diff

View file

@ -73,6 +73,11 @@ def orchestrate(mods, saltenv='base', test=None, exclude=None, pillar=None):
Execute a state run from the master, used as a powerful orchestration
system.
.. seealso:: More Orchestrate documentation
* :ref:`Full Orchestrate Tutorial <orchestrate-tutorial>`
* :py:mod:`Docs for the master-side state module <salt.states.saltmod>`
CLI Examples:
.. code-block:: bash
@ -177,7 +182,8 @@ def event(tagmatch='*', count=-1, quiet=False, sock_dir=None, pretty=False):
# Usage: ./eventlisten.sh '*' test.sleep 10
# Mimic fnmatch from the Python stdlib.
fnmatch () { case "$2" in $1) return 0 ;; *) return 1 ;; esac ; }
fnmatch() { case "$2" in $1) return 0 ;; *) return 1 ;; esac ; }
count() { printf '%s\n' "$#" ; }
listen() {
events='events'
@ -189,15 +195,32 @@ def event(tagmatch='*', count=-1, quiet=False, sock_dir=None, pretty=False):
salt-run state.event count=-1 >&3 &
events_pid=$!
(
timeout=$(( 60 * 60 ))
sleep $timeout
kill -s USR2 $$
) &
timeout_pid=$!
# Give the runner a few to connect to the event bus.
printf 'Subscribing to the Salt event bus...\n'
sleep 4
trap '
excode=$?; trap - EXIT;
exec 3>&-
kill '"${timeout_pid}"'
kill '"${events_pid}"'
rm '"${events}"'
exit
echo $excode
' INT TERM EXIT
trap '
printf '\''Timeout reached; exiting.\n'\''
exit 4
' USR2
# Run the command and get the JID.
jid=$(salt --async "$@")
jid="${jid#*: }" # Remove leading text up to the colon.
@ -206,11 +229,13 @@ def event(tagmatch='*', count=-1, quiet=False, sock_dir=None, pretty=False):
start_tag="salt/job/${jid}/new"
ret_tag="salt/job/${jid}/ret/*"
# ``read`` will block when no events are going through the bus.
printf 'Waiting for tag %s\n' "$ret_tag"
while read -r tag data; do
if fnmatch "$start_tag" "$tag"; then
minions=$(printf '%s\n' "${data}" | jq -r '.["minions"][]')
printf 'Waiting for minions: %s\n' "${minions}" | xargs
num_minions=$(count $minions)
printf 'Waiting for %s minions.\n' "$num_minions"
continue
fi
@ -221,10 +246,12 @@ def event(tagmatch='*', count=-1, quiet=False, sock_dir=None, pretty=False):
printf '%s\n' "$data" | jq .
minions="$(printf '%s\n' "$minions" | sed -e '/'"$mid"'/d')"
if (( ${#minions} )); then
printf 'Remaining minions: %s\n' "$minions" | xargs
else
num_minions=$(count $minions)
if [ $((num_minions)) -eq 0 ]; then
printf 'All minions returned.\n'
break
else
printf 'Remaining minions: %s\n' "$num_minions"
fi
else
printf 'Skipping tag: %s\n' "$tag"

View file

@ -591,25 +591,27 @@ def absent(name):
return _invalid(comment=("Container {0!r} could not be stopped"
.format(cid)))
else:
changes[cid]['new'] = 'stopped'
__salt__['docker.remove_container'](cid)
is_gone = __salt__['docker.exists'](cid)
if is_gone:
return _valid(comment=('Container {0!r}'
' was stopped and destroyed, '.format(cid)),
changes={name: True})
else:
return _valid(comment=('Container {0!r}'
' was stopped but could not be destroyed,'.format(cid)),
changes={name: True})
else:
changes[cid]['old'] = 'stopped'
# Remove the stopped container
removal = __salt__['docker.remove_container'](cid)
if removal['status'] is True:
changes[cid]['new'] = 'removed'
return _valid(comment=("Container {0!r} has been destroyed"
.format(cid)),
changes=changes)
else:
if 'new' not in changes[cid]:
changes = None
return _invalid(comment=("Container {0!r} could not be destroyed"
.format(cid)),
changes=changes)
__salt__['docker.remove_container'](cid)
is_gone = __salt__['docker.exists'](cid)
if is_gone:
return _valid(comment=('Container {0!r}'
' is stopped and was destroyed, '.format(cid)),
changes={name: True})
else:
return _valid(comment=('Container {0!r}'
' is stopped but could not be destroyed,'.format(cid)),
changes={name: True})
else:
return _valid(comment="Container {0!r} not found".format(name))

View file

@ -3,27 +3,14 @@
Control the Salt command interface
==================================
The Salt state is used to control the salt command interface. This state is
intended for use primarily from the state runner from the master.
This state is intended for use from the Salt Master. It provides access to
sending commands down to minions as well as access to executing master-side
modules. These state functions wrap Salt's :ref:`Python API <python-api>`.
The salt.state declaration can call out a highstate or a list of sls:
.. seealso:: More Orchestrate documentation
.. code-block:: yaml
webservers:
salt.state:
- tgt: 'web*'
- sls:
- apache
- django
- core
- saltenv: prod
databases:
salt.state:
- tgt: role:database
- tgt_type: grain
- highstate: True
* :ref:`Full Orchestrate Tutorial <orchestrate-tutorial>`
* :py:func:`The Orchestrate runner <salt.runners.state.orchestrate>`
'''
from __future__ import absolute_import
@ -129,6 +116,33 @@ def state(
WARNING: This flag is potentially dangerous. It is designed
for use when multiple state runs can safely be run at the same
Do not use this flag for performance optimization.
Examples:
Run a list of sls files via :py:func:`state.sls <salt.state.sls>` on target
minions:
.. code-block:: yaml
webservers:
salt.state:
- tgt: 'web*'
- sls:
- apache
- django
- core
- saltenv: prod
Run a full :py:func:`state.highstate <salt.state.highstate>` on target
mininons.
.. code-block:: yaml
databases:
salt.state:
- tgt: role:database
- tgt_type: grain
- highstate: True
'''
cmd_kw = {'arg': [], 'kwarg': {}, 'ret': ret, 'timeout': timeout}

View file

@ -2424,7 +2424,7 @@ def import_json():
for fast_json in ('ujson', 'yajl', 'json'):
try:
mod = __import__(fast_json)
log.info('loaded {0} json lib'.format(fast_json))
log.trace('loaded {0} json lib'.format(fast_json))
return mod
except ImportError:
continue

View file

@ -59,7 +59,6 @@ import logging
import time
import datetime
import multiprocessing
from multiprocessing import Process
from collections import MutableMapping
# Import third party libs
@ -527,7 +526,7 @@ class MinionEvent(SaltEvent):
super(MinionEvent, self).__init__('minion', sock_dir=opts.get('sock_dir', None), opts=opts)
class EventPublisher(Process):
class EventPublisher(multiprocessing.Process):
'''
The interface that takes master events and republishes them out to anyone
who wants to listen
@ -725,8 +724,8 @@ class ReactWrap(object):
LowData
'''
l_fun = getattr(self, low['state'])
f_call = salt.utils.format_call(l_fun, low)
try:
f_call = salt.utils.format_call(l_fun, low)
ret = l_fun(*f_call.get('args', ()), **f_call.get('kwargs', {}))
except Exception:
log.error(

View file

@ -9,8 +9,6 @@ from __future__ import absolute_import
import inspect
import logging
from collections import namedtuple
from salt.utils.odict import OrderedDict
import six
@ -274,27 +272,16 @@ class SaltObject(object):
Salt.cmd.run(bar)
'''
def __init__(self, salt):
_mods = {}
for full_func in salt:
mod, func = full_func.split('.')
if mod not in _mods:
_mods[mod] = {}
_mods[mod][func] = salt[full_func]
# now transform using namedtuples
self.mods = {}
for mod in _mods:
mod_name = '{0}Module'.format(str(mod).capitalize())
mod_object = namedtuple(mod_name, _mods[mod])
self.mods[mod] = mod_object(**_mods[mod])
self._salt = salt
def __getattr__(self, mod):
if mod not in self.mods:
raise AttributeError
return self.mods[mod]
class __wrapper__(object):
def __getattr__(wself, func): # pylint: disable=E0213
try:
return self._salt['{0}.{1}'.format(mod, func)]
except KeyError:
raise AttributeError
return __wrapper__()
class MapMeta(type):

View file

@ -217,6 +217,8 @@ def query(key, keyid, method='GET', params=None, headers=None,
if return_url is True:
return ret, requesturl
else:
if method == 'GET' or method == 'HEAD':
return
ret = {'headers': []}
for header in result.headers:
ret['headers'].append(header.strip())

View file

@ -41,6 +41,11 @@ external_auth:
- '@wheel'
- '@runner'
- test.*
saltdev_api:
- '@wheel'
- '@runner'
- test.*
- grains.*
'*':
- '@wheel'
- '@runner'

View file

@ -0,0 +1 @@
# encoding: utf-8

View file

@ -0,0 +1,494 @@
# coding: utf-8
import json
from salt.netapi.rest_tornado import saltnado
import tornado.testing
import tornado.concurrent
import tornado.web
import tornado.ioloop
from unit.netapi.rest_tornado.test_handlers import SaltnadoTestCase
import json
import time
class TestSaltAPIHandler(SaltnadoTestCase):
def get_app(self):
application = tornado.web.Application([('/', saltnado.SaltAPIHandler)], debug=True)
application.auth = self.auth
application.opts = self.opts
application.event_listener = saltnado.EventListener({}, self.opts)
return application
def test_root(self):
'''
Test the root path which returns the list of clients we support
'''
response = self.fetch('/')
assert response.code == 200
response_obj = json.loads(response.body)
assert response_obj['clients'] == ['runner',
'local_async',
'local',
'local_batch']
assert response_obj['return'] == 'Welcome'
def test_post_no_auth(self):
'''
Test post with no auth token, should 401
'''
# get a token for this test
low = [{'client': 'local',
'tgt': '*',
'fun': 'test.ping',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json']},
follow_redirects=False
)
assert response.code == 302
assert response.headers['Location'] == '/login'
# Local client tests
def test_simple_local_post(self):
'''
Test a basic API of /
'''
low = [{'client': 'local',
'tgt': '*',
'fun': 'test.ping',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert response_obj['return'] == [{'minion': True, 'sub_minion': True}]
def test_simple_local_post_no_tgt(self):
'''
POST job with invalid tgt
'''
low = [{'client': 'local',
'tgt': 'minion_we_dont_have',
'fun': 'test.ping',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert response_obj['return'] == ["No minions matched the target. No command was sent, no jid was assigned."]
# local_batch tests
def test_simple_local_batch_post(self):
'''
Basic post against local_batch
'''
low = [{'client': 'local_batch',
'tgt': '*',
'fun': 'test.ping',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert response_obj['return'] == [{'minion': True, 'sub_minion': True}]
# local_batch tests
def test_full_local_batch_post(self):
'''
Test full parallelism of local_batch
'''
low = [{'client': 'local_batch',
'tgt': '*',
'fun': 'test.ping',
'batch': '100%',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert response_obj['return'] == [{'minion': True, 'sub_minion': True}]
def test_simple_local_batch_post_no_tgt(self):
'''
Local_batch testing with no tgt
'''
low = [{'client': 'local_batch',
'tgt': 'minion_we_dont_have',
'fun': 'test.ping',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert response_obj['return'] == [{}]
# local_async tests
def test_simple_local_async_post(self):
low = [{'client': 'local_async',
'tgt': '*',
'fun': 'test.ping',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
# TODO: verify pub function? Maybe look at how we test the publisher
assert len(response_obj['return']) == 1
assert 'jid' in response_obj['return'][0]
assert response_obj['return'][0]['minions'] == ['minion', 'sub_minion']
def test_multi_local_async_post(self):
low = [{'client': 'local_async',
'tgt': '*',
'fun': 'test.ping',
},
{'client': 'local_async',
'tgt': '*',
'fun': 'test.ping',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert len(response_obj['return']) == 2
assert 'jid' in response_obj['return'][0]
assert 'jid' in response_obj['return'][1]
assert response_obj['return'][0]['minions'] == ['minion', 'sub_minion']
assert response_obj['return'][1]['minions'] == ['minion', 'sub_minion']
def test_multi_local_async_post_multitoken(self):
low = [{'client': 'local_async',
'tgt': '*',
'fun': 'test.ping',
},
{'client': 'local_async',
'tgt': '*',
'fun': 'test.ping',
'token': self.token['token'], # send a different (but still valid token)
},
{'client': 'local_async',
'tgt': '*',
'fun': 'test.ping',
'token': 'BAD_TOKEN', # send a bad token
},
]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert len(response_obj['return']) == 3 # make sure we got 3 responses
assert 'jid' in response_obj['return'][0] # the first 2 are regular returns
assert 'jid' in response_obj['return'][1]
assert 'Failed to authenticate' in response_obj['return'][2] # bad auth
assert response_obj['return'][0]['minions'] == ['minion', 'sub_minion']
assert response_obj['return'][1]['minions'] == ['minion', 'sub_minion']
def test_simple_local_async_post_no_tgt(self):
low = [{'client': 'local_async',
'tgt': 'minion_we_dont_have',
'fun': 'test.ping',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert response_obj['return'] == [{}]
# runner tests
def test_simple_local_runner_post(self):
low = [{'client': 'runner',
'fun': 'manage.up',
}]
response = self.fetch('/',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert response_obj['return'] == [['minion', 'sub_minion']]
class TestMinionSaltAPIHandler(SaltnadoTestCase):
def get_app(self):
application = tornado.web.Application([(r"/minions/(.*)", saltnado.MinionSaltAPIHandler),
(r"/minions", saltnado.MinionSaltAPIHandler),
], debug=True)
application.auth = self.auth
application.opts = self.opts
application.event_listener = saltnado.EventListener({}, self.opts)
return application
def test_get_no_mid(self):
response = self.fetch('/minions',
method='GET',
headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
follow_redirects=False,
)
response_obj = json.loads(response.body)
assert len(response_obj['return']) == 1
# one per minion
assert len(response_obj['return'][0]) == 2
# check a single grain
for minion_id, grains in response_obj['return'][0].iteritems():
assert minion_id == grains['id']
def test_get(self):
response = self.fetch('/minions/minion',
method='GET',
headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
follow_redirects=False,
)
response_obj = json.loads(response.body)
assert len(response_obj['return']) == 1
assert len(response_obj['return'][0]) == 1
# check a single grain
assert response_obj['return'][0]['minion']['id'] == 'minion'
def test_post(self):
low = [{'tgt': '*',
'fun': 'test.ping',
}]
response = self.fetch('/minions',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
# TODO: verify pub function? Maybe look at how we test the publisher
assert len(response_obj['return']) == 1
assert 'jid' in response_obj['return'][0]
assert response_obj['return'][0]['minions'] == ['minion', 'sub_minion']
def test_post_with_client(self):
# get a token for this test
low = [{'client': 'local_async',
'tgt': '*',
'fun': 'test.ping',
}]
response = self.fetch('/minions',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
# TODO: verify pub function? Maybe look at how we test the publisher
assert len(response_obj['return']) == 1
assert 'jid' in response_obj['return'][0]
assert response_obj['return'][0]['minions'] == ['minion', 'sub_minion']
def test_post_with_incorrect_client(self):
'''
The /minions endpoint is async only, so if you try something else
make sure you get an error
'''
# get a token for this test
low = [{'client': 'local_batch',
'tgt': '*',
'fun': 'test.ping',
}]
response = self.fetch('/minions',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
assert response.code == 400
class TestJobsSaltAPIHandler(SaltnadoTestCase):
def get_app(self):
application = tornado.web.Application([(r"/jobs/(.*)", saltnado.JobsSaltAPIHandler),
(r"/jobs", saltnado.JobsSaltAPIHandler),
], debug=True)
application.auth = self.auth
application.opts = self.opts
application.event_listener = saltnado.EventListener({}, self.opts)
return application
def test_get(self):
# test with no JID
self.http_client.fetch(self.get_url('/jobs'),
self.stop,
method='GET',
headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
follow_redirects=False,
request_timeout=10, # wait up to 10s for this response-- jenkins seems to be slow
)
response = self.wait(timeout=10)
response_obj = json.loads(response.body)['return'][0]
for jid, ret in response_obj.iteritems():
assert 'Function' in ret
assert 'Target' in ret
assert 'Target-type' in ret
assert 'User' in ret
assert 'StartTime' in ret
assert 'Arguments' in ret
# test with a specific JID passed in
jid = response_obj.iterkeys().next()
self.http_client.fetch(self.get_url('/jobs/{0}'.format(jid)),
self.stop,
method='GET',
headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
follow_redirects=False,
request_timeout=10, # wait up to 10s for this response-- jenkins seems to be slow
)
response = self.wait(timeout=10)
response_obj = json.loads(response.body)['return'][0]
assert 'Function' in response_obj
assert 'Target' in response_obj
assert 'Target-type' in response_obj
assert 'User' in response_obj
assert 'StartTime' in response_obj
assert 'Arguments' in response_obj
assert 'Result' in response_obj
# TODO: run all the same tests from the root handler, but for now since they are
# the same code, we'll just sanity check
class TestRunSaltAPIHandler(SaltnadoTestCase):
def get_app(self):
application = tornado.web.Application([("/run", saltnado.RunSaltAPIHandler),
], debug=True)
application.auth = self.auth
application.opts = self.opts
application.event_listener = saltnado.EventListener({}, self.opts)
return application
def test_get(self):
low = [{'client': 'local',
'tgt': '*',
'fun': 'test.ping',
}]
response = self.fetch('/run',
method='POST',
body=json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert response_obj['return'] == [{'minion': True, 'sub_minion': True}]
class TestEventsSaltAPIHandler(SaltnadoTestCase):
def get_app(self):
application = tornado.web.Application([(r"/events", saltnado.EventsSaltAPIHandler),
], debug=True)
application.auth = self.auth
application.opts = self.opts
application.event_listener = saltnado.EventListener({}, self.opts)
# store a reference, for magic later!
self.application = application
self.events_to_fire = 0
return application
def test_get(self):
self.events_to_fire = 5
response = self.fetch('/events',
headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
streaming_callback=self.on_event
)
def _stop(self):
self.stop()
def on_event(self, event):
if self.events_to_fire > 0:
self.application.event_listener.event.fire_event({
'foo': 'bar',
'baz': 'qux',
}, 'salt/netapi/test')
self.events_to_fire -= 1
# once we've fired all the events, lets call it a day
else:
# wait so that we can ensure that the next future is ready to go
# to make sure we don't explode if the next one is ready
tornado.ioloop.IOLoop.current().add_timeout(time.time() + 0.5, self._stop)
event = event.strip()
# if we got a retry, just continue
if event != 'retry: 400':
tag, data = event.splitlines()
assert tag.startswith('tag: ')
assert data.startswith('data: ')
class TestWebhookSaltAPIHandler(SaltnadoTestCase):
def get_app(self):
application = tornado.web.Application([(r"/hook(/.*)?", saltnado.WebhookSaltAPIHandler),
], debug=True)
application.auth = self.auth
application.opts = self.opts
self.application = application
application.event_listener = saltnado.EventListener({}, self.opts)
return application
def test_post(self):
def verify_event(event):
'''
Verify that the event fired on the master matches what we sent
'''
assert event['tag'] == 'salt/netapi/hook'
assert 'headers' in event['data']
assert event['data']['post'] == {'foo': 'bar'}
# get an event future
event = self.application.event_listener.get_event(self,
tag='salt/netapi/hook',
callback=verify_event,
)
# fire the event
response = self.fetch('/hook',
method='POST',
body='foo=bar',
headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
)
response_obj = json.loads(response.body)
assert response_obj['success'] is True

View file

@ -0,0 +1 @@
# encoding: utf-8

View file

@ -0,0 +1,251 @@
# coding: utf-8
import json
import yaml
import urllib
from salt.netapi.rest_tornado import saltnado
import salt.auth
import integration
import tornado.testing
import tornado.concurrent
class SaltnadoTestCase(integration.ModuleCase, tornado.testing.AsyncHTTPTestCase):
'''
Mixin to hold some shared things
'''
content_type_map = {'json': 'application/json',
'yaml': 'application/x-yaml',
'text': 'text/plain',
'form': 'application/x-www-form-urlencoded'}
auth_creds = (
('username', 'saltdev_api'),
('password', 'saltdev'),
('eauth', 'auto'))
@property
def auth_creds_dict(self):
return dict(self.auth_creds)
@property
def opts(self):
return self.get_config('master', from_scratch=True)
@property
def auth(self):
if not hasattr(self, '__auth'):
self.__auth = salt.auth.LoadAuth(self.opts)
return self.__auth
@property
def token(self):
''' Mint and return a valid token for auth_creds '''
return self.auth.mk_token(self.auth_creds_dict)
class TestBaseSaltAPIHandler(SaltnadoTestCase):
def get_app(self):
class StubHandler(saltnado.BaseSaltAPIHandler):
def get(self):
return self.echo_stuff()
def post(self):
return self.echo_stuff()
def echo_stuff(self):
ret_dict = {'foo': 'bar'}
attrs = ('token',
'start',
'connected',
'lowstate',
)
for attr in attrs:
ret_dict[attr] = getattr(self, attr)
self.write(self.serialize(ret_dict))
return tornado.web.Application([('/', StubHandler)], debug=True)
def test_content_type(self):
'''
Test the base handler's accept picking
'''
# send NO accept header, should come back with json
response = self.fetch('/')
assert response.headers['Content-Type'] == self.content_type_map['json']
assert type(json.loads(response.body)) == dict
# send application/json
response = self.fetch('/', headers={'Accept': self.content_type_map['json']})
assert response.headers['Content-Type'] == self.content_type_map['json']
assert type(json.loads(response.body)) == dict
# send application/x-yaml
response = self.fetch('/', headers={'Accept': self.content_type_map['yaml']})
assert response.headers['Content-Type'] == self.content_type_map['yaml']
assert type(yaml.load(response.body)) == dict
def test_token(self):
'''
Test that the token is returned correctly
'''
token = json.loads(self.fetch('/').body)['token']
assert token is None
# send a token as a header
response = self.fetch('/', headers={saltnado.AUTH_TOKEN_HEADER: 'foo'})
token = json.loads(response.body)['token']
assert token == 'foo'
# send a token as a cookie
response = self.fetch('/', headers={'Cookie': '{0}=foo'.format(saltnado.AUTH_COOKIE_NAME)})
token = json.loads(response.body)['token']
assert token == 'foo'
# send both, make sure its the header
response = self.fetch('/', headers={saltnado.AUTH_TOKEN_HEADER: 'foo',
'Cookie': '{0}=bar'.format(saltnado.AUTH_COOKIE_NAME)})
token = json.loads(response.body)['token']
assert token == 'foo'
def test_deserialize(self):
'''
Send various encoded forms of lowstates (and bad ones) to make sure we
handle deserialization correctly
'''
valid_lowstate = [{
"client": "local",
"tgt": "*",
"fun": "test.fib",
"arg": ["10"]
},
{
"client": "runner",
"fun": "jobs.lookup_jid",
"jid": "20130603122505459265"
}]
# send as JSON
response = self.fetch('/',
method='POST',
body=json.dumps(valid_lowstate),
headers={'Content-Type': self.content_type_map['json']})
assert valid_lowstate == json.loads(response.body)['lowstate']
# send yaml as json (should break)
response = self.fetch('/',
method='POST',
body=yaml.dump(valid_lowstate),
headers={'Content-Type': self.content_type_map['json']})
assert response.code == 400
# send as yaml
response = self.fetch('/',
method='POST',
body=yaml.dump(valid_lowstate),
headers={'Content-Type': self.content_type_map['yaml']})
assert valid_lowstate == json.loads(response.body)['lowstate']
# send json as yaml (works since yaml is a superset of json)
response = self.fetch('/',
method='POST',
body=json.dumps(valid_lowstate),
headers={'Content-Type': self.content_type_map['yaml']})
assert valid_lowstate == json.loads(response.body)['lowstate']
# send json as text/plain
response = self.fetch('/',
method='POST',
body=json.dumps(valid_lowstate),
headers={'Content-Type': self.content_type_map['text']})
assert valid_lowstate == json.loads(response.body)['lowstate']
# send form-urlencoded
form_lowstate = (
('client', 'local'),
('tgt', '*'),
('fun', 'test.fib'),
('arg', '10'),
('arg', 'foo'),
)
response = self.fetch('/',
method='POST',
body=urllib.urlencode(form_lowstate),
headers={'Content-Type': self.content_type_map['form']})
returned_lowstate = json.loads(response.body)['lowstate']
assert len(returned_lowstate) == 1
returned_lowstate = returned_lowstate[0]
assert returned_lowstate['client'] == 'local'
assert returned_lowstate['tgt'] == '*'
assert returned_lowstate['fun'] == 'test.fib'
assert returned_lowstate['arg'] == ['10', 'foo']
class TestSaltAuthHandler(SaltnadoTestCase):
def get_app(self):
# TODO: make a "GET APPPLICATION" func
application = tornado.web.Application([('/login', saltnado.SaltAuthHandler)], debug=True)
application.auth = self.auth
application.opts = self.opts
return application
def test_get(self):
'''
We don't allow gets, so assert we get 401s
'''
response = self.fetch('/login')
assert response.code == 401
def test_login(self):
'''
Test valid logins
'''
response = self.fetch('/login',
method='POST',
body=urllib.urlencode(self.auth_creds),
headers={'Content-Type': self.content_type_map['form']})
response_obj = json.loads(response.body)['return'][0]
assert response_obj['perms'] == self.opts['external_auth']['auto'][self.auth_creds_dict['username']]
assert 'token' in response_obj # TODO: verify that its valid?
assert response_obj['user'] == self.auth_creds_dict['username']
assert response_obj['eauth'] == self.auth_creds_dict['eauth']
def test_login_missing_password(self):
'''
Test logins with bad/missing passwords
'''
bad_creds = []
for key, val in self.auth_creds_dict.iteritems():
if key == 'password':
continue
bad_creds.append((key, val))
response = self.fetch('/login',
method='POST',
body=urllib.urlencode(bad_creds),
headers={'Content-Type': self.content_type_map['form']})
assert response.code == 400
def test_login_bad_creds(self):
'''
Test logins with bad/missing passwords
'''
bad_creds = []
for key, val in self.auth_creds_dict.iteritems():
if key == 'username':
val = val + 'foo'
bad_creds.append((key, val))
response = self.fetch('/login',
method='POST',
body=urllib.urlencode(bad_creds),
headers={'Content-Type': self.content_type_map['form']})
assert response.code == 401

View file

@ -0,0 +1,89 @@
# coding: utf-8
import os
from salt.netapi.rest_tornado import saltnado
import tornado.testing
import tornado.concurrent
from salttesting import TestCase
from unit.utils.event_test import eventpublisher_process, event, SOCK_DIR
class TestUtils(TestCase):
def test_batching(self):
assert 1 == saltnado.get_batch_size('1', 10)
assert 2 == saltnado.get_batch_size('2', 10)
assert 1 == saltnado.get_batch_size('10%', 10)
# TODO: exception in this case? The core doesn't so we shouldn't
assert 11 == saltnado.get_batch_size('110%', 10)
class TestSaltnadoUtils(tornado.testing.AsyncTestCase):
def test_any_future(self):
'''
Test that the Any Future does what we think it does
'''
# create a few futures
futures = []
for x in xrange(0, 3):
future = tornado.concurrent.Future()
future.add_done_callback(self.stop)
futures.append(future)
# create an any future, make sure it isn't immediately done
any_ = saltnado.Any(futures)
assert any_.done() is False
# finish one, lets see who finishes
futures[0].set_result('foo')
self.wait()
assert any_.done() is True
assert futures[0].done() is True
assert futures[1].done() is False
assert futures[2].done() is False
# make sure it returned the one that finished
assert any_.result() == futures[0]
futures = futures[1:]
# re-wait on some other futures
any_ = saltnado.Any(futures)
futures[0].set_result('foo')
self.wait()
assert any_.done() is True
assert futures[0].done() is True
assert futures[1].done() is False
class TestEventListener(tornado.testing.AsyncTestCase):
def setUp(self):
if not os.path.exists(SOCK_DIR):
os.makedirs(SOCK_DIR)
super(TestEventListener, self).setUp()
def test_simple(self):
'''
Test getting a few events
'''
with eventpublisher_process():
me = event.MasterEvent(SOCK_DIR)
event_listener = saltnado.EventListener({}, # we don't use mod_opts, don't save?
{'sock_dir': SOCK_DIR,
'transport': 'zeromq'})
event_future = event_listener.get_event(1, 'evt1', self.stop) # get an event future
me.fire_event({'data': 'foo2'}, 'evt2') # fire an event we don't want
me.fire_event({'data': 'foo1'}, 'evt1') # fire an event we do want
self.wait() # wait for the future
# check that we got the event we wanted
assert event_future.done()
assert event_future.result()['tag'] == 'evt1'
assert event_future.result()['data']['data'] == 'foo1'
if __name__ == '__main__':
from integration import run_tests
run_tests(TestUtils, needs_daemon=False)