2013-12-26 16:40:24 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
'''
|
2018-05-28 21:13:12 +00:00
|
|
|
:codeauthor: Mike Place <mp@saltstack.com>
|
2013-12-26 16:40:24 +00:00
|
|
|
'''
|
|
|
|
|
2014-05-22 16:59:26 +00:00
|
|
|
# Import python libs
|
2014-11-21 19:05:13 +00:00
|
|
|
from __future__ import absolute_import
|
2016-09-07 20:13:59 +00:00
|
|
|
import copy
|
2014-05-22 16:59:26 +00:00
|
|
|
import os
|
|
|
|
|
2013-12-26 16:40:24 +00:00
|
|
|
# Import Salt Testing libs
|
2017-02-27 13:58:07 +00:00
|
|
|
from tests.support.unit import TestCase, skipIf
|
|
|
|
from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock
|
2018-04-20 16:02:52 +00:00
|
|
|
from tests.support.mixins import AdaptedConfigurationTestCaseMixin
|
2017-04-04 17:57:27 +00:00
|
|
|
from tests.support.helpers import skip_if_not_root
|
2014-05-22 16:59:26 +00:00
|
|
|
# Import salt libs
|
2018-01-24 21:36:05 +00:00
|
|
|
import salt.minion
|
Use explicit unicode strings + break up salt.utils
This PR is part of what will be an ongoing effort to use explicit
unicode strings in Salt. Because Python 3 does not suport Python 2's raw
unicode string syntax (i.e. `ur'\d+'`), we must use
`salt.utils.locales.sdecode()` to ensure that the raw string is unicode.
However, because of how `salt/utils/__init__.py` has evolved into the
hulking monstrosity it is today, this means importing a large module in
places where it is not needed, which could negatively impact
performance. For this reason, this PR also breaks out some of the
functions from `salt/utils/__init__.py` into new/existing modules under
`salt/utils/`. The long term goal will be that the modules within this
directory do not depend on importing `salt.utils`.
A summary of the changes in this PR is as follows:
* Moves the following functions from `salt.utils` to new locations
(including a deprecation warning if invoked from `salt.utils`):
`to_bytes`, `to_str`, `to_unicode`, `str_to_num`, `is_quoted`,
`dequote`, `is_hex`, `is_bin_str`, `rand_string`,
`contains_whitespace`, `clean_kwargs`, `invalid_kwargs`, `which`,
`which_bin`, `path_join`, `shlex_split`, `rand_str`, `is_windows`,
`is_proxy`, `is_linux`, `is_darwin`, `is_sunos`, `is_smartos`,
`is_smartos_globalzone`, `is_smartos_zone`, `is_freebsd`, `is_netbsd`,
`is_openbsd`, `is_aix`
* Moves the functions already deprecated by @rallytime to the bottom of
`salt/utils/__init__.py` for better organization, so we can keep the
deprecated ones separate from the ones yet to be deprecated as we
continue to break up `salt.utils`
* Updates `salt/*.py` and all files under `salt/client/` to use explicit
unicode string literals.
* Gets rid of implicit imports of `salt.utils` (e.g. `from salt.utils
import foo` becomes `import salt.utils.foo as foo`).
* Renames the `test.rand_str` function to `test.random_hash` to more
accurately reflect what it does
* Modifies `salt.utils.stringutils.random()` (née `salt.utils.rand_string()`)
such that it returns a string matching the passed size. Previously
this function would get `size` bytes from `os.urandom()`,
base64-encode it, and return the result, which would in most cases not
be equal to the passed size.
2017-07-25 01:47:15 +00:00
|
|
|
import salt.utils.event as event
|
2018-09-25 00:50:23 +00:00
|
|
|
from salt.exceptions import SaltSystemExit, SaltMasterUnresolvableError
|
2014-05-22 16:59:26 +00:00
|
|
|
import salt.syspaths
|
2016-09-15 11:11:12 +00:00
|
|
|
import tornado
|
2018-08-22 21:46:34 +00:00
|
|
|
import tornado.testing
|
2017-09-21 08:00:00 +00:00
|
|
|
from salt.ext.six.moves import range
|
2013-12-26 16:40:24 +00:00
|
|
|
|
2018-08-22 21:46:34 +00:00
|
|
|
|
2013-12-26 16:40:24 +00:00
|
|
|
__opts__ = {}
|
|
|
|
|
|
|
|
|
2013-12-31 13:53:37 +00:00
|
|
|
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
2018-04-20 16:02:52 +00:00
|
|
|
class MinionTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
|
2013-12-26 16:40:24 +00:00
|
|
|
def test_invalid_master_address(self):
|
|
|
|
with patch.dict(__opts__, {'ipv6': False, 'master': float('127.0'), 'master_port': '4555', 'retry_dns': False}):
|
2018-01-24 21:36:05 +00:00
|
|
|
self.assertRaises(SaltSystemExit, salt.minion.resolve_dns, __opts__)
|
2014-03-31 02:03:40 +00:00
|
|
|
|
2018-03-13 20:27:02 +00:00
|
|
|
def test_source_int_name_local(self):
|
|
|
|
'''
|
|
|
|
test when file_client local and
|
|
|
|
source_interface_name is set
|
|
|
|
'''
|
|
|
|
interfaces = {'bond0.1234': {'hwaddr': '01:01:01:d0:d0:d0',
|
|
|
|
'up': True, 'inet':
|
|
|
|
[{'broadcast': '111.1.111.255',
|
|
|
|
'netmask': '111.1.0.0',
|
|
|
|
'label': 'bond0',
|
|
|
|
'address': '111.1.0.1'}]}}
|
|
|
|
with patch.dict(__opts__, {'ipv6': False, 'master': '127.0.0.1',
|
|
|
|
'master_port': '4555', 'file_client': 'local',
|
|
|
|
'source_interface_name': 'bond0.1234',
|
|
|
|
'source_ret_port': 49017,
|
|
|
|
'source_publish_port': 49018}), \
|
|
|
|
patch('salt.utils.network.interfaces',
|
|
|
|
MagicMock(return_value=interfaces)):
|
|
|
|
assert salt.minion.resolve_dns(__opts__) == {'master_ip': '127.0.0.1',
|
|
|
|
'source_ip': '111.1.0.1',
|
|
|
|
'source_ret_port': 49017,
|
|
|
|
'source_publish_port': 49018,
|
|
|
|
'master_uri': 'tcp://127.0.0.1:4555'}
|
|
|
|
|
|
|
|
def test_source_int_name_remote(self):
|
|
|
|
'''
|
|
|
|
test when file_client remote and
|
|
|
|
source_interface_name is set and
|
|
|
|
interface is down
|
|
|
|
'''
|
|
|
|
interfaces = {'bond0.1234': {'hwaddr': '01:01:01:d0:d0:d0',
|
|
|
|
'up': False, 'inet':
|
|
|
|
[{'broadcast': '111.1.111.255',
|
|
|
|
'netmask': '111.1.0.0',
|
|
|
|
'label': 'bond0',
|
|
|
|
'address': '111.1.0.1'}]}}
|
|
|
|
with patch.dict(__opts__, {'ipv6': False, 'master': '127.0.0.1',
|
|
|
|
'master_port': '4555', 'file_client': 'remote',
|
|
|
|
'source_interface_name': 'bond0.1234',
|
|
|
|
'source_ret_port': 49017,
|
|
|
|
'source_publish_port': 49018}), \
|
|
|
|
patch('salt.utils.network.interfaces',
|
|
|
|
MagicMock(return_value=interfaces)):
|
|
|
|
assert salt.minion.resolve_dns(__opts__) == {'master_ip': '127.0.0.1',
|
|
|
|
'source_ret_port': 49017,
|
|
|
|
'source_publish_port': 49018,
|
|
|
|
'master_uri': 'tcp://127.0.0.1:4555'}
|
|
|
|
|
|
|
|
def test_source_address(self):
|
|
|
|
'''
|
|
|
|
test when source_address is set
|
|
|
|
'''
|
|
|
|
interfaces = {'bond0.1234': {'hwaddr': '01:01:01:d0:d0:d0',
|
|
|
|
'up': False, 'inet':
|
|
|
|
[{'broadcast': '111.1.111.255',
|
|
|
|
'netmask': '111.1.0.0',
|
|
|
|
'label': 'bond0',
|
|
|
|
'address': '111.1.0.1'}]}}
|
|
|
|
with patch.dict(__opts__, {'ipv6': False, 'master': '127.0.0.1',
|
|
|
|
'master_port': '4555', 'file_client': 'local',
|
|
|
|
'source_interface_name': '',
|
|
|
|
'source_address': '111.1.0.1',
|
|
|
|
'source_ret_port': 49017,
|
|
|
|
'source_publish_port': 49018}), \
|
|
|
|
patch('salt.utils.network.interfaces',
|
|
|
|
MagicMock(return_value=interfaces)):
|
|
|
|
assert salt.minion.resolve_dns(__opts__) == {'source_publish_port': 49018,
|
|
|
|
'source_ret_port': 49017,
|
|
|
|
'master_uri': 'tcp://127.0.0.1:4555',
|
|
|
|
'source_ip': '111.1.0.1',
|
|
|
|
'master_ip': '127.0.0.1'}
|
|
|
|
|
2016-09-07 20:13:59 +00:00
|
|
|
# Tests for _handle_decoded_payload in the salt.minion.Minion() class: 3
|
|
|
|
|
|
|
|
def test_handle_decoded_payload_jid_match_in_jid_queue(self):
|
|
|
|
'''
|
|
|
|
Tests that the _handle_decoded_payload function returns when a jid is given that is already present
|
|
|
|
in the jid_queue.
|
|
|
|
|
|
|
|
Note: This test doesn't contain all of the patch decorators above the function like the other tests
|
|
|
|
for _handle_decoded_payload below. This is essential to this test as the call to the function must
|
|
|
|
return None BEFORE any of the processes are spun up because we should be avoiding firing duplicate
|
|
|
|
jobs.
|
|
|
|
'''
|
2017-04-11 15:28:19 +00:00
|
|
|
mock_opts = salt.config.DEFAULT_MINION_OPTS
|
2016-09-07 20:13:59 +00:00
|
|
|
mock_data = {'fun': 'foo.bar',
|
|
|
|
'jid': 123}
|
|
|
|
mock_jid_queue = [123]
|
2018-01-24 21:36:05 +00:00
|
|
|
minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop())
|
2016-09-15 08:51:00 +00:00
|
|
|
try:
|
2017-09-20 14:53:09 +00:00
|
|
|
ret = minion._handle_decoded_payload(mock_data).result()
|
2016-09-15 08:51:00 +00:00
|
|
|
self.assertEqual(minion.jid_queue, mock_jid_queue)
|
|
|
|
self.assertIsNone(ret)
|
|
|
|
finally:
|
|
|
|
minion.destroy()
|
2016-09-07 20:13:59 +00:00
|
|
|
|
|
|
|
def test_handle_decoded_payload_jid_queue_addition(self):
|
|
|
|
'''
|
|
|
|
Tests that the _handle_decoded_payload function adds a jid to the minion's jid_queue when the new
|
|
|
|
jid isn't already present in the jid_queue.
|
|
|
|
'''
|
2017-04-10 13:00:57 +00:00
|
|
|
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)):
|
|
|
|
mock_jid = 11111
|
|
|
|
mock_opts = salt.config.DEFAULT_MINION_OPTS
|
|
|
|
mock_data = {'fun': 'foo.bar',
|
|
|
|
'jid': mock_jid}
|
|
|
|
mock_jid_queue = [123, 456]
|
2018-01-24 21:36:05 +00:00
|
|
|
minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop())
|
2017-04-10 13:00:57 +00:00
|
|
|
try:
|
2016-09-15 08:51:00 +00:00
|
|
|
|
2017-04-10 13:00:57 +00:00
|
|
|
# Assert that the minion's jid_queue attribute matches the mock_jid_queue as a baseline
|
|
|
|
# This can help debug any test failures if the _handle_decoded_payload call fails.
|
|
|
|
self.assertEqual(minion.jid_queue, mock_jid_queue)
|
2016-09-15 08:51:00 +00:00
|
|
|
|
2017-04-10 13:00:57 +00:00
|
|
|
# Call the _handle_decoded_payload function and update the mock_jid_queue to include the new
|
|
|
|
# mock_jid. The mock_jid should have been added to the jid_queue since the mock_jid wasn't
|
|
|
|
# previously included. The minion's jid_queue attribute and the mock_jid_queue should be equal.
|
2017-09-20 14:53:09 +00:00
|
|
|
minion._handle_decoded_payload(mock_data).result()
|
2017-04-10 13:00:57 +00:00
|
|
|
mock_jid_queue.append(mock_jid)
|
|
|
|
self.assertEqual(minion.jid_queue, mock_jid_queue)
|
|
|
|
finally:
|
|
|
|
minion.destroy()
|
2016-09-07 20:13:59 +00:00
|
|
|
|
|
|
|
def test_handle_decoded_payload_jid_queue_reduced_minion_jid_queue_hwm(self):
|
|
|
|
'''
|
|
|
|
Tests that the _handle_decoded_payload function removes a jid from the minion's jid_queue when the
|
|
|
|
minion's jid_queue high water mark (minion_jid_queue_hwm) is hit.
|
|
|
|
'''
|
2017-04-10 13:00:57 +00:00
|
|
|
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)):
|
|
|
|
mock_opts = salt.config.DEFAULT_MINION_OPTS
|
|
|
|
mock_opts['minion_jid_queue_hwm'] = 2
|
|
|
|
mock_data = {'fun': 'foo.bar',
|
|
|
|
'jid': 789}
|
|
|
|
mock_jid_queue = [123, 456]
|
2018-01-24 21:36:05 +00:00
|
|
|
minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop())
|
2017-04-10 13:00:57 +00:00
|
|
|
try:
|
|
|
|
|
|
|
|
# Assert that the minion's jid_queue attribute matches the mock_jid_queue as a baseline
|
|
|
|
# This can help debug any test failures if the _handle_decoded_payload call fails.
|
|
|
|
self.assertEqual(minion.jid_queue, mock_jid_queue)
|
|
|
|
|
|
|
|
# Call the _handle_decoded_payload function and check that the queue is smaller by one item
|
|
|
|
# and contains the new jid
|
2017-09-20 14:53:09 +00:00
|
|
|
minion._handle_decoded_payload(mock_data).result()
|
2017-04-10 13:00:57 +00:00
|
|
|
self.assertEqual(len(minion.jid_queue), 2)
|
|
|
|
self.assertEqual(minion.jid_queue, [456, 789])
|
|
|
|
finally:
|
|
|
|
minion.destroy()
|
2017-09-21 08:00:00 +00:00
|
|
|
|
|
|
|
def test_process_count_max(self):
|
|
|
|
'''
|
|
|
|
Tests that the _handle_decoded_payload function does not spawn more than the configured amount of processes,
|
|
|
|
as per process_count_max.
|
|
|
|
'''
|
|
|
|
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)), \
|
|
|
|
patch('salt.utils.minion.running', MagicMock(return_value=[])), \
|
|
|
|
patch('tornado.gen.sleep', MagicMock(return_value=tornado.concurrent.Future())):
|
|
|
|
process_count_max = 10
|
|
|
|
mock_opts = salt.config.DEFAULT_MINION_OPTS
|
|
|
|
mock_opts['minion_jid_queue_hwm'] = 100
|
|
|
|
mock_opts["process_count_max"] = process_count_max
|
|
|
|
|
2018-01-24 21:36:05 +00:00
|
|
|
io_loop = tornado.ioloop.IOLoop()
|
|
|
|
minion = salt.minion.Minion(mock_opts, jid_queue=[], io_loop=io_loop)
|
2017-09-21 08:00:00 +00:00
|
|
|
try:
|
|
|
|
|
|
|
|
# mock gen.sleep to throw a special Exception when called, so that we detect it
|
|
|
|
class SleepCalledEception(Exception):
|
|
|
|
"""Thrown when sleep is called"""
|
|
|
|
pass
|
|
|
|
tornado.gen.sleep.return_value.set_exception(SleepCalledEception())
|
|
|
|
|
|
|
|
# up until process_count_max: gen.sleep does not get called, processes are started normally
|
|
|
|
for i in range(process_count_max):
|
|
|
|
mock_data = {'fun': 'foo.bar',
|
|
|
|
'jid': i}
|
|
|
|
io_loop.run_sync(lambda data=mock_data: minion._handle_decoded_payload(data))
|
|
|
|
self.assertEqual(salt.utils.process.SignalHandlingMultiprocessingProcess.start.call_count, i + 1)
|
|
|
|
self.assertEqual(len(minion.jid_queue), i + 1)
|
|
|
|
salt.utils.minion.running.return_value += [i]
|
|
|
|
|
|
|
|
# above process_count_max: gen.sleep does get called, JIDs are created but no new processes are started
|
|
|
|
mock_data = {'fun': 'foo.bar',
|
|
|
|
'jid': process_count_max + 1}
|
|
|
|
|
|
|
|
self.assertRaises(SleepCalledEception,
|
|
|
|
lambda: io_loop.run_sync(lambda: minion._handle_decoded_payload(mock_data)))
|
|
|
|
self.assertEqual(salt.utils.process.SignalHandlingMultiprocessingProcess.start.call_count,
|
|
|
|
process_count_max)
|
|
|
|
self.assertEqual(len(minion.jid_queue), process_count_max + 1)
|
|
|
|
finally:
|
|
|
|
minion.destroy()
|
2017-12-11 23:24:41 +00:00
|
|
|
|
2017-11-10 22:03:24 +00:00
|
|
|
def test_beacons_before_connect(self):
|
|
|
|
'''
|
|
|
|
Tests that the 'beacons_before_connect' option causes the beacons to be initialized before connect.
|
|
|
|
'''
|
|
|
|
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
|
|
|
|
patch('salt.minion.Minion.sync_connect_master', MagicMock(side_effect=RuntimeError('stop execution'))), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)):
|
2018-04-20 16:02:52 +00:00
|
|
|
mock_opts = self.get_config('minion', from_scratch=True)
|
2017-11-10 22:03:24 +00:00
|
|
|
mock_opts['beacons_before_connect'] = True
|
2018-05-22 19:11:58 +00:00
|
|
|
io_loop = tornado.ioloop.IOLoop()
|
|
|
|
io_loop.make_current()
|
|
|
|
minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
|
2017-11-10 22:03:24 +00:00
|
|
|
try:
|
|
|
|
|
|
|
|
try:
|
|
|
|
minion.tune_in(start=True)
|
|
|
|
except RuntimeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Make sure beacons are initialized but the sheduler is not
|
|
|
|
self.assertTrue('beacons' in minion.periodic_callbacks)
|
|
|
|
self.assertTrue('schedule' not in minion.periodic_callbacks)
|
|
|
|
finally:
|
|
|
|
minion.destroy()
|
|
|
|
|
|
|
|
def test_scheduler_before_connect(self):
|
|
|
|
'''
|
|
|
|
Tests that the 'scheduler_before_connect' option causes the scheduler to be initialized before connect.
|
|
|
|
'''
|
|
|
|
with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
|
|
|
|
patch('salt.minion.Minion.sync_connect_master', MagicMock(side_effect=RuntimeError('stop execution'))), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \
|
|
|
|
patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)):
|
2018-04-20 16:02:52 +00:00
|
|
|
mock_opts = self.get_config('minion', from_scratch=True)
|
2017-11-10 22:03:24 +00:00
|
|
|
mock_opts['scheduler_before_connect'] = True
|
2018-05-22 19:11:58 +00:00
|
|
|
io_loop = tornado.ioloop.IOLoop()
|
|
|
|
io_loop.make_current()
|
|
|
|
minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
|
2017-11-10 22:03:24 +00:00
|
|
|
try:
|
|
|
|
try:
|
|
|
|
minion.tune_in(start=True)
|
|
|
|
except RuntimeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Make sure the scheduler is initialized but the beacons are not
|
|
|
|
self.assertTrue('schedule' in minion.periodic_callbacks)
|
|
|
|
self.assertTrue('beacons' not in minion.periodic_callbacks)
|
|
|
|
finally:
|
|
|
|
minion.destroy()
|
2018-08-22 21:46:34 +00:00
|
|
|
|
2018-09-25 00:50:23 +00:00
|
|
|
def test_minion_retry_dns_count(self):
|
|
|
|
'''
|
|
|
|
Tests that the resolve_dns will retry dns look ups for a maximum of
|
|
|
|
3 times before raising a SaltMasterUnresolvableError exception.
|
|
|
|
'''
|
|
|
|
with patch.dict(__opts__, {'ipv6': False, 'master': 'dummy',
|
|
|
|
'master_port': '4555',
|
|
|
|
'retry_dns': 1, 'retry_dns_count': 3}):
|
|
|
|
self.assertRaises(SaltMasterUnresolvableError,
|
|
|
|
salt.minion.resolve_dns, __opts__)
|
|
|
|
|
2018-08-22 21:46:34 +00:00
|
|
|
|
|
|
|
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
|
|
|
class MinionAsyncTestCase(TestCase, AdaptedConfigurationTestCaseMixin, tornado.testing.AsyncTestCase):
|
|
|
|
|
|
|
|
@skip_if_not_root
|
|
|
|
def test_sock_path_len(self):
|
|
|
|
'''
|
|
|
|
This tests whether or not a larger hash causes the sock path to exceed
|
|
|
|
the system's max sock path length. See the below link for more
|
|
|
|
information.
|
|
|
|
|
|
|
|
https://github.com/saltstack/salt/issues/12172#issuecomment-43903643
|
|
|
|
'''
|
|
|
|
opts = {
|
|
|
|
'id': 'salt-testing',
|
|
|
|
'hash_type': 'sha512',
|
|
|
|
'sock_dir': os.path.join(salt.syspaths.SOCK_DIR, 'minion'),
|
|
|
|
'extension_modules': ''
|
|
|
|
}
|
|
|
|
with patch.dict(__opts__, opts):
|
|
|
|
try:
|
|
|
|
event_publisher = event.AsyncEventPublisher(__opts__)
|
|
|
|
result = True
|
|
|
|
except ValueError:
|
|
|
|
# There are rare cases where we operate a closed socket, especially in containers.
|
|
|
|
# In this case, don't fail the test because we'll catch it down the road.
|
|
|
|
result = True
|
|
|
|
except SaltSystemExit:
|
|
|
|
result = False
|
|
|
|
self.assertTrue(result)
|