Merge pull request #22995 from basepi/merge-forward-develop

Merge forward from 2015.2 to develop
This commit is contained in:
Colton Myers 2015-04-23 14:10:19 -06:00
commit ed6eadceff
10 changed files with 457 additions and 26 deletions

View File

@ -188,6 +188,13 @@ class AuthorizationError(SaltException):
'''
class SaltDaemonNotRunning(SaltException):
'''
Throw when a running master/minion/syndic is not running but is needed to
perform the requested operation (e.g., eauth).
'''
class SaltRunnerError(SaltException):
'''
Problem in runner

View File

@ -3124,7 +3124,9 @@ def get_managed(
source_sum = {}
if template and source:
sfn = __salt__['cp.cache_file'](source, saltenv)
if not os.path.exists(sfn):
# exists doesn't play nice with sfn as bool
# but if cache failed, sfn == False
if not sfn or not os.path.exists(sfn):
return sfn, {}, 'Source file {0} not found'.format(source)
if sfn == name:
raise SaltInvocationError(

View File

@ -36,6 +36,8 @@ SCHEDULE_CONF = [
'splay',
'range',
'when',
'once',
'once_fmt',
'returner',
'jid_include',
'args',
@ -263,7 +265,8 @@ def build_schedule_item(name, **kwargs):
else:
schedule[name]['splay'] = kwargs['splay']
for item in ['range', 'when', 'cron', 'returner', 'return_config', 'until']:
for item in ['range', 'when', 'once', 'once_fmt', 'cron', 'returner',
'return_config', 'until']:
if item in kwargs:
schedule[name][item] = kwargs[item]

View File

@ -16,7 +16,7 @@ import salt.syspaths
import salt.wheel
import salt.utils
import salt.client.ssh.client
from salt.exceptions import SaltException, EauthAuthenticationError
import salt.exceptions
class NetapiClient(object):
@ -31,16 +31,34 @@ class NetapiClient(object):
def __init__(self, opts):
self.opts = opts
def _is_master_running(self):
'''
Perform a lightweight check to see if the master daemon is running
Note, this will return an invalid success if the master crashed or was
not shut down cleanly.
'''
return os.path.exists(os.path.join(
self.opts['sock_dir'],
'workers.ipc'))
def run(self, low):
'''
Execute the specified function in the specified client by passing the
lowstate
'''
# Eauth currently requires a running daemon and commands run through
# this method require eauth so perform a quick check to raise a
# more meaningful error.
if not self._is_master_running():
raise salt.exceptions.SaltDaemonNotRunning(
'Salt Master is not available.')
if 'client' not in low:
raise SaltException('No client specified')
raise salt.exceptions.SaltException('No client specified')
if not ('token' in low or 'eauth' in low) and low['client'] != 'ssh':
raise EauthAuthenticationError(
raise salt.exceptions.EauthAuthenticationError(
'No authentication credentials given')
l_fun = getattr(self, low['client'])

View File

@ -278,6 +278,41 @@ except ImportError:
HAS_WEBSOCKETS = False
def html_override_tool():
'''
Bypass the normal handler and serve HTML for all URLs
The ``app_path`` setting must be non-empty and the request must ask for
``text/html`` in the ``Accept`` header.
'''
apiopts = cherrypy.config['apiopts']
request = cherrypy.request
url_blacklist = (
apiopts.get('app_path', '/app'),
apiopts.get('static_path', '/static'),
)
if 'app' not in cherrypy.config['apiopts']:
return
if request.path_info.startswith(url_blacklist):
return
if request.headers.get('Accept') == '*/*':
return
try:
wants_html = cherrypy.lib.cptools.accept('text/html')
except cherrypy.HTTPError:
return
else:
if wants_html != 'text/html':
return
raise cherrypy.InternalRedirect(apiopts.get('app_path', '/app'))
def salt_token_tool():
'''
If the custom authentication header is supplied, put it in the cookie dict
@ -398,6 +433,9 @@ def hypermedia_handler(*args, **kwargs):
except (salt.exceptions.EauthAuthenticationError,
salt.exceptions.TokenAuthenticationError):
raise cherrypy.HTTPError(401)
except (salt.exceptions.SaltDaemonNotRunning,
salt.exceptions.SaltReqTimeoutError) as exc:
raise cherrypy.HTTPError(503, exc.strerror)
except cherrypy.CherryPyException:
raise
except Exception as exc:
@ -578,6 +616,8 @@ def lowdata_fmt():
cherrypy.serving.request.lowstate = data
cherrypy.tools.html_override = cherrypy.Tool('on_start_resource',
html_override_tool, priority=53)
cherrypy.tools.salt_token = cherrypy.Tool('on_start_resource',
salt_token_tool, priority=55)
cherrypy.tools.salt_auth = cherrypy.Tool('before_request_body',
@ -1365,6 +1405,10 @@ class Login(LowDataAdapter):
]
}}
'''
if not self.api._is_master_running():
raise salt.exceptions.SaltDaemonNotRunning(
'Salt Master is not available.')
# the urlencoded_processor will wrap this in a list
if isinstance(cherrypy.serving.request.lowstate, list):
creds = cherrypy.serving.request.lowstate[0]
@ -2216,10 +2260,17 @@ class API(object):
'tools.cpstats.on': self.apiopts.get('collect_stats', False),
'tools.html_override.on': True,
'tools.cors_tool.on': True,
},
}
if 'favicon' in self.apiopts:
conf['/favicon.ico'] = {
'tools.staticfile.on': True,
'tools.staticfile.filename': self.apiopts['favicon'],
}
if self.apiopts.get('debug', False) is False:
conf['global']['environment'] = 'production'

View File

@ -85,6 +85,18 @@ localtime.
- Thursday 3:00pm
- Friday 5:00pm
This will schedule a job to run once on the specified date. The default date
format is ISO 8601 but can be overridden by also specifying the ``once_fmt``
option.
.. code-block:: yaml
schedule:
job1:
function: test.ping
once: 2015-04-22T20:21:00
once_fmt: '%Y-%m-%dT%H:%M:%S'
This will schedule the command: state.sls httpd test=True at 5pm on Monday,
Wednesday and Friday, and 3pm on Tuesday and Thursday.
@ -230,6 +242,7 @@ from __future__ import absolute_import
import os
import time
import datetime
import itertools
import multiprocessing
import threading
import sys
@ -650,7 +663,6 @@ class Schedule(object):
seconds = 0
cron = 0
now = int(time.time())
time_conflict = False
if 'until' in data:
if not _WHEN_SUPPORTED:
@ -665,30 +677,54 @@ class Schedule(object):
'skipping job: {0}.'.format(data['name']))
continue
for item in ['seconds', 'minutes', 'hours', 'days']:
if item in data and 'when' in data:
time_conflict = True
if item in data and 'cron' in data:
time_conflict = True
# Used for quick lookups when detecting invalid option combinations.
schedule_keys = set(data.keys())
if time_conflict:
log.error('Unable to use "seconds", "minutes",'
'"hours", or "days" with '
'"when" or "cron" options. Ignoring.')
time_elements = ('seconds', 'minutes', 'hours', 'days')
scheduling_elements = ('when', 'cron', 'once')
invalid_sched_combos = [set(i)
for i in itertools.combinations(scheduling_elements, 2)]
if any(i <= schedule_keys for i in invalid_sched_combos):
log.error('Unable to use "{0}" options together. Ignoring.'
.format('", "'.join(scheduling_elements)))
continue
if 'when' in data and 'cron' in data:
log.error('Unable to use "when" and "cron" options together.'
'Ignoring.')
invalid_time_combos = []
for item in scheduling_elements:
all_items = itertools.chain([item], time_elements)
invalid_time_combos.append(
set(itertools.combinations(all_items, 2)))
if any(set(x) <= schedule_keys for x in invalid_time_combos):
log.error('Unable to use "{0}" with "{1}" options. Ignoring'
.format('", "'.join(time_elements),
'", "'.join(scheduling_elements)))
continue
time_elements = ['seconds', 'minutes', 'hours', 'days']
if True in [True for item in time_elements if item in data]:
# Add up how many seconds between now and then
seconds += int(data.get('seconds', 0))
seconds += int(data.get('minutes', 0)) * 60
seconds += int(data.get('hours', 0)) * 3600
seconds += int(data.get('days', 0)) * 86400
elif 'once' in data:
once_fmt = data.get('once_fmt', '%Y-%m-%dT%H:%M:%S')
try:
once = datetime.datetime.strptime(data['once'], once_fmt)
once = int(time.mktime(once.timetuple()))
except (TypeError, ValueError):
log.error('Date string could not be parsed: %s, %s',
data['once'], once_fmt)
continue
if now != once:
continue
else:
seconds = 1
elif 'when' in data:
if not _WHEN_SUPPORTED:
log.error('Missing python-dateutil.'

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
'''
:codeauthor: :email:`Rupesh Tare <rupesht@saltstack.com>`
'''
# Import Python Libs
from __future__ import absolute_import
# Import Salt Testing Libs
from salttesting import TestCase, skipIf
from salttesting.mock import (
MagicMock,
patch,
NO_MOCK,
NO_MOCK_REASON
)
from salttesting.helpers import ensure_in_syspath
ensure_in_syspath('../../')
# Import Salt Libs
from salt.modules import qemu_img
import os
# Globals
qemu_img.__salt__ = {}
@skipIf(NO_MOCK, NO_MOCK_REASON)
class QemuimgTestCase(TestCase):
'''
Test cases for salt.modules.qemu_img
'''
def test_make_image(self):
'''
Test for create a blank virtual machine image file
of the specified size in megabytes
'''
with patch.object(os.path, 'isabs',
MagicMock(side_effect=[False, True, True, True])):
self.assertEqual(qemu_img.make_image('location', 'size', 'fmt'), '')
with patch.object(os.path, 'isdir',
MagicMock(side_effect=[False, True, True])):
self.assertEqual(qemu_img.make_image('location', 'size', 'fmt'),
'')
with patch.dict(qemu_img.__salt__,
{'cmd.retcode': MagicMock(side_effect=[False,
True])}):
self.assertEqual(qemu_img.make_image('location', 'size',
'fmt'), 'location')
self.assertEqual(qemu_img.make_image('location', 'size',
'fmt'), '')
if __name__ == '__main__':
from integration import run_tests
run_tests(QemuimgTestCase, needs_daemon=False)

View File

@ -39,28 +39,34 @@ class ApacheTestCase(TestCase):
Test to allows for inputting a yaml dictionary into a file
for apache configuration files.
'''
name = 'yaml'
name = '/etc/distro/specific/apache.conf'
config = 'VirtualHost: this: "*:80"'
new_config = 'LiteralHost: that: "*:79"'
ret = {'name': name,
'result': True,
'changes': {},
'comment': ''}
mock = MagicMock(side_effect=[config, '', ''])
with patch.object(salt.utils, 'fopen', mock_open(read_data=config)):
with patch.dict(apache.__salt__,
{'apache.config': mock}):
mock_config = MagicMock(return_value=config)
with patch.dict(apache.__salt__, {'apache.config': mock_config}):
ret.update({'comment': 'Configuration is up to date.'})
self.assertDictEqual(apache.configfile(name, config), ret)
with patch.object(salt.utils, 'fopen', mock_open(read_data=config)):
mock_config = MagicMock(return_value=new_config)
with patch.dict(apache.__salt__, {'apache.config': mock_config}):
ret.update({'comment': 'Configuration will update.',
'changes': {'new': '',
'old': 'VirtualHost: this: "*:80"'},
'changes': {'new': new_config,
'old': config},
'result': None})
with patch.dict(apache.__opts__, {'test': True}):
self.assertDictEqual(apache.configfile(name, config), ret)
self.assertDictEqual(apache.configfile(name, new_config), ret)
with patch.object(salt.utils, 'fopen', mock_open(read_data=config)):
mock_config = MagicMock(return_value=new_config)
with patch.dict(apache.__salt__, {'apache.config': mock_config}):
ret.update({'comment': 'Successfully created configuration.',
'result': True})
with patch.dict(apache.__opts__, {'test': False}):

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
'''
:codeauthor: :email:`Jayesh Kariya <jayeshk@saltstack.com>`
'''
# Import Python libs
from __future__ import absolute_import
# Import Salt Testing Libs
from salttesting import skipIf, TestCase
from salttesting.mock import (
NO_MOCK,
NO_MOCK_REASON,
MagicMock,
patch)
from salttesting.helpers import ensure_in_syspath
ensure_in_syspath('../../')
# Import Salt Libs
from salt.states import boto_elasticache
boto_elasticache.__salt__ = {}
boto_elasticache.__opts__ = {}
@skipIf(NO_MOCK, NO_MOCK_REASON)
class BotoElasticacheTestCase(TestCase):
'''
Test cases for salt.states.boto_elasticache
'''
# 'present' function tests: 1
def test_present(self):
'''
Test to ensure the cache cluster exists.
'''
name = 'myelasticache'
engine = 'redis'
cache_node_type = 'cache.t1.micro'
ret = {'name': name,
'result': None,
'changes': {},
'comment': ''}
mock = MagicMock(side_effect=[None, False, False, True])
mock_bool = MagicMock(return_value=False)
with patch.dict(boto_elasticache.__salt__,
{'boto_elasticache.get_config': mock,
'boto_elasticache.create': mock_bool}):
comt = ('Failed to retrieve cache cluster info from AWS.')
ret.update({'comment': comt})
self.assertDictEqual(boto_elasticache.present(name, engine,
cache_node_type), ret)
with patch.dict(boto_elasticache.__opts__, {'test': True}):
comt = ('Cache cluster {0} is set to be created.'.format(name))
ret.update({'comment': comt})
self.assertDictEqual(boto_elasticache.present(name, engine,
cache_node_type),
ret)
with patch.dict(boto_elasticache.__opts__, {'test': False}):
comt = ('Failed to create {0} cache cluster.'.format(name))
ret.update({'comment': comt, 'result': False})
self.assertDictEqual(boto_elasticache.present(name, engine,
cache_node_type),
ret)
comt = ('Cache cluster {0} is present.'.format(name))
ret.update({'comment': comt, 'result': True})
self.assertDictEqual(boto_elasticache.present(name, engine,
cache_node_type),
ret)
# 'absent' function tests: 1
def test_absent(self):
'''
Test to ensure the named elasticache cluster is deleted.
'''
name = 'new_table'
ret = {'name': name,
'result': True,
'changes': {},
'comment': ''}
mock = MagicMock(side_effect=[False, True])
with patch.dict(boto_elasticache.__salt__,
{'boto_elasticache.exists': mock}):
comt = ('{0} does not exist in None.'.format(name))
ret.update({'comment': comt})
self.assertDictEqual(boto_elasticache.absent(name), ret)
with patch.dict(boto_elasticache.__opts__, {'test': True}):
comt = ('Cache cluster {0} is set to be removed.'.format(name))
ret.update({'comment': comt, 'result': None})
self.assertDictEqual(boto_elasticache.absent(name), ret)
if __name__ == '__main__':
from integration import run_tests
run_tests(BotoElasticacheTestCase, needs_daemon=False)

View File

@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
'''
:codeauthor: :email:`Jayesh Kariya <jayeshk@saltstack.com>`
'''
# Import Python libs
from __future__ import absolute_import
import copy
# Import Salt Testing Libs
from salttesting import skipIf, TestCase
from salttesting.mock import (
NO_MOCK,
NO_MOCK_REASON,
MagicMock,
patch)
from salttesting.helpers import ensure_in_syspath
ensure_in_syspath('../../')
# Import Salt Libs
from salt.states import boto_elb
boto_elb.__salt__ = {}
boto_elb.__opts__ = {}
@skipIf(NO_MOCK, NO_MOCK_REASON)
class BotoElbTestCase(TestCase):
'''
Test cases for salt.states.boto_elb
'''
# 'present' function tests: 1
def test_present(self):
'''
Test to ensure the IAM role exists.
'''
name = 'myelb'
listeners = [{'elb_port': 'ELBPORT', 'instance_port': 'PORT',
'elb_protocol': 'HTTPS', 'certificate': 'A'}]
attributes = {'alarm_actions': ['arn:aws:sns:us-east-1:12345:myalarm'],
'insufficient_data_actions': [],
'ok_actions': ['arn:aws:sns:us-east-1:12345:myalarm']}
avail_zones = ['us-east-1a', 'us-east-1c', 'us-east-1d']
alarms = {'alarm_actions': {'name': name,
'attributes': {'description': 'A'}}}
ret = {'name': name,
'result': False,
'changes': {},
'comment': ''}
ret1 = copy.deepcopy(ret)
mock = MagicMock(return_value={})
mock_bool = MagicMock(return_value=False)
with patch.dict(boto_elb.__salt__,
{'config.option': mock,
'boto_elb.exists': mock_bool,
'boto_elb.create': mock_bool,
'boto_elb.get_attributes': mock}):
with patch.dict(boto_elb.__opts__, {'test': False}):
comt = (' Failed to create myelb ELB.')
ret.update({'comment': comt})
self.assertDictEqual(boto_elb.present
(name, listeners, attributes=attributes,
availability_zones=avail_zones), ret)
mock = MagicMock(return_value={})
mock_ret = MagicMock(return_value={'result': {'result': False}})
comt1 = (' Failed to retrieve health_check for ELB myelb.')
with patch.dict(boto_elb.__salt__,
{'config.option': mock,
'boto_elb.get_attributes': mock,
'boto_elb.get_health_check': mock,
'boto_elb.get_elb_config': mock,
'state.single': mock_ret}):
with patch.dict(boto_elb.__opts__, {'test': False}):
ret1.update({'result': True})
mock_elb_present = MagicMock(return_value=ret1)
with patch.object(boto_elb, '_elb_present', mock_elb_present):
comt = (' Failed to retrieve attributes for ELB myelb.')
ret.update({'comment': comt})
self.assertDictEqual(boto_elb.present
(name, listeners), ret)
with patch.object(boto_elb, '_attributes_present',
mock_elb_present):
ret.update({'comment': comt1})
self.assertDictEqual(boto_elb.present
(name, listeners), ret)
with patch.object(boto_elb, '_health_check_present',
mock_elb_present):
comt = (' Failed to retrieve ELB myelb.')
ret.update({'comment': comt})
self.assertDictEqual(boto_elb.present
(name, listeners), ret)
with patch.object(boto_elb, '_cnames_present',
mock_elb_present):
comt = (' ')
ret.update({'comment': comt})
self.assertDictEqual(boto_elb.present
(name, listeners,
alarms=alarms), ret)
with patch.object(boto_elb, '_alarms_present',
mock_elb_present):
ret.update({'result': True})
self.assertDictEqual(boto_elb.present
(name, listeners,
alarms=alarms), ret)
# 'absent' function tests: 1
def test_absent(self):
'''
Test to ensure the IAM role is deleted.
'''
name = 'new_table'
ret = {'name': name,
'result': True,
'changes': {},
'comment': ''}
mock = MagicMock(side_effect=[False, True])
with patch.dict(boto_elb.__salt__, {'boto_elb.exists': mock}):
comt = ('{0} ELB does not exist.'.format(name))
ret.update({'comment': comt})
self.assertDictEqual(boto_elb.absent(name), ret)
with patch.dict(boto_elb.__opts__, {'test': True}):
comt = ('ELB {0} is set to be removed.'.format(name))
ret.update({'comment': comt, 'result': None})
self.assertDictEqual(boto_elb.absent(name), ret)
if __name__ == '__main__':
from integration import run_tests
run_tests(BotoElbTestCase, needs_daemon=False)