Merge branch '2017.7' into '2018.3'

Conflicts:
  - salt/daemons/flo/zero.py
  - salt/minion.py
  - salt/pillar/pillar_ldap.py
  - salt/transport/zeromq.py
  - salt/utils/async.py
  - salt/utils/zeromq.py
  - tests/integration/modules/test_service.py
  - tests/integration/netapi/rest_tornado/test_app.py
  - tests/unit/fileserver/test_gitfs.py
  - tests/unit/modules/test_pip.py
This commit is contained in:
rallytime 2018-05-02 15:00:40 -04:00
commit a94cdf8a0d
No known key found for this signature in database
GPG key ID: E8F1A4B90D0DEA19
15 changed files with 286 additions and 115 deletions

View file

@ -34,8 +34,8 @@ from salt.ext.six.moves import range
from salt.utils.zeromq import zmq, ZMQDefaultLoop, install_zmq, ZMQ_VERSION_INFO
# pylint: enable=no-name-in-module,redefined-builtin
import tornado
# Import third party libs
HAS_RANGE = False
try:
import seco.range
@ -1172,7 +1172,7 @@ class Minion(MinionBase):
# I made the following 3 line oddity to preserve traceback.
# Please read PR #23978 before changing, hopefully avoiding regressions.
# Good luck, we're all counting on you. Thanks.
future_exception = self._connect_master_future.exc_info()
future_exception = self._connect_master_future.exception()
if future_exception:
# This needs to be re-raised to preserve restart_on_error behavior.
raise six.reraise(*future_exception)

View file

@ -2309,10 +2309,10 @@ def create_route(route_table_id=None, destination_cidr_block=None,
'must be provided.')
if not _exactly_one((gateway_id, internet_gateway_name, instance_id, interface_id, vpc_peering_connection_id,
nat_gateway_id, nat_gateway_subnet_id, nat_gateway_subnet_name)):
nat_gateway_id, nat_gateway_subnet_id, nat_gateway_subnet_name, vpc_peering_connection_name)):
raise SaltInvocationError('Only one of gateway_id, internet_gateway_name, instance_id, '
'interface_id, vpc_peering_connection_id, nat_gateway_id, '
'nat_gateway_subnet_id or nat_gateway_subnet_name may be provided.')
'nat_gateway_subnet_id, nat_gateway_subnet_name or vpc_peering_connection_name may be provided.')
if destination_cidr_block is None:
raise SaltInvocationError('destination_cidr_block is required.')

View file

@ -458,8 +458,8 @@ def delval(key, destructive=False):
.. versionadded:: 0.17.0
Delete a grain value from the grains config file. This will just set the
grain value to `None`. To completely remove the grain run `grains.delkey`
of pass `destructive=True` to `grains.delval`.
grain value to ``None``. To completely remove the grain, run ``grains.delkey``
or pass ``destructive=True`` to ``grains.delval``.
key
The grain key from which to delete the value.

View file

@ -437,6 +437,9 @@ class BaseSaltAPIHandler(tornado.web.RequestHandler): # pylint: disable=W0223
'runner_async': None, # empty, since we use the same client as `runner`
}
if not hasattr(self, 'ckminions'):
self.ckminions = salt.utils.minions.CkMinions(self.application.opts)
@property
def token(self):
'''
@ -931,7 +934,8 @@ class SaltAPIHandler(BaseSaltAPIHandler): # pylint: disable=W0223
chunk['jid'] = salt.utils.jid.gen_jid(self.application.opts)
# Subscribe returns from minions before firing a job
future_minion_map = self.subscribe_minion_returns(chunk['jid'], chunk['tgt'])
minions = set(self.ckminions.check_minions(chunk['tgt'], chunk.get('tgt_type', 'glob')))
future_minion_map = self.subscribe_minion_returns(chunk['jid'], minions)
f_call = self._format_call_run_job_async(chunk)
# fire a job off
@ -947,9 +951,9 @@ class SaltAPIHandler(BaseSaltAPIHandler): # pylint: disable=W0223
pass
raise tornado.gen.Return('No minions matched the target. No command was sent, no jid was assigned.')
syndic_min_wait = None
# wait syndic a while to avoid missing published events
if self.application.opts['order_masters']:
syndic_min_wait = tornado.gen.sleep(self.application.opts['syndic_wait'])
yield tornado.gen.sleep(self.application.opts['syndic_wait'])
# To ensure job_not_running and all_return are terminated by each other, communicate using a future
is_finished = Future()
@ -959,10 +963,6 @@ class SaltAPIHandler(BaseSaltAPIHandler): # pylint: disable=W0223
f_call['kwargs']['tgt_type'],
is_finished)
# if we have a min_wait, do that
if syndic_min_wait is not None:
yield syndic_min_wait
minion_returns_future = self.sanitize_minion_returns(future_minion_map, pub_data['minions'], is_finished)
yield job_not_running_future
@ -999,7 +999,7 @@ class SaltAPIHandler(BaseSaltAPIHandler): # pylint: disable=W0223
chunk_ret = {}
while True:
f = yield Any(future_minion_map.keys() + [is_finished])
f = yield Any(list(future_minion_map.keys()) + [is_finished])
try:
# When finished entire routine, cleanup other futures and return result
if f is is_finished:

View file

@ -50,66 +50,76 @@ possible to reference grains within the configuration.
to trick the master into returning secret data.
Use only the 'id' grain which is verified through the minion's key/cert.
Map Mode
--------
The ``it-admins`` configuration below returns the Pillar ``it-admins`` by:
- filtering for:
- members of the group ``it-admins``
- objects with ``objectclass=user``
- returning the data of users (``mode: map``), where each user is a dictionary
containing the configured string or list attributes.
- members of the group ``it-admins``
- objects with ``objectclass=user``
- returning the data of users, where each user is a dictionary containing the
configured string or list attributes.
**Configuration:**
Configuration
*************
.. code-block:: yaml
salt-users:
server: ldap.company.tld
port: 389
tls: true
dn: 'dc=company,dc=tld'
binddn: 'cn=salt-pillars,ou=users,dc=company,dc=tld'
bindpw: bi7ieBai5Ano
referrals: false
anonymous: false
mode: map
dn: 'ou=users,dc=company,dc=tld'
filter: '(&(memberof=cn=it-admins,ou=groups,dc=company,dc=tld)(objectclass=user))'
attrs:
- cn
- displayName
- givenName
- sn
lists:
- memberOf
server: ldap.company.tld
port: 389
tls: true
dn: 'dc=company,dc=tld'
binddn: 'cn=salt-pillars,ou=users,dc=company,dc=tld'
bindpw: bi7ieBai5Ano
referrals: false
anonymous: false
mode: map
dn: 'ou=users,dc=company,dc=tld'
filter: '(&(memberof=cn=it-admins,ou=groups,dc=company,dc=tld)(objectclass=user))'
attrs:
- cn
- displayName
- givenName
- sn
lists:
- memberOf
**Result:**
search_order:
- salt-users
.. code-block:: yaml
Result
******
salt-users:
- cn: cn=johndoe,ou=users,dc=company,dc=tld
displayName: John Doe
givenName: John
sn: Doe
memberOf:
- cn=it-admins,ou=groups,dc=company,dc=tld
- cn=team01,ou=groups,dc=company
- cn: cn=janedoe,ou=users,dc=company,dc=tld
displayName: Jane Doe
givenName: Jane
sn: Doe
memberOf:
- cn=it-admins,ou=groups,dc=company,dc=tld
- cn=team02,ou=groups,dc=company
.. code-block:: python
List Mode
---------
TODO: see also ``_result_to_dict()`` documentation
{
'salt-users': [
{
'cn': 'cn=johndoe,ou=users,dc=company,dc=tld',
'displayName': 'John Doe'
'givenName': 'John'
'sn': 'Doe'
'memberOf': [
'cn=it-admins,ou=groups,dc=company,dc=tld',
'cn=team01,ou=groups,dc=company'
]
},
{
'cn': 'cn=janedoe,ou=users,dc=company,dc=tld',
'displayName': 'Jane Doe',
'givenName': 'Jane',
'sn': 'Doe',
'memberOf': [
'cn=it-admins,ou=groups,dc=company,dc=tld',
'cn=team02,ou=groups,dc=company'
]
}
]
}
'''
# Import python libs
@ -122,7 +132,7 @@ import salt.utils.data
from salt.exceptions import SaltInvocationError
# Import third party libs
from jinja2 import Environment, FileSystemLoader
import jinja2
try:
import ldap # pylint: disable=W0611
HAS_LDAP = True
@ -148,10 +158,9 @@ def _render_template(config_file):
Render config template, substituting grains where found.
'''
dirname, filename = os.path.split(config_file)
env = Environment(loader=FileSystemLoader(dirname))
env = jinja2.Environment(loader=jinja2.FileSystemLoader(dirname))
template = env.get_template(filename)
config = template.render(__grains__)
return config
return template.render(__grains__)
def _config(name, conf):
@ -185,18 +194,18 @@ def _result_to_dict(data, result, conf, source):
For example, search result:
{ saltKeyValue': ['ntpserver=ntp.acme.local', 'foo=myfoo'],
'saltList': ['vhost=www.acme.net', 'vhost=www.acme.local' }
'saltList': ['vhost=www.acme.net', 'vhost=www.acme.local'] }
is written to the pillar data dictionary as:
{ 'ntpserver': 'ntp.acme.local', 'foo': 'myfoo',
'vhost': ['www.acme.net', 'www.acme.local' }
'vhost': ['www.acme.net', 'www.acme.local'] }
'''
attrs = _config('attrs', conf) or []
lists = _config('lists', conf) or []
# TODO:
# deprecate the default 'mode: split' and make the more
# straightforward 'mode: dict' the new default
# straightforward 'mode: map' the new default
mode = _config('mode', conf) or 'split'
if mode == 'map':
data[source] = []
@ -272,22 +281,46 @@ def ext_pillar(minion_id, # pylint: disable=W0613
'''
Execute LDAP searches and return the aggregated data
'''
if os.path.isfile(config_file):
import salt.utils.yaml
try:
#open(config_file, 'r') as raw_config:
config = _render_template(config_file) or {}
opts = salt.utils.yaml.safe_load(config) or {}
opts['conf_file'] = config_file
except Exception as err:
import salt.log
msg = 'Error parsing configuration file: {0} - {1}'.format(config_file, err)
if salt.log.is_console_configured():
log.warning(msg)
else:
print(msg)
config_template = None
try:
config_template = _render_template(config_file)
except jinja2.exceptions.TemplateNotFound:
log.debug('pillar_ldap: missing configuration file %s', config_file)
except Exception:
log.debug('pillar_ldap: failed to render template for %s',
config_file, exc_info=True)
if not config_template:
# We don't have a config file
return {}
import salt.utils.yaml
try:
opts = salt.utils.yaml.safe_load(config_template) or {}
opts['conf_file'] = config_file
except Exception as err:
import salt.log
msg = 'pillar_ldap: error parsing configuration file: {0} - {1}'
if salt.log.is_console_configured():
log.warning(msg.format(config_file, err))
else:
print(msg.format(config_file, err))
return {}
else:
log.debug('Missing configuration file: %s', config_file)
if not isinstance(opts, dict):
log.warning(
'pillar_ldap: %s is invalidly formatted, must be a YAML '
'dictionary. See the documentation for more information.',
config_file
)
return {}
if 'search_order' not in opts:
log.warning(
'pillar_ldap: search_order missing from configuration. See the '
'documentation for more information.'
)
return {}
data = {}
for source in opts['search_order']:

View file

@ -154,6 +154,16 @@ def __virtual__():
return 'pkg.install' in __salt__
def _warn_virtual(virtual):
return [
'The following package(s) are "virtual package" names: {0}. These '
'will no longer be supported as of the Fluorine release. Please '
'update your SLS file(s) to use the actual package name.'.format(
', '.join(virtual)
)
]
def _get_comparison_spec(pkgver):
'''
Return a tuple containing the comparison operator and the version. If no
@ -521,6 +531,11 @@ def _find_install_targets(name=None,
was_refreshed = True
refresh = False
def _get_virtual(desired):
return [x for x in desired if cur_pkgs.get(x, []) == ['1']]
virtual_pkgs = []
if any((pkgs, sources)):
if pkgs:
desired = _repack_pkgs(pkgs, normalize=normalize)
@ -538,6 +553,8 @@ def _find_install_targets(name=None,
'comment': 'Invalidly formatted \'{0}\' parameter. See '
'minion log.'.format('pkgs' if pkgs
else 'sources')}
virtual_pkgs = _get_virtual(desired)
to_unpurge = _find_unpurge_targets(desired)
else:
if salt.utils.platform.is_windows():
@ -558,6 +575,7 @@ def _find_install_targets(name=None,
else:
desired = {name: version}
virtual_pkgs = _get_virtual(desired)
to_unpurge = _find_unpurge_targets(desired)
# FreeBSD pkg supports `openjdk` and `java/openjdk7` package names
@ -574,22 +592,28 @@ def _find_install_targets(name=None,
and not reinstall \
and not pkg_verify:
# The package is installed and is the correct version
return {'name': name,
'changes': {},
'result': True,
'comment': 'Version {0} of package \'{1}\' is already '
'installed'.format(version, name)}
ret = {'name': name,
'changes': {},
'result': True,
'comment': 'Version {0} of package \'{1}\' is already '
'installed'.format(version, name)}
if virtual_pkgs:
ret['warnings'] = _warn_virtual(virtual_pkgs)
return ret
# if cver is not an empty string, the package is already installed
elif cver and version is None \
and not reinstall \
and not pkg_verify:
# The package is installed
return {'name': name,
'changes': {},
'result': True,
'comment': 'Package {0} is already '
'installed'.format(name)}
ret = {'name': name,
'changes': {},
'result': True,
'comment': 'Package {0} is already '
'installed'.format(name)}
if virtual_pkgs:
ret['warnings'] = _warn_virtual(virtual_pkgs)
return ret
version_spec = False
if not sources:
@ -627,10 +651,13 @@ def _find_install_targets(name=None,
if comments:
if len(comments) > 1:
comments.append('')
return {'name': name,
'changes': {},
'result': False,
'comment': '. '.join(comments).rstrip()}
ret = {'name': name,
'changes': {},
'result': False,
'comment': '. '.join(comments).rstrip()}
if virtual_pkgs:
ret['warnings'] = _warn_virtual(virtual_pkgs)
return ret
# Resolve the latest package version for any packages with "latest" in the
# package version
@ -765,10 +792,13 @@ def _find_install_targets(name=None,
problems.append(failed_verify)
if problems:
return {'name': name,
'changes': {},
'result': False,
'comment': ' '.join(problems)}
ret = {'name': name,
'changes': {},
'result': False,
'comment': ' '.join(problems)}
if virtual_pkgs:
ret['warnings'] = _warn_virtual(virtual_pkgs)
return ret
if not any((targets, to_unpurge, to_reinstall)):
# All specified packages are installed
@ -783,6 +813,8 @@ def _find_install_targets(name=None,
'comment': msg}
if warnings:
ret.setdefault('warnings', []).extend(warnings)
if virtual_pkgs:
ret.setdefault('warnings', []).extend(_warn_virtual(virtual_pkgs))
return ret
return (desired, targets, to_unpurge, to_reinstall, altered_files,
@ -1759,6 +1791,25 @@ def installed(
and x not in to_reinstall]
failed = [x for x in failed if x in targets]
# Check for virtual packages in list of desired packages
if not sources:
try:
virtual_pkgs = []
for pkgname in [next(iter(x)) for x in pkgs] if pkgs else [name]:
cver = new_pkgs.get(pkgname, [])
if '1' in cver:
virtual_pkgs.append(pkgname)
if virtual_pkgs:
warnings.extend(_warn_virtual(virtual_pkgs))
except Exception:
# This is just some temporary code to warn the user about using
# virtual packages. Don't let an exception break the entire
# state.
log.debug(
'Failed to detect virtual packages after running '
'pkg.install', exc_info=True
)
# If there was nothing unpurged, just set the changes dict to the contents
# of changes['installed'].
if not changes.get('purge_desired'):

View file

@ -295,7 +295,7 @@ class IPCClient(object):
else:
if hasattr(self, '_connecting_future'):
# read previous future result to prevent the "unhandled future exception" error
self._connecting_future.exc_info() # pylint: disable=E0203
self._connecting_future.exception() # pylint: disable=E0203
future = tornado.concurrent.Future()
self._connecting_future = future
self._connect(timeout=timeout)
@ -752,9 +752,9 @@ class IPCMessageSubscriber(IPCClient):
# '[ERROR ] Future exception was never retrieved:
# StreamClosedError'
if self._read_sync_future is not None:
self._read_sync_future.exc_info()
self._read_sync_future.exception()
if self._read_stream_future is not None:
self._read_stream_future.exc_info()
self._read_stream_future.exception()
def __del__(self):
if IPCMessageSubscriber in globals():

View file

@ -899,7 +899,7 @@ class SaltMessageClient(object):
# This happens because the logic is always waiting to read
# the next message and the associated read future is marked
# 'StreamClosedError' when the stream is closed.
self._read_until_future.exc_info()
self._read_until_future.exception()
if (not self._stream_return_future.done() and
self.io_loop != tornado.ioloop.IOLoop.current(
instance=False)):
@ -1162,7 +1162,7 @@ class Subscriber(object):
# This happens because the logic is always waiting to read
# the next message and the associated read future is marked
# 'StreamClosedError' when the stream is closed.
self._read_until_future.exc_info()
self._read_until_future.exception()
def __del__(self):
self.close()

View file

@ -914,7 +914,7 @@ class SaltNova(object):
try:
ret[item.name] = {
'id': item.id,
'status': 'Running'
'state': 'Running'
}
except TypeError:
pass

View file

@ -703,6 +703,7 @@ class Schedule(object):
for global_key, value in six.iteritems(func_globals):
self.functions[mod_name].__globals__[global_key] = value
self.functions.pack['__context__']['retcode'] = 0
ret['return'] = self.functions[func](*args, **kwargs)
if not self.standalone:

View file

@ -36,8 +36,11 @@ if ZMQDefaultLoop is None:
# Support for ZeroMQ 13.x
if not hasattr(zmq.eventloop.ioloop, 'ZMQIOLoop'):
zmq.eventloop.ioloop.ZMQIOLoop = zmq.eventloop.ioloop.IOLoop
ZMQDefaultLoop = zmq.eventloop.ioloop.ZMQIOLoop
if tornado.version_info < (5,):
ZMQDefaultLoop = zmq.eventloop.ioloop.ZMQIOLoop
except ImportError:
ZMQDefaultLoop = None
if ZMQDefaultLoop is None:
ZMQDefaultLoop = tornado.ioloop.IOLoop
@ -48,7 +51,8 @@ def install_zmq():
:return:
'''
if zmq and ZMQ_VERSION_INFO[0] < 17:
zmq.eventloop.ioloop.install()
if tornado.version_info < (5,):
zmq.eventloop.ioloop.install()
def check_ipc_path_max_len(uri):

View file

@ -6,10 +6,11 @@ from __future__ import absolute_import, print_function, unicode_literals
# Import Salt Testing libs
from tests.support.case import ModuleCase
from tests.support.helpers import destructiveTest
from tests.support.unit import skipIf
# Import Salt libs
import salt.utils.path
import salt.utils.platform
@destructiveTest
class ServiceModuleTest(ModuleCase):
@ -30,10 +31,32 @@ class ServiceModuleTest(ModuleCase):
self.service_name = 'org.ntp.ntpd'
if int(os_release.split('.')[1]) >= 13:
self.service_name = 'com.apple.AirPlayXPCHelper'
elif salt.utils.is_windows():
self.service_name = 'Spooler'
if salt.utils.path.which(cmd_name) is None:
self.pre_srv_status = self.run_function('service.status', [self.service_name])
self.pre_srv_enabled = True if self.service_name in self.run_function('service.get_enabled') else False
if salt.utils.path.which(cmd_name) is None and not salt.utils.platform.is_windows():
self.skipTest('{0} is not installed'.format(cmd_name))
def tearDown(self):
post_srv_status = self.run_function('service.status', [self.service_name])
post_srv_enabled = True if self.service_name in self.run_function('service.get_enabled') else False
if post_srv_status != self.pre_srv_status:
if self.pre_srv_status:
self.run_function('service.enable', [self.service_name])
else:
self.run_function('service.disable', [self.service_name])
if post_srv_enabled != self.pre_srv_enabled:
if self.pre_srv_enabled:
self.run_function('service.enable', [self.service_name])
else:
self.run_function('service.disable', [self.service_name])
del self.service_name
def test_service_status_running(self):
'''
test service.status execution module
@ -51,3 +74,37 @@ class ServiceModuleTest(ModuleCase):
self.run_function('service.stop', [self.service_name])
check_service = self.run_function('service.status', [self.service_name])
self.assertFalse(check_service)
def test_service_restart(self):
'''
test service.restart
'''
self.assertTrue(self.run_function('service.restart', [self.service_name]))
def test_service_enable(self):
'''
test service.get_enabled and service.enable module
'''
# disable service before test
self.assertTrue(self.run_function('service.disable', [self.service_name]))
self.assertTrue(self.run_function('service.enable', [self.service_name]))
self.assertIn(self.service_name, self.run_function('service.get_enabled'))
def test_service_disable(self):
'''
test service.get_disabled and service.disable module
'''
# enable service before test
self.assertTrue(self.run_function('service.enable', [self.service_name]))
self.assertTrue(self.run_function('service.disable', [self.service_name]))
self.assertIn(self.service_name, self.run_function('service.get_disabled'))
@skipIf(not salt.utils.is_windows(), 'Windows Only Test')
def test_service_get_service_name(self):
'''
test service.get_service_name
'''
ret = self.run_function('service.get_service_name')
self.assertIn(self.service_name, ret.values())

View file

@ -42,6 +42,7 @@ class TestSaltAPIHandler(_SaltnadoIntegrationTestCase):
application = self.build_tornado_app(urls)
application.event_listener = saltnado.EventListener({}, self.opts)
self.application = application
return application
def test_root(self):
@ -78,8 +79,6 @@ class TestSaltAPIHandler(_SaltnadoIntegrationTestCase):
self.assertEqual(response.code, 302)
self.assertEqual(response.headers['Location'], '/login')
# Local client tests
@skipIf(True, 'to be re-enabled when #23623 is merged')
def test_simple_local_post(self):
'''
Test a basic API of /
@ -97,7 +96,8 @@ class TestSaltAPIHandler(_SaltnadoIntegrationTestCase):
request_timeout=30,
)
response_obj = salt.utils.json.loads(response.body)
self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}])
self.assertEqual(len(response_obj['return']), 1)
self.assertEqual(response_obj['return'][0], {'minion': True, 'sub_minion': True})
def test_simple_local_post_no_tgt(self):
'''
@ -118,8 +118,6 @@ class TestSaltAPIHandler(_SaltnadoIntegrationTestCase):
response_obj = salt.utils.json.loads(response.body)
self.assertEqual(response_obj['return'], ["No minions matched the target. No command was sent, no jid was assigned."])
# local client request body test
@skipIf(True, 'Undetermined race condition in test. Temporarily disabled.')
def test_simple_local_post_only_dictionary_request(self):
'''
Test a basic API of /
@ -137,7 +135,8 @@ class TestSaltAPIHandler(_SaltnadoIntegrationTestCase):
request_timeout=30,
)
response_obj = salt.utils.json.loads(response.body)
self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}])
self.assertEqual(len(response_obj['return']), 1)
self.assertEqual(response_obj['return'][0], {'minion': True, 'sub_minion': True})
def test_simple_local_post_invalid_request(self):
'''
@ -252,6 +251,28 @@ class TestSaltAPIHandler(_SaltnadoIntegrationTestCase):
response_obj = salt.utils.json.loads(response.body)
self.assertEqual(response_obj['return'], [{}])
def test_simple_local_post_only_dictionary_request_with_order_masters(self):
'''
Test a basic API of /
'''
low = {'client': 'local',
'tgt': '*',
'fun': 'test.ping',
}
response = self.fetch('/',
method='POST',
body=salt.utils.json.dumps(low),
headers={'Content-Type': self.content_type_map['json'],
saltnado.AUTH_TOKEN_HEADER: self.token['token']},
connect_timeout=30,
request_timeout=30,
)
response_obj = salt.utils.json.loads(response.body)
self.application.opts['order_masters'] = []
self.application.opts['syndic_wait'] = 5
self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}])
# runner tests
def test_simple_local_runner_post(self):
low = [{'client': 'runner',

View file

@ -10,6 +10,7 @@ from tests.support.unit import skipIf, TestCase
from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch
# Import salt libs
from salt.ext import six
import salt.utils.platform
import salt.modules.pip as pip
from salt.exceptions import CommandExecutionError
@ -300,6 +301,8 @@ class PipTestCase(TestCase, LoaderModuleMockMixin):
if salt.utils.platform.is_windows():
venv_path = 'c:\\test_env'
bin_path = os.path.join(venv_path, 'Scripts', 'pip.exe')
if six.PY2:
bin_path = bin_path.encode('string-escape')
else:
venv_path = '/test_env'
bin_path = os.path.join(venv_path, 'bin', 'pip')

View file

@ -21,6 +21,7 @@ integration.modules.test_ntp
integration.modules.test_pillar
integration.modules.test_pkg
integration.modules.test_publish
integration.modules.test_service
integration.modules.test_state
integration.modules.test_status
integration.modules.test_sysmod