mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
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:
commit
716a7e3331
32 changed files with 2217 additions and 475 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -19,6 +19,7 @@ MANIFEST
|
|||
bin/
|
||||
include/
|
||||
lib/
|
||||
lib64
|
||||
pip/
|
||||
share/
|
||||
tests/integration/tmp/
|
||||
|
|
12
doc/faq.rst
12
doc/faq.rst
|
@ -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
|
||||
*******
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
==========
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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:':
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -41,6 +41,11 @@ external_auth:
|
|||
- '@wheel'
|
||||
- '@runner'
|
||||
- test.*
|
||||
saltdev_api:
|
||||
- '@wheel'
|
||||
- '@runner'
|
||||
- test.*
|
||||
- grains.*
|
||||
'*':
|
||||
- '@wheel'
|
||||
- '@runner'
|
||||
|
|
1
tests/integration/netapi/rest_tornado/__init__.py
Normal file
1
tests/integration/netapi/rest_tornado/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# encoding: utf-8
|
494
tests/integration/netapi/rest_tornado/test_app.py
Normal file
494
tests/integration/netapi/rest_tornado/test_app.py
Normal 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
|
1
tests/unit/netapi/rest_tornado/__init__.py
Normal file
1
tests/unit/netapi/rest_tornado/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# encoding: utf-8
|
251
tests/unit/netapi/rest_tornado/test_handlers.py
Normal file
251
tests/unit/netapi/rest_tornado/test_handlers.py
Normal 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
|
89
tests/unit/netapi/rest_tornado/test_utils.py
Normal file
89
tests/unit/netapi/rest_tornado/test_utils.py
Normal 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)
|
Loading…
Add table
Reference in a new issue