Merge branch 'develop' into bbczeuz-patch-phusion-initd

This commit is contained in:
Nicole Thomas 2017-12-05 12:47:40 -05:00 committed by GitHub
commit f967a54dc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1239 additions and 132 deletions

4
.github/stale.yml vendored
View File

@ -1,8 +1,8 @@
# Probot Stale configuration file # Probot Stale configuration file
# Number of days of inactivity before an issue becomes stale # Number of days of inactivity before an issue becomes stale
# 875 is approximately 2 years and 5 months # 860 is approximately 2 years and 4 months
daysUntilStale: 875 daysUntilStale: 860
# Number of days of inactivity before a stale issue is closed # Number of days of inactivity before a stale issue is closed
daysUntilClose: 7 daysUntilClose: 7

View File

@ -1725,9 +1725,15 @@ enabled and can be disabled by changing this value to ``False``.
If ``extmod_whitelist`` is specified, modules which are not whitelisted will also be cleaned here. If ``extmod_whitelist`` is specified, modules which are not whitelisted will also be cleaned here.
.. conf_minion:: environment .. conf_minion:: environment
.. conf_minion:: saltenv
``environment`` ``saltenv``
--------------- -----------
.. versionchanged:: Oxygen
Renamed from ``environment`` to ``saltenv``. If ``environment`` is used,
``saltenv`` will take its value. If both are used, ``environment`` will be
ignored and ``saltenv`` will be used.
Normally the minion is not isolated to any single environment on the master Normally the minion is not isolated to any single environment on the master
when running states, but the environment can be isolated on the minion side when running states, but the environment can be isolated on the minion side
@ -1736,7 +1742,25 @@ environments is to isolate via the top file.
.. code-block:: yaml .. code-block:: yaml
environment: dev saltenv: dev
.. conf_minion:: lock_saltenv
``lock_saltenv``
----------------
.. versionadded:: Oxygen
Default: ``False``
For purposes of running states, this option prevents using the ``saltenv``
argument to manually set the environment. This is useful to keep a minion which
has the :conf_minion:`saltenv` option set to ``dev`` from running states from
an environment other than ``dev``.
.. code-block:: yaml
lock_saltenv: True
.. conf_minion:: snapper_states .. conf_minion:: snapper_states

View File

@ -137,6 +137,43 @@ can specify the "name" argument to avoid conflicting IDs:
- kwarg: - kwarg:
remove_existing: true remove_existing: true
.. _orchestrate-runner-fail-functions:
Fail Functions
**************
When running a remote execution function in orchestration, certain return
values for those functions may indicate failure, while the function itself
doesn't set a return code. For those circumstances, using a "fail function"
allows for a more flexible means of assessing success or failure.
A fail function can be written as part of a :ref:`custom execution module
<writing-execution-modules>`. The function should accept one argument, and
return a boolean result. For example:
.. code-block:: python
def check_func_result(retval):
if some_condition:
return True
else:
return False
The function can then be referenced in orchestration SLS like so:
.. code-block:: yaml
do_stuff:
salt.function:
- name: modname.funcname
- tgt: '*'
- fail_function: mymod.check_func_result
.. important::
Fail functions run *on the master*, so they must be synced using ``salt-run
saltutil.sync_modules``.
State State
^^^^^ ^^^^^
@ -221,6 +258,7 @@ To execute with pillar data.
salt-run state.orch orch.deploy pillar='{"servers": "newsystem1", salt-run state.orch orch.deploy pillar='{"servers": "newsystem1",
"master": "mymaster"}' "master": "mymaster"}'
.. _orchestrate-runner-return-codes-runner-wheel:
Return Codes in Runner/Wheel Jobs Return Codes in Runner/Wheel Jobs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -298,3 +336,270 @@ Given the above setup, the orchestration will be carried out as follows:
.. note:: .. note::
Remember, salt-run is always executed on the master. Remember, salt-run is always executed on the master.
.. _orchestrate-runner-parsing-results-programatically:
Parsing Results Programatically
-------------------------------
Orchestration jobs return output in a specific data structure. That data
structure is represented differently depending on the outputter used. With the
default outputter for orchestration, you get a nice human-readable output.
Assume the following orchestration SLS:
.. code-block:: yaml
good_state:
salt.state:
- tgt: myminion
- sls:
- succeed_with_changes
bad_state:
salt.state:
- tgt: myminion
- sls:
- fail_with_changes
mymod.myfunc:
salt.function:
- tgt: myminion
mymod.myfunc_false_result:
salt.function:
- tgt: myminion
Running this using the default outputter would produce output which looks like
this:
.. code-block:: text
fa5944a73aa8_master:
----------
ID: good_state
Function: salt.state
Result: True
Comment: States ran successfully. Updating myminion.
Started: 21:08:02.681604
Duration: 265.565 ms
Changes:
myminion:
----------
ID: test succeed with changes
Function: test.succeed_with_changes
Result: True
Comment: Success!
Started: 21:08:02.835893
Duration: 0.375 ms
Changes:
----------
testing:
----------
new:
Something pretended to change
old:
Unchanged
Summary for myminion
------------
Succeeded: 1 (changed=1)
Failed: 0
------------
Total states run: 1
Total run time: 0.375 ms
----------
ID: bad_state
Function: salt.state
Result: False
Comment: Run failed on minions: myminion
Started: 21:08:02.947702
Duration: 177.01 ms
Changes:
myminion:
----------
ID: test fail with changes
Function: test.fail_with_changes
Result: False
Comment: Failure!
Started: 21:08:03.116634
Duration: 0.502 ms
Changes:
----------
testing:
----------
new:
Something pretended to change
old:
Unchanged
Summary for myminion
------------
Succeeded: 0 (changed=1)
Failed: 1
------------
Total states run: 1
Total run time: 0.502 ms
----------
ID: mymod.myfunc
Function: salt.function
Result: True
Comment: Function ran successfully. Function mymod.myfunc ran on myminion.
Started: 21:08:03.125011
Duration: 159.488 ms
Changes:
myminion:
True
----------
ID: mymod.myfunc_false_result
Function: salt.function
Result: False
Comment: Running function mymod.myfunc_false_result failed on minions: myminion. Function mymod.myfunc_false_result ran on myminion.
Started: 21:08:03.285148
Duration: 176.787 ms
Changes:
myminion:
False
Summary for fa5944a73aa8_master
------------
Succeeded: 2 (changed=4)
Failed: 2
------------
Total states run: 4
Total run time: 778.850 ms
However, using the ``json`` outputter, you can get the output in an easily
loadable and parsable format:
.. code-block:: bash
salt-run state.orchestrate test --out=json
.. code-block:: json
{
"outputter": "highstate",
"data": {
"fa5944a73aa8_master": {
"salt_|-good_state_|-good_state_|-state": {
"comment": "States ran successfully. Updating myminion.",
"name": "good_state",
"start_time": "21:35:16.868345",
"result": true,
"duration": 267.299,
"__run_num__": 0,
"__jid__": "20171130213516897392",
"__sls__": "test",
"changes": {
"ret": {
"myminion": {
"test_|-test succeed with changes_|-test succeed with changes_|-succeed_with_changes": {
"comment": "Success!",
"name": "test succeed with changes",
"start_time": "21:35:17.022592",
"result": true,
"duration": 0.362,
"__run_num__": 0,
"__sls__": "succeed_with_changes",
"changes": {
"testing": {
"new": "Something pretended to change",
"old": "Unchanged"
}
},
"__id__": "test succeed with changes"
}
}
},
"out": "highstate"
},
"__id__": "good_state"
},
"salt_|-bad_state_|-bad_state_|-state": {
"comment": "Run failed on minions: test",
"name": "bad_state",
"start_time": "21:35:17.136511",
"result": false,
"duration": 197.635,
"__run_num__": 1,
"__jid__": "20171130213517202203",
"__sls__": "test",
"changes": {
"ret": {
"myminion": {
"test_|-test fail with changes_|-test fail with changes_|-fail_with_changes": {
"comment": "Failure!",
"name": "test fail with changes",
"start_time": "21:35:17.326268",
"result": false,
"duration": 0.509,
"__run_num__": 0,
"__sls__": "fail_with_changes",
"changes": {
"testing": {
"new": "Something pretended to change",
"old": "Unchanged"
}
},
"__id__": "test fail with changes"
}
}
},
"out": "highstate"
},
"__id__": "bad_state"
},
"salt_|-mymod.myfunc_|-mymod.myfunc_|-function": {
"comment": "Function ran successfully. Function mymod.myfunc ran on myminion.",
"name": "mymod.myfunc",
"start_time": "21:35:17.334373",
"result": true,
"duration": 151.716,
"__run_num__": 2,
"__jid__": "20171130213517361706",
"__sls__": "test",
"changes": {
"ret": {
"myminion": true
},
"out": "highstate"
},
"__id__": "mymod.myfunc"
},
"salt_|-mymod.myfunc_false_result-mymod.myfunc_false_result-function": {
"comment": "Running function mymod.myfunc_false_result failed on minions: myminion. Function mymod.myfunc_false_result ran on myminion.",
"name": "mymod.myfunc_false_result",
"start_time": "21:35:17.486625",
"result": false,
"duration": 174.241,
"__run_num__": 3,
"__jid__": "20171130213517536270",
"__sls__": "test",
"changes": {
"ret": {
"myminion": false
},
"out": "highstate"
},
"__id__": "mymod.myfunc_false_result"
}
}
},
"retcode": 1
}
The Oxygen release includes a couple fixes to make parsing this data easier and
more accurate. The first is the ability to set a :ref:`return code
<orchestrate-runner-return-codes-runner-wheel>` in a custom runner or wheel
function, as noted above. The second is a change to how failures are included
in the return data. Prior to the Oxygen release, minions that failed a
``salt.state`` orchestration job would show up in the ``comment`` field of the
return data, in a human-readable string that was not easily parsed. They are
now included in the ``changes`` dictionary alongside the minions that
succeeded. In addition, ``salt.function`` jobs which failed because the
:ref:`fail function <orchestrate-runner-fail-functions>` returned ``False``
used to handle their failures in the same way ``salt.state`` jobs did, and this
has likewise been corrected.

View File

@ -65,6 +65,37 @@ noon PST so the Stormpath external authentication module has been removed.
https://stormpath.com/oktaplusstormpath https://stormpath.com/oktaplusstormpath
:conf_minion:`environment` config option renamed to :conf_minion:`saltenv`
--------------------------------------------------------------------------
The :conf_minion:`environment` config option predates referring to a salt
fileserver environment as a **saltenv**. To pin a minion to a single
environment for running states, one would use :conf_minion:`environment`, but
overriding that environment would be done with the ``saltenv`` argument. For
consistency, :conf_minion:`environment` is now simply referred to as
:conf_minion:`saltenv`. There are no plans to deprecate or remove
:conf_minion:`environment`, if used it will log a warning and its value will be
used as :conf_minion:`saltenv`.
:conf_minion:`lock_saltenv` config option added
-----------------------------------------------
If set to ``True``, this option will prevent a minion from allowing the
``saltenv`` argument to override the value set in :conf_minion:`saltenv` when
running states.
Failed Minions for State/Function Orchestration Jobs Added to Changes Dictionary
--------------------------------------------------------------------------------
For orchestration jobs which run states (or run remote execution functions and
also use a :ref:`fail function <orchestrate-runner-fail-functions>` to indicate
success or failure), minions which have ``False`` results were previously
included as a formatted string in the comment field of the return for that
orchestration job. This made the failed returns difficult to :ref:`parse
programatically <orchestrate-runner-parsing-results-programatically>`. The
failed returns in these cases are now included in the changes dictionary,
making for much easier parsing.
New Grains New Grains
---------- ----------

View File

@ -1050,7 +1050,7 @@ class Single(object):
popts, popts,
opts_pkg[u'grains'], opts_pkg[u'grains'],
opts_pkg[u'id'], opts_pkg[u'id'],
opts_pkg.get(u'environment', u'base') opts_pkg.get(u'saltenv', u'base')
) )
pillar_data = pillar.compile_pillar() pillar_data = pillar.compile_pillar()

View File

@ -246,7 +246,10 @@ VALID_OPTS = {
'autoload_dynamic_modules': bool, 'autoload_dynamic_modules': bool,
# Force the minion into a single environment when it fetches files from the master # Force the minion into a single environment when it fetches files from the master
'environment': str, 'saltenv': str,
# Prevent saltenv from being overriden on the command line
'lock_saltenv': bool,
# Force the minion into a single pillar root when it fetches pillar data from the master # Force the minion into a single pillar root when it fetches pillar data from the master
'pillarenv': str, 'pillarenv': str,
@ -1181,7 +1184,8 @@ DEFAULT_MINION_OPTS = {
'random_startup_delay': 0, 'random_startup_delay': 0,
'failhard': False, 'failhard': False,
'autoload_dynamic_modules': True, 'autoload_dynamic_modules': True,
'environment': None, 'saltenv': None,
'lock_saltenv': False,
'pillarenv': None, 'pillarenv': None,
'pillarenv_from_saltenv': False, 'pillarenv_from_saltenv': False,
'pillar_opts': False, 'pillar_opts': False,
@ -1458,7 +1462,8 @@ DEFAULT_MASTER_OPTS = {
}, },
'top_file_merging_strategy': 'merge', 'top_file_merging_strategy': 'merge',
'env_order': [], 'env_order': [],
'environment': None, 'saltenv': None,
'lock_saltenv': False,
'default_top': 'base', 'default_top': 'base',
'file_client': 'local', 'file_client': 'local',
'git_pillar_base': 'master', 'git_pillar_base': 'master',
@ -3596,6 +3601,24 @@ def apply_minion_config(overrides=None,
if overrides: if overrides:
opts.update(overrides) opts.update(overrides)
if u'environment' in opts:
if u'saltenv' in opts:
log.warning(
u'The \'saltenv\' and \'environment\' minion config options '
u'cannot both be used. Ignoring \'environment\' in favor of '
u'\'saltenv\'.',
)
# Set environment to saltenv in case someone's custom module is
# refrencing __opts__['environment']
opts[u'environment'] = opts[u'saltenv']
else:
log.warning(
u'The \'environment\' minion config option has been renamed '
u'to \'saltenv\'. Using %s as the \'saltenv\' config value.',
opts[u'environment']
)
opts[u'saltenv'] = opts[u'environment']
opts['__cli'] = os.path.basename(sys.argv[0]) opts['__cli'] = os.path.basename(sys.argv[0])
# No ID provided. Will getfqdn save us? # No ID provided. Will getfqdn save us?
@ -3748,6 +3771,24 @@ def apply_master_config(overrides=None, defaults=None):
if overrides: if overrides:
opts.update(overrides) opts.update(overrides)
if u'environment' in opts:
if u'saltenv' in opts:
log.warning(
u'The \'saltenv\' and \'environment\' master config options '
u'cannot both be used. Ignoring \'environment\' in favor of '
u'\'saltenv\'.',
)
# Set environment to saltenv in case someone's custom runner is
# refrencing __opts__['environment']
opts[u'environment'] = opts[u'saltenv']
else:
log.warning(
u'The \'environment\' master config option has been renamed '
u'to \'saltenv\'. Using %s as the \'saltenv\' config value.',
opts[u'environment']
)
opts[u'saltenv'] = opts[u'environment']
if len(opts['sock_dir']) > len(opts['cachedir']) + 10: if len(opts['sock_dir']) > len(opts['cachedir']) + 10:
opts['sock_dir'] = os.path.join(opts['cachedir'], '.salt-unix') opts['sock_dir'] = os.path.join(opts['cachedir'], '.salt-unix')

View File

@ -737,7 +737,7 @@ class SaltLoadPillar(ioflo.base.deeding.Deed):
'dst': (master.name, None, 'remote_cmd')} 'dst': (master.name, None, 'remote_cmd')}
load = {'id': self.opts.value['id'], load = {'id': self.opts.value['id'],
'grains': self.grains.value, 'grains': self.grains.value,
'saltenv': self.opts.value['environment'], 'saltenv': self.opts.value['saltenv'],
'ver': '2', 'ver': '2',
'cmd': '_pillar'} 'cmd': '_pillar'}
self.road_stack.value.transmit({'route': route, 'load': load}, self.road_stack.value.transmit({'route': route, 'load': load},

View File

@ -1593,8 +1593,10 @@ class LazyLoader(salt.utils.lazy.LazyDict):
Load a single item if you have it Load a single item if you have it
''' '''
# if the key doesn't have a '.' then it isn't valid for this mod dict # if the key doesn't have a '.' then it isn't valid for this mod dict
if not isinstance(key, six.string_types) or u'.' not in key: if not isinstance(key, six.string_types):
raise KeyError raise KeyError(u'The key must be a string.')
if u'.' not in key:
raise KeyError(u'The key \'%s\' should contain a \'.\'', key)
mod_name, _ = key.split(u'.', 1) mod_name, _ = key.split(u'.', 1)
if mod_name in self.missing_modules: if mod_name in self.missing_modules:
return True return True

View File

@ -736,8 +736,8 @@ class SMinion(MinionBase):
if not os.path.isdir(pdir): if not os.path.isdir(pdir):
os.makedirs(pdir, 0o700) os.makedirs(pdir, 0o700)
ptop = os.path.join(pdir, u'top.sls') ptop = os.path.join(pdir, u'top.sls')
if self.opts[u'environment'] is not None: if self.opts[u'saltenv'] is not None:
penv = self.opts[u'environment'] penv = self.opts[u'saltenv']
else: else:
penv = u'base' penv = u'base'
cache_top = {penv: {self.opts[u'id']: [u'cache']}} cache_top = {penv: {self.opts[u'id']: [u'cache']}}
@ -773,7 +773,7 @@ class SMinion(MinionBase):
self.opts, self.opts,
self.opts[u'grains'], self.opts[u'grains'],
self.opts[u'id'], self.opts[u'id'],
self.opts[u'environment'], self.opts[u'saltenv'],
pillarenv=self.opts.get(u'pillarenv'), pillarenv=self.opts.get(u'pillarenv'),
).compile_pillar() ).compile_pillar()
@ -1144,7 +1144,7 @@ class Minion(MinionBase):
self.opts, self.opts,
self.opts[u'grains'], self.opts[u'grains'],
self.opts[u'id'], self.opts[u'id'],
self.opts[u'environment'], self.opts[u'saltenv'],
pillarenv=self.opts.get(u'pillarenv') pillarenv=self.opts.get(u'pillarenv')
).compile_pillar() ).compile_pillar()
@ -2032,7 +2032,7 @@ class Minion(MinionBase):
self.opts, self.opts,
self.opts[u'grains'], self.opts[u'grains'],
self.opts[u'id'], self.opts[u'id'],
self.opts[u'environment'], self.opts[u'saltenv'],
pillarenv=self.opts.get(u'pillarenv'), pillarenv=self.opts.get(u'pillarenv'),
).compile_pillar() ).compile_pillar()
except SaltClientError: except SaltClientError:
@ -3353,7 +3353,7 @@ class ProxyMinion(Minion):
self.opts, self.opts,
self.opts[u'grains'], self.opts[u'grains'],
self.opts[u'id'], self.opts[u'id'],
saltenv=self.opts[u'environment'], saltenv=self.opts[u'saltenv'],
pillarenv=self.opts.get(u'pillarenv'), pillarenv=self.opts.get(u'pillarenv'),
).compile_pillar() ).compile_pillar()
@ -3395,7 +3395,7 @@ class ProxyMinion(Minion):
# we can then sync any proxymodules down from the master # we can then sync any proxymodules down from the master
# we do a sync_all here in case proxy code was installed by # we do a sync_all here in case proxy code was installed by
# SPM or was manually placed in /srv/salt/_modules etc. # SPM or was manually placed in /srv/salt/_modules etc.
self.functions[u'saltutil.sync_all'](saltenv=self.opts[u'environment']) self.functions[u'saltutil.sync_all'](saltenv=self.opts[u'saltenv'])
# Pull in the utils # Pull in the utils
self.utils = salt.loader.utils(self.opts) self.utils = salt.loader.utils(self.opts)

View File

@ -219,7 +219,7 @@ def _gather_pillar(pillarenv, pillar_override):
__opts__, __opts__,
__grains__, __grains__,
__opts__['id'], __opts__['id'],
__opts__['environment'], __opts__['saltenv'],
pillar_override=pillar_override, pillar_override=pillar_override,
pillarenv=pillarenv pillarenv=pillarenv
) )
@ -589,11 +589,17 @@ def _run(cmd,
out = proc.stdout.decode(__salt_system_encoding__) out = proc.stdout.decode(__salt_system_encoding__)
except AttributeError: except AttributeError:
out = u'' out = u''
except UnicodeDecodeError:
log.error('UnicodeDecodeError while decoding output of cmd {0}'.format(cmd))
out = proc.stdout.decode(__salt_system_encoding__, 'replace')
try: try:
err = proc.stderr.decode(__salt_system_encoding__) err = proc.stderr.decode(__salt_system_encoding__)
except AttributeError: except AttributeError:
err = u'' err = u''
except UnicodeDecodeError:
log.error('UnicodeDecodeError while decoding error of cmd {0}'.format(cmd))
err = proc.stderr.decode(__salt_system_encoding__, 'replace')
if rstrip: if rstrip:
if out is not None: if out is not None:

View File

@ -49,7 +49,7 @@ def _gather_pillar(pillarenv, pillar_override):
__opts__, __opts__,
__grains__, __grains__,
__opts__['id'], __opts__['id'],
__opts__['environment'], __opts__['saltenv'],
pillar_override=pillar_override, pillar_override=pillar_override,
pillarenv=pillarenv pillarenv=pillarenv
) )

View File

@ -5290,7 +5290,7 @@ def _gather_pillar(pillarenv, pillar_override, **grains):
grains, grains,
# Not sure if these two are correct # Not sure if these two are correct
__opts__['id'], __opts__['id'],
__opts__['environment'], __opts__['saltenv'],
pillar_override=pillar_override, pillar_override=pillar_override,
pillarenv=pillarenv pillarenv=pillarenv
) )

View File

@ -225,7 +225,7 @@ def send(tag,
data_dict['pillar'] = __pillar__ data_dict['pillar'] = __pillar__
if with_env_opts: if with_env_opts:
data_dict['saltenv'] = __opts__.get('environment', 'base') data_dict['saltenv'] = __opts__.get('saltenv', 'base')
data_dict['pillarenv'] = __opts__.get('pillarenv') data_dict['pillarenv'] = __opts__.get('pillarenv')
if kwargs: if kwargs:

View File

@ -237,7 +237,7 @@ def items(*args, **kwargs):
pillarenv = kwargs.get('pillarenv') pillarenv = kwargs.get('pillarenv')
if pillarenv is None: if pillarenv is None:
if __opts__.get('pillarenv_from_saltenv', False): if __opts__.get('pillarenv_from_saltenv', False):
pillarenv = kwargs.get('saltenv') or __opts__['environment'] pillarenv = kwargs.get('saltenv') or __opts__['saltenv']
else: else:
pillarenv = __opts__['pillarenv'] pillarenv = __opts__['pillarenv']
@ -468,7 +468,7 @@ def ext(external, pillar=None):
__opts__, __opts__,
__grains__, __grains__,
__opts__['id'], __opts__['id'],
__opts__['environment'], __opts__['saltenv'],
ext=external, ext=external,
pillar_override=pillar) pillar_override=pillar)

View File

@ -509,7 +509,7 @@ class SaltCheck(object):
# state cache should be updated before running this method # state cache should be updated before running this method
search_list = [] search_list = []
cachedir = __opts__.get('cachedir', None) cachedir = __opts__.get('cachedir', None)
environment = __opts__['environment'] environment = __opts__['saltenv']
if environment: if environment:
path = cachedir + os.sep + "files" + os.sep + environment path = cachedir + os.sep + "files" + os.sep + environment
search_list.append(path) search_list.append(path)

View File

@ -364,10 +364,14 @@ def _get_opts(**kwargs):
if 'saltenv' in kwargs: if 'saltenv' in kwargs:
saltenv = kwargs['saltenv'] saltenv = kwargs['saltenv']
if saltenv is not None and not isinstance(saltenv, six.string_types): if saltenv is not None:
opts['environment'] = str(kwargs['saltenv']) if not isinstance(saltenv, six.string_types):
else: saltenv = six.text_type(saltenv)
opts['environment'] = kwargs['saltenv'] if opts['lock_saltenv'] and saltenv != opts['saltenv']:
raise CommandExecutionError(
'lock_saltenv is enabled, saltenv cannot be changed'
)
opts['saltenv'] = kwargs['saltenv']
if 'pillarenv' in kwargs or opts.get('pillarenv_from_saltenv', False): if 'pillarenv' in kwargs or opts.get('pillarenv_from_saltenv', False):
pillarenv = kwargs.get('pillarenv') or kwargs.get('saltenv') pillarenv = kwargs.get('pillarenv') or kwargs.get('saltenv')
@ -934,7 +938,7 @@ def highstate(test=None, queue=False, **kwargs):
kwargs.pop('env') kwargs.pop('env')
if 'saltenv' in kwargs: if 'saltenv' in kwargs:
opts['environment'] = kwargs['saltenv'] opts['saltenv'] = kwargs['saltenv']
if 'pillarenv' in kwargs: if 'pillarenv' in kwargs:
opts['pillarenv'] = kwargs['pillarenv'] opts['pillarenv'] = kwargs['pillarenv']
@ -1126,8 +1130,8 @@ def sls(mods, test=None, exclude=None, queue=False, **kwargs):
# Since this is running a specific SLS file (or files), fall back to the # Since this is running a specific SLS file (or files), fall back to the
# 'base' saltenv if none is configured and none was passed. # 'base' saltenv if none is configured and none was passed.
if opts['environment'] is None: if opts['saltenv'] is None:
opts['environment'] = 'base' opts['saltenv'] = 'base'
pillar_override = kwargs.get('pillar') pillar_override = kwargs.get('pillar')
pillar_enc = kwargs.get('pillar_enc') pillar_enc = kwargs.get('pillar_enc')
@ -1183,7 +1187,7 @@ def sls(mods, test=None, exclude=None, queue=False, **kwargs):
st_.push_active() st_.push_active()
ret = {} ret = {}
try: try:
high_, errors = st_.render_highstate({opts['environment']: mods}) high_, errors = st_.render_highstate({opts['saltenv']: mods})
if errors: if errors:
__context__['retcode'] = 1 __context__['retcode'] = 1
@ -1505,8 +1509,8 @@ def sls_id(id_, mods, test=None, queue=False, **kwargs):
# Since this is running a specific ID within a specific SLS file, fall back # Since this is running a specific ID within a specific SLS file, fall back
# to the 'base' saltenv if none is configured and none was passed. # to the 'base' saltenv if none is configured and none was passed.
if opts['environment'] is None: if opts['saltenv'] is None:
opts['environment'] = 'base' opts['saltenv'] = 'base'
pillar_override = kwargs.get('pillar') pillar_override = kwargs.get('pillar')
pillar_enc = kwargs.get('pillar_enc') pillar_enc = kwargs.get('pillar_enc')
@ -1540,7 +1544,7 @@ def sls_id(id_, mods, test=None, queue=False, **kwargs):
split_mods = mods.split(',') split_mods = mods.split(',')
st_.push_active() st_.push_active()
try: try:
high_, errors = st_.render_highstate({opts['environment']: split_mods}) high_, errors = st_.render_highstate({opts['saltenv']: split_mods})
finally: finally:
st_.pop_active() st_.pop_active()
errors += st_.state.verify_high(high_) errors += st_.state.verify_high(high_)
@ -1566,7 +1570,7 @@ def sls_id(id_, mods, test=None, queue=False, **kwargs):
if not ret: if not ret:
raise SaltInvocationError( raise SaltInvocationError(
'No matches for ID \'{0}\' found in SLS \'{1}\' within saltenv ' 'No matches for ID \'{0}\' found in SLS \'{1}\' within saltenv '
'\'{2}\''.format(id_, mods, opts['environment']) '\'{2}\''.format(id_, mods, opts['saltenv'])
) )
return ret return ret
@ -1617,8 +1621,8 @@ def show_low_sls(mods, test=None, queue=False, **kwargs):
# Since this is dealing with a specific SLS file (or files), fall back to # Since this is dealing with a specific SLS file (or files), fall back to
# the 'base' saltenv if none is configured and none was passed. # the 'base' saltenv if none is configured and none was passed.
if opts['environment'] is None: if opts['saltenv'] is None:
opts['environment'] = 'base' opts['saltenv'] = 'base'
pillar_override = kwargs.get('pillar') pillar_override = kwargs.get('pillar')
pillar_enc = kwargs.get('pillar_enc') pillar_enc = kwargs.get('pillar_enc')
@ -1649,7 +1653,7 @@ def show_low_sls(mods, test=None, queue=False, **kwargs):
mods = mods.split(',') mods = mods.split(',')
st_.push_active() st_.push_active()
try: try:
high_, errors = st_.render_highstate({opts['environment']: mods}) high_, errors = st_.render_highstate({opts['saltenv']: mods})
finally: finally:
st_.pop_active() st_.pop_active()
errors += st_.state.verify_high(high_) errors += st_.state.verify_high(high_)
@ -1688,7 +1692,7 @@ def show_sls(mods, test=None, queue=False, **kwargs):
.. code-block:: bash .. code-block:: bash
salt '*' state.show_sls core,edit.vim dev salt '*' state.show_sls core,edit.vim saltenv=dev
''' '''
if 'env' in kwargs: if 'env' in kwargs:
# "env" is not supported; Use "saltenv". # "env" is not supported; Use "saltenv".
@ -1704,8 +1708,8 @@ def show_sls(mods, test=None, queue=False, **kwargs):
# Since this is dealing with a specific SLS file (or files), fall back to # Since this is dealing with a specific SLS file (or files), fall back to
# the 'base' saltenv if none is configured and none was passed. # the 'base' saltenv if none is configured and none was passed.
if opts['environment'] is None: if opts['saltenv'] is None:
opts['environment'] = 'base' opts['saltenv'] = 'base'
pillar_override = kwargs.get('pillar') pillar_override = kwargs.get('pillar')
pillar_enc = kwargs.get('pillar_enc') pillar_enc = kwargs.get('pillar_enc')
@ -1738,7 +1742,7 @@ def show_sls(mods, test=None, queue=False, **kwargs):
mods = mods.split(',') mods = mods.split(',')
st_.push_active() st_.push_active()
try: try:
high_, errors = st_.render_highstate({opts['environment']: mods}) high_, errors = st_.render_highstate({opts['saltenv']: mods})
finally: finally:
st_.pop_active() st_.pop_active()
errors += st_.state.verify_high(high_) errors += st_.state.verify_high(high_)
@ -1904,6 +1908,7 @@ def pkg(pkg_path,
salt '*' state.pkg /tmp/salt_state.tgz 760a9353810e36f6d81416366fc426dc md5 salt '*' state.pkg /tmp/salt_state.tgz 760a9353810e36f6d81416366fc426dc md5
''' '''
# TODO - Add ability to download from salt master or other source # TODO - Add ability to download from salt master or other source
popts = _get_opts(**kwargs)
if not os.path.isfile(pkg_path): if not os.path.isfile(pkg_path):
return {} return {}
if not salt.utils.hashutils.get_hash(pkg_path, hash_type) == pkg_sum: if not salt.utils.hashutils.get_hash(pkg_path, hash_type) == pkg_sum:
@ -1938,7 +1943,6 @@ def pkg(pkg_path,
with salt.utils.files.fopen(roster_grains_json, 'r') as fp_: with salt.utils.files.fopen(roster_grains_json, 'r') as fp_:
roster_grains = json.load(fp_, object_hook=salt.utils.data.decode_dict) roster_grains = json.load(fp_, object_hook=salt.utils.data.decode_dict)
popts = _get_opts(**kwargs)
if os.path.isfile(roster_grains_json): if os.path.isfile(roster_grains_json):
popts['grains'] = roster_grains popts['grains'] = roster_grains
popts['fileclient'] = 'local' popts['fileclient'] = 'local'

View File

@ -399,6 +399,14 @@ def _systemd_scope():
and __salt__['config.get']('systemd.scope', True) and __salt__['config.get']('systemd.scope', True)
def _clean_cache():
'''
Clean cached results
'''
for cache_name in ['pkg.list_pkgs', 'pkg.list_provides']:
__context__.pop(cache_name, None)
def list_upgrades(refresh=True, **kwargs): def list_upgrades(refresh=True, **kwargs):
''' '''
List all available package upgrades on this system List all available package upgrades on this system
@ -1049,6 +1057,10 @@ def install(name=None,
operator (<, >, <=, >=, =) and a version number (ex. '>1.2.3-4'). operator (<, >, <=, >=, =) and a version number (ex. '>1.2.3-4').
This parameter is ignored if ``pkgs`` or ``sources`` is passed. This parameter is ignored if ``pkgs`` or ``sources`` is passed.
resolve_capabilities
If this option is set to True zypper will take capabilites into
account. In this case names which are just provided by a package
will get installed. Default is False.
Multiple Package Installation Options: Multiple Package Installation Options:
@ -1164,7 +1176,10 @@ def install(name=None,
log.info('Targeting repo \'{0}\''.format(fromrepo)) log.info('Targeting repo \'{0}\''.format(fromrepo))
else: else:
fromrepoopt = '' fromrepoopt = ''
cmd_install = ['install', '--name', '--auto-agree-with-licenses'] cmd_install = ['install', '--auto-agree-with-licenses']
cmd_install.append(kwargs.get('resolve_capabilities') and '--capability' or '--name')
if not refresh: if not refresh:
cmd_install.insert(0, '--no-refresh') cmd_install.insert(0, '--no-refresh')
if skip_verify: if skip_verify:
@ -1194,7 +1209,7 @@ def install(name=None,
downgrades = downgrades[500:] downgrades = downgrades[500:]
__zypper__(no_repo_failure=ignore_repo_failure).call(*cmd) __zypper__(no_repo_failure=ignore_repo_failure).call(*cmd)
__context__.pop('pkg.list_pkgs', None) _clean_cache()
new = list_pkgs(attr=diff_attr) if not downloadonly else list_downloaded() new = list_pkgs(attr=diff_attr) if not downloadonly else list_downloaded()
# Handle packages which report multiple new versions # Handle packages which report multiple new versions
@ -1311,7 +1326,7 @@ def upgrade(refresh=True,
old = list_pkgs() old = list_pkgs()
__zypper__(systemd_scope=_systemd_scope()).noraise.call(*cmd_update) __zypper__(systemd_scope=_systemd_scope()).noraise.call(*cmd_update)
__context__.pop('pkg.list_pkgs', None) _clean_cache()
new = list_pkgs() new = list_pkgs()
# Handle packages which report multiple new versions # Handle packages which report multiple new versions
@ -1360,7 +1375,7 @@ def _uninstall(name=None, pkgs=None):
__zypper__(systemd_scope=systemd_scope).call('remove', *targets[:500]) __zypper__(systemd_scope=systemd_scope).call('remove', *targets[:500])
targets = targets[500:] targets = targets[500:]
__context__.pop('pkg.list_pkgs', None) _clean_cache()
ret = salt.utils.data.compare_dicts(old, list_pkgs()) ret = salt.utils.data.compare_dicts(old, list_pkgs())
if errors: if errors:
@ -1750,7 +1765,7 @@ def list_installed_patterns():
return _get_patterns(installed_only=True) return _get_patterns(installed_only=True)
def search(criteria, refresh=False): def search(criteria, refresh=False, **kwargs):
''' '''
List known packags, available to the system. List known packags, available to the system.
@ -1759,26 +1774,94 @@ def search(criteria, refresh=False):
If set to False (default) it depends on zypper if a refresh is If set to False (default) it depends on zypper if a refresh is
executed. executed.
match (str)
One of `exact`, `words`, `substrings`. Search for an `exact` match
or for the whole `words` only. Default to `substrings` to patch
partial words.
provides (bool)
Search for packages which provide the search strings.
recommends (bool)
Search for packages which recommend the search strings.
requires (bool)
Search for packages which require the search strings.
suggests (bool)
Search for packages which suggest the search strings.
conflicts (bool)
Search packages conflicting with search strings.
obsoletes (bool)
Search for packages which obsolete the search strings.
file_list (bool)
Search for a match in the file list of packages.
search_descriptions (bool)
Search also in package summaries and descriptions.
case_sensitive (bool)
Perform case-sensitive search.
installed_only (bool)
Show only installed packages.
not_installed_only (bool)
Show only packages which are not installed.
details (bool)
Show version and repository
CLI Examples: CLI Examples:
.. code-block:: bash .. code-block:: bash
salt '*' pkg.search <criteria> salt '*' pkg.search <criteria>
''' '''
ALLOWED_SEARCH_OPTIONS = {
'provides': '--provides',
'recommends': '--recommends',
'requires': '--requires',
'suggests': '--suggests',
'conflicts': '--conflicts',
'obsoletes': '--obsoletes',
'file_list': '--file-list',
'search_descriptions': '--search-descriptions',
'case_sensitive': '--case-sensitive',
'installed_only': '--installed-only',
'not_installed_only': '-u',
'details': '--details'
}
if refresh: if refresh:
refresh_db() refresh_db()
solvables = __zypper__.nolock.xml.call('se', criteria).getElementsByTagName('solvable') cmd = ['search']
if kwargs.get('match') == 'exact':
cmd.append('--match-exact')
elif kwargs.get('match') == 'words':
cmd.append('--match-words')
elif kwargs.get('match') == 'substrings':
cmd.append('--match-substrings')
for opt in kwargs:
if opt in ALLOWED_SEARCH_OPTIONS:
cmd.append(ALLOWED_SEARCH_OPTIONS.get(opt))
cmd.append(criteria)
solvables = __zypper__.nolock.noraise.xml.call(*cmd).getElementsByTagName('solvable')
if not solvables: if not solvables:
raise CommandExecutionError( raise CommandExecutionError(
'No packages found matching \'{0}\''.format(criteria) 'No packages found matching \'{0}\''.format(criteria)
) )
out = {} out = {}
for solvable in [slv for slv in solvables for solvable in solvables:
if slv.getAttribute('status') == 'not-installed' out[solvable.getAttribute('name')] = dict()
and slv.getAttribute('kind') == 'package']: for k, v in solvable.attributes.items():
out[solvable.getAttribute('name')] = {'summary': solvable.getAttribute('summary')} out[solvable.getAttribute('name')][k] = v
return out return out
@ -2033,3 +2116,97 @@ def list_installed_patches():
salt '*' pkg.list_installed_patches salt '*' pkg.list_installed_patches
''' '''
return _get_patches(installed_only=True) return _get_patches(installed_only=True)
def list_provides(**kwargs):
'''
.. versionadded:: Oxygen
List package provides of installed packages as a dict.
{'<provided_name>': ['<package_name>', '<package_name>', ...]}
CLI Examples:
.. code-block:: bash
salt '*' pkg.list_provides
'''
ret = __context__.get('pkg.list_provides')
if not ret:
cmd = ['rpm', '-qa', '--queryformat', '[%{PROVIDES}_|-%{NAME}\n]']
ret = dict()
for line in __salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=False).splitlines():
provide, realname = line.split('_|-')
if provide == realname:
continue
if provide not in ret:
ret[provide] = list()
ret[provide].append(realname)
__context__['pkg.list_provides'] = ret
return ret
def resolve_capabilities(pkgs, refresh, **kwargs):
'''
.. versionadded:: Oxygen
Convert name provides in ``pkgs`` into real package names if
``resolve_capabilities`` parameter is set to True. In case of
``resolve_capabilities`` is set to False the package list
is returned unchanged.
refresh
force a refresh if set to True.
If set to False (default) it depends on zypper if a refresh is
executed.
resolve_capabilities
If this option is set to True the input will be checked if
a package with this name exists. If not, this function will
search for a package which provides this name. If one is found
the output is exchanged with the real package name.
In case this option is set to False (Default) the input will
be returned unchanged.
CLI Examples:
.. code-block:: bash
salt '*' pkg.resolve_capabilities resolve_capabilities=True w3m_ssl
'''
if refresh:
refresh_db()
ret = list()
for pkg in pkgs:
if isinstance(pkg, dict):
name = next(iter(pkg))
version = pkg[name]
else:
name = pkg
version = None
if kwargs.get('resolve_capabilities', False):
try:
search(name, match='exact')
except CommandExecutionError:
# no package this such a name found
# search for a package which provides this name
try:
result = search(name, provides=True, match='exact')
if len(result) == 1:
name = result.keys()[0]
elif len(result) > 1:
log.warn("Found ambiguous match for capability '{0}'.".format(pkg))
except CommandExecutionError as exc:
# when search throws an exception stay with original name and version
log.debug("Search failed with: {0}".format(exc))
if version:
ret.append({name: version})
else:
ret.append(name)
return ret

View File

@ -138,7 +138,7 @@ class AsyncRemotePillar(RemotePillarMixin):
def __init__(self, opts, grains, minion_id, saltenv, ext=None, functions=None, def __init__(self, opts, grains, minion_id, saltenv, ext=None, functions=None,
pillar_override=None, pillarenv=None, extra_minion_data=None): pillar_override=None, pillarenv=None, extra_minion_data=None):
self.opts = opts self.opts = opts
self.opts['environment'] = saltenv self.opts['saltenv'] = saltenv
self.ext = ext self.ext = ext
self.grains = grains self.grains = grains
self.minion_id = minion_id self.minion_id = minion_id
@ -165,7 +165,7 @@ class AsyncRemotePillar(RemotePillarMixin):
''' '''
load = {'id': self.minion_id, load = {'id': self.minion_id,
'grains': self.grains, 'grains': self.grains,
'saltenv': self.opts['environment'], 'saltenv': self.opts['saltenv'],
'pillarenv': self.opts['pillarenv'], 'pillarenv': self.opts['pillarenv'],
'pillar_override': self.pillar_override, 'pillar_override': self.pillar_override,
'extra_minion_data': self.extra_minion_data, 'extra_minion_data': self.extra_minion_data,
@ -198,7 +198,7 @@ class RemotePillar(RemotePillarMixin):
def __init__(self, opts, grains, minion_id, saltenv, ext=None, functions=None, def __init__(self, opts, grains, minion_id, saltenv, ext=None, functions=None,
pillar_override=None, pillarenv=None, extra_minion_data=None): pillar_override=None, pillarenv=None, extra_minion_data=None):
self.opts = opts self.opts = opts
self.opts['environment'] = saltenv self.opts['saltenv'] = saltenv
self.ext = ext self.ext = ext
self.grains = grains self.grains = grains
self.minion_id = minion_id self.minion_id = minion_id
@ -224,7 +224,7 @@ class RemotePillar(RemotePillarMixin):
''' '''
load = {'id': self.minion_id, load = {'id': self.minion_id,
'grains': self.grains, 'grains': self.grains,
'saltenv': self.opts['environment'], 'saltenv': self.opts['saltenv'],
'pillarenv': self.opts['pillarenv'], 'pillarenv': self.opts['pillarenv'],
'pillar_override': self.pillar_override, 'pillar_override': self.pillar_override,
'extra_minion_data': self.extra_minion_data, 'extra_minion_data': self.extra_minion_data,
@ -445,9 +445,9 @@ class Pillar(object):
else: else:
opts['grains'] = grains opts['grains'] = grains
# Allow minion/CLI saltenv/pillarenv to take precedence over master # Allow minion/CLI saltenv/pillarenv to take precedence over master
opts['environment'] = saltenv \ opts['saltenv'] = saltenv \
if saltenv is not None \ if saltenv is not None \
else opts.get('environment') else opts.get('saltenv')
opts['pillarenv'] = pillarenv \ opts['pillarenv'] = pillarenv \
if pillarenv is not None \ if pillarenv is not None \
else opts.get('pillarenv') else opts.get('pillarenv')

View File

@ -404,7 +404,7 @@ def ext_pillar(minion_id, pillar, *repos): # pylint: disable=unused-argument
# Map env if env == '__env__' before checking the env value # Map env if env == '__env__' before checking the env value
if env == '__env__': if env == '__env__':
env = opts.get('pillarenv') \ env = opts.get('pillarenv') \
or opts.get('environment') \ or opts.get('saltenv') \
or opts.get('git_pillar_base') or opts.get('git_pillar_base')
log.debug('__env__ maps to %s', env) log.debug('__env__ maps to %s', env)

View File

@ -763,7 +763,7 @@ class State(object):
self.opts, self.opts,
self.opts[u'grains'], self.opts[u'grains'],
self.opts[u'id'], self.opts[u'id'],
self.opts[u'environment'], self.opts[u'saltenv'],
pillar_override=self._pillar_override, pillar_override=self._pillar_override,
pillarenv=self.opts.get(u'pillarenv')) pillarenv=self.opts.get(u'pillarenv'))
return pillar.compile_pillar() return pillar.compile_pillar()
@ -1892,20 +1892,27 @@ class State(object):
(u'onlyif' in low and u'{0[state]}.mod_run_check'.format(low) not in self.states): (u'onlyif' in low and u'{0[state]}.mod_run_check'.format(low) not in self.states):
ret.update(self._run_check(low)) ret.update(self._run_check(low))
if u'saltenv' in low: if not self.opts.get(u'lock_saltenv', False):
inject_globals[u'__env__'] = six.text_type(low[u'saltenv']) # NOTE: Overriding the saltenv when lock_saltenv is blocked in
elif isinstance(cdata[u'kwargs'].get(u'env', None), six.string_types): # salt/modules/state.py, before we ever get here, but this
# User is using a deprecated env setting which was parsed by # additional check keeps use of the State class outside of the
# format_call. # salt/modules/state.py from getting around this setting.
# We check for a string type since module functions which if u'saltenv' in low:
# allow setting the OS environ also make use of the "env" inject_globals[u'__env__'] = six.text_type(low[u'saltenv'])
# keyword argument, which is not a string elif isinstance(cdata[u'kwargs'].get(u'env', None), six.string_types):
inject_globals[u'__env__'] = six.text_type(cdata[u'kwargs'][u'env']) # User is using a deprecated env setting which was parsed by
elif u'__env__' in low: # format_call.
# The user is passing an alternative environment using __env__ # We check for a string type since module functions which
# which is also not the appropriate choice, still, handle it # allow setting the OS environ also make use of the "env"
inject_globals[u'__env__'] = six.text_type(low[u'__env__']) # keyword argument, which is not a string
else: inject_globals[u'__env__'] = six.text_type(cdata[u'kwargs'][u'env'])
elif u'__env__' in low:
# The user is passing an alternative environment using
# __env__ which is also not the appropriate choice, still,
# handle it
inject_globals[u'__env__'] = six.text_type(low[u'__env__'])
if u'__env__' not in inject_globals:
# Let's use the default environment # Let's use the default environment
inject_globals[u'__env__'] = u'base' inject_globals[u'__env__'] = u'base'
@ -2952,32 +2959,32 @@ class BaseHighState(object):
found = 0 # did we find any contents in the top files? found = 0 # did we find any contents in the top files?
# Gather initial top files # Gather initial top files
merging_strategy = self.opts[u'top_file_merging_strategy'] merging_strategy = self.opts[u'top_file_merging_strategy']
if merging_strategy == u'same' and not self.opts[u'environment']: if merging_strategy == u'same' and not self.opts[u'saltenv']:
if not self.opts[u'default_top']: if not self.opts[u'default_top']:
raise SaltRenderError( raise SaltRenderError(
u'top_file_merging_strategy set to \'same\', but no ' u'top_file_merging_strategy set to \'same\', but no '
u'default_top configuration option was set' u'default_top configuration option was set'
) )
if self.opts[u'environment']: if self.opts[u'saltenv']:
contents = self.client.cache_file( contents = self.client.cache_file(
self.opts[u'state_top'], self.opts[u'state_top'],
self.opts[u'environment'] self.opts[u'saltenv']
) )
if contents: if contents:
found = 1 found = 1
tops[self.opts[u'environment']] = [ tops[self.opts[u'saltenv']] = [
compile_template( compile_template(
contents, contents,
self.state.rend, self.state.rend,
self.state.opts[u'renderer'], self.state.opts[u'renderer'],
self.state.opts[u'renderer_blacklist'], self.state.opts[u'renderer_blacklist'],
self.state.opts[u'renderer_whitelist'], self.state.opts[u'renderer_whitelist'],
saltenv=self.opts[u'environment'] saltenv=self.opts[u'saltenv']
) )
] ]
else: else:
tops[self.opts[u'environment']] = [{}] tops[self.opts[u'saltenv']] = [{}]
else: else:
found = 0 found = 0
@ -3309,8 +3316,8 @@ class BaseHighState(object):
matches = DefaultOrderedDict(OrderedDict) matches = DefaultOrderedDict(OrderedDict)
# pylint: disable=cell-var-from-loop # pylint: disable=cell-var-from-loop
for saltenv, body in six.iteritems(top): for saltenv, body in six.iteritems(top):
if self.opts[u'environment']: if self.opts[u'saltenv']:
if saltenv != self.opts[u'environment']: if saltenv != self.opts[u'saltenv']:
continue continue
for match, data in six.iteritems(body): for match, data in six.iteritems(body):
def _filter_matches(_match, _data, _opts): def _filter_matches(_match, _data, _opts):

View File

@ -508,8 +508,10 @@ def _find_install_targets(name=None,
# add it to the kwargs. # add it to the kwargs.
kwargs['refresh'] = refresh kwargs['refresh'] = refresh
resolve_capabilities = kwargs.get('resolve_capabilities', False) and 'pkg.list_provides' in __salt__
try: try:
cur_pkgs = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs) cur_pkgs = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs)
cur_prov = resolve_capabilities and __salt__['pkg.list_provides'](**kwargs) or dict()
except CommandExecutionError as exc: except CommandExecutionError as exc:
return {'name': name, return {'name': name,
'changes': {}, 'changes': {},
@ -669,6 +671,9 @@ def _find_install_targets(name=None,
failed_verify = False failed_verify = False
for key, val in six.iteritems(desired): for key, val in six.iteritems(desired):
cver = cur_pkgs.get(key, []) cver = cur_pkgs.get(key, [])
if resolve_capabilities and not cver and key in cur_prov:
cver = cur_pkgs.get(cur_prov.get(key)[0], [])
# Package not yet installed, so add to targets # Package not yet installed, so add to targets
if not cver: if not cver:
targets[key] = val targets[key] = val
@ -786,13 +791,15 @@ def _find_install_targets(name=None,
warnings, was_refreshed) warnings, was_refreshed)
def _verify_install(desired, new_pkgs, ignore_epoch=False): def _verify_install(desired, new_pkgs, ignore_epoch=False, new_caps=None):
''' '''
Determine whether or not the installed packages match what was requested in Determine whether or not the installed packages match what was requested in
the SLS file. the SLS file.
''' '''
ok = [] ok = []
failed = [] failed = []
if not new_caps:
new_caps = dict()
for pkgname, pkgver in desired.items(): for pkgname, pkgver in desired.items():
# FreeBSD pkg supports `openjdk` and `java/openjdk7` package names. # FreeBSD pkg supports `openjdk` and `java/openjdk7` package names.
# Homebrew for Mac OSX does something similar with tap names # Homebrew for Mac OSX does something similar with tap names
@ -809,6 +816,8 @@ def _verify_install(desired, new_pkgs, ignore_epoch=False):
cver = new_pkgs.get(pkgname.split('=')[0]) cver = new_pkgs.get(pkgname.split('=')[0])
else: else:
cver = new_pkgs.get(pkgname) cver = new_pkgs.get(pkgname)
if not cver and pkgname in new_caps:
cver = new_pkgs.get(new_caps.get(pkgname)[0])
if not cver: if not cver:
failed.append(pkgname) failed.append(pkgname)
@ -873,6 +882,26 @@ def _nested_output(obj):
return ret return ret
def _resolve_capabilities(pkgs, refresh=False, **kwargs):
'''
Resolve capabilities in ``pkgs`` and exchange them with real package
names, when the result is distinct.
This feature can be turned on while setting the paramter
``resolve_capabilities`` to True.
Return the input dictionary with replaced capability names and as
second return value a bool which say if a refresh need to be run.
In case of ``resolve_capabilities`` is False (disabled) or not
supported by the implementation the input is returned unchanged.
'''
if not pkgs or 'pkg.resolve_capabilities' not in __salt__:
return pkgs, refresh
ret = __salt__['pkg.resolve_capabilities'](pkgs, refresh=refresh, **kwargs)
return ret, False
def installed( def installed(
name, name,
version=None, version=None,
@ -1105,6 +1134,11 @@ def installed(
.. versionadded:: 2014.1.1 .. versionadded:: 2014.1.1
:param bool resolve_capabilities:
Turn on resolving capabilities. This allow to name "provides" or alias names for packages.
.. versionadded:: Oxygen
:param bool allow_updates: :param bool allow_updates:
Allow the package to be updated outside Salt's control (e.g. auto Allow the package to be updated outside Salt's control (e.g. auto
updates on Windows). This means a package on the Minion can have a updates on Windows). This means a package on the Minion can have a
@ -1448,6 +1482,12 @@ def installed(
kwargs['saltenv'] = __env__ kwargs['saltenv'] = __env__
refresh = salt.utils.pkg.check_refresh(__opts__, refresh) refresh = salt.utils.pkg.check_refresh(__opts__, refresh)
# check if capabilities should be checked and modify the requested packages
# accordingly.
if pkgs:
pkgs, refresh = _resolve_capabilities(pkgs, refresh=refresh, **kwargs)
if not isinstance(pkg_verify, list): if not isinstance(pkg_verify, list):
pkg_verify = pkg_verify is True pkg_verify = pkg_verify is True
if (pkg_verify or isinstance(pkg_verify, list)) \ if (pkg_verify or isinstance(pkg_verify, list)) \
@ -1707,8 +1747,13 @@ def installed(
if __grains__['os'] == 'FreeBSD': if __grains__['os'] == 'FreeBSD':
kwargs['with_origin'] = True kwargs['with_origin'] = True
new_pkgs = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs) new_pkgs = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs)
if kwargs.get('resolve_capabilities', False) and 'pkg.list_provides' in __salt__:
new_caps = __salt__['pkg.list_provides'](**kwargs)
else:
new_caps = {}
ok, failed = _verify_install(desired, new_pkgs, ok, failed = _verify_install(desired, new_pkgs,
ignore_epoch=ignore_epoch) ignore_epoch=ignore_epoch,
new_caps=new_caps)
modified = [x for x in ok if x in targets] modified = [x for x in ok if x in targets]
not_modified = [x for x in ok not_modified = [x for x in ok
if x not in targets if x not in targets
@ -1927,6 +1972,11 @@ def downloaded(name,
- dos2unix - dos2unix
- salt-minion: 2015.8.5-1.el6 - salt-minion: 2015.8.5-1.el6
:param bool resolve_capabilities:
Turn on resolving capabilities. This allow to name "provides" or alias names for packages.
.. versionadded:: Oxygen
CLI Example: CLI Example:
.. code-block:: yaml .. code-block:: yaml
@ -1952,11 +2002,22 @@ def downloaded(name,
ret['comment'] = 'No packages to download provided' ret['comment'] = 'No packages to download provided'
return ret return ret
# If just a name (and optionally a version) is passed, just pack them into
# the pkgs argument.
if name and not pkgs:
if version:
pkgs = [{name: version}]
version = None
else:
pkgs = [name]
# It doesn't make sense here to received 'downloadonly' as kwargs # It doesn't make sense here to received 'downloadonly' as kwargs
# as we're explicitely passing 'downloadonly=True' to execution module. # as we're explicitely passing 'downloadonly=True' to execution module.
if 'downloadonly' in kwargs: if 'downloadonly' in kwargs:
del kwargs['downloadonly'] del kwargs['downloadonly']
pkgs, _refresh = _resolve_capabilities(pkgs, **kwargs)
# Only downloading not yet downloaded packages # Only downloading not yet downloaded packages
targets = _find_download_targets(name, targets = _find_download_targets(name,
version, version,
@ -2203,6 +2264,10 @@ def latest(
This parameter is available only on Debian based distributions and This parameter is available only on Debian based distributions and
has no effect on the rest. has no effect on the rest.
:param bool resolve_capabilities:
Turn on resolving capabilities. This allow to name "provides" or alias names for packages.
.. versionadded:: Oxygen
Multiple Package Installation Options: Multiple Package Installation Options:
@ -2300,6 +2365,10 @@ def latest(
kwargs['saltenv'] = __env__ kwargs['saltenv'] = __env__
# check if capabilities should be checked and modify the requested packages
# accordingly.
desired_pkgs, refresh = _resolve_capabilities(desired_pkgs, refresh=refresh, **kwargs)
try: try:
avail = __salt__['pkg.latest_version'](*desired_pkgs, avail = __salt__['pkg.latest_version'](*desired_pkgs,
fromrepo=fromrepo, fromrepo=fromrepo,
@ -2822,6 +2891,11 @@ def uptodate(name, refresh=False, pkgs=None, **kwargs):
This parameter available only on Debian based distributions, and This parameter available only on Debian based distributions, and
have no effect on the rest. have no effect on the rest.
:param bool resolve_capabilities:
Turn on resolving capabilities. This allow to name "provides" or alias names for packages.
.. versionadded:: Oxygen
kwargs kwargs
Any keyword arguments to pass through to ``pkg.upgrade``. Any keyword arguments to pass through to ``pkg.upgrade``.
@ -2842,6 +2916,7 @@ def uptodate(name, refresh=False, pkgs=None, **kwargs):
return ret return ret
if isinstance(refresh, bool): if isinstance(refresh, bool):
pkgs, refresh = _resolve_capabilities(pkgs, refresh=refresh, **kwargs)
try: try:
packages = __salt__['pkg.list_upgrades'](refresh=refresh, **kwargs) packages = __salt__['pkg.list_upgrades'](refresh=refresh, **kwargs)
if isinstance(pkgs, list): if isinstance(pkgs, list):

View File

@ -351,7 +351,6 @@ def state(name,
changes = {} changes = {}
fail = set() fail = set()
failures = {}
no_change = set() no_change = set()
if fail_minions is None: if fail_minions is None:
@ -393,7 +392,7 @@ def state(name,
if not m_state: if not m_state:
if minion not in fail_minions: if minion not in fail_minions:
fail.add(minion) fail.add(minion)
failures[minion] = m_ret or 'Minion did not respond' changes[minion] = m_ret
continue continue
try: try:
for state_item in six.itervalues(m_ret): for state_item in six.itervalues(m_ret):
@ -418,18 +417,6 @@ def state(name,
state_ret['comment'] += ' Updating {0}.'.format(', '.join(changes)) state_ret['comment'] += ' Updating {0}.'.format(', '.join(changes))
if no_change: if no_change:
state_ret['comment'] += ' No changes made to {0}.'.format(', '.join(no_change)) state_ret['comment'] += ' No changes made to {0}.'.format(', '.join(no_change))
if failures:
state_ret['comment'] += '\nFailures:\n'
for minion, failure in six.iteritems(failures):
state_ret['comment'] += '\n'.join(
(' ' * 4 + l)
for l in salt.output.out_format(
{minion: failure},
'highstate',
__opts__,
).splitlines()
)
state_ret['comment'] += '\n'
if test or __opts__.get('test'): if test or __opts__.get('test'):
if state_ret['changes'] and state_ret['result'] is True: if state_ret['changes'] and state_ret['result'] is True:
# Test mode with changes is the only case where result should ever be none # Test mode with changes is the only case where result should ever be none
@ -570,7 +557,6 @@ def function(
changes = {} changes = {}
fail = set() fail = set()
failures = {}
if fail_minions is None: if fail_minions is None:
fail_minions = () fail_minions = ()
@ -598,7 +584,7 @@ def function(
if not m_func: if not m_func:
if minion not in fail_minions: if minion not in fail_minions:
fail.add(minion) fail.add(minion)
failures[minion] = m_ret and m_ret or 'Minion did not respond' changes[minion] = m_ret
continue continue
changes[minion] = m_ret changes[minion] = m_ret
if not cmd_ret: if not cmd_ret:
@ -614,18 +600,6 @@ def function(
func_ret['comment'] = 'Function ran successfully.' func_ret['comment'] = 'Function ran successfully.'
if changes: if changes:
func_ret['comment'] += ' Function {0} ran on {1}.'.format(name, ', '.join(changes)) func_ret['comment'] += ' Function {0} ran on {1}.'.format(name, ', '.join(changes))
if failures:
func_ret['comment'] += '\nFailures:\n'
for minion, failure in six.iteritems(failures):
func_ret['comment'] += '\n'.join(
(' ' * 4 + l)
for l in salt.output.out_format(
{minion: failure},
'highstate',
__opts__,
).splitlines()
)
func_ret['comment'] += '\n'
return func_ret return func_ret

View File

@ -910,7 +910,7 @@ class GitProvider(object):
''' '''
if self.branch == '__env__': if self.branch == '__env__':
target = self.opts.get('pillarenv') \ target = self.opts.get('pillarenv') \
or self.opts.get('environment') \ or self.opts.get('saltenv') \
or 'base' or 'base'
return self.opts['{0}_base'.format(self.role)] \ return self.opts['{0}_base'.format(self.role)] \
if target == 'base' \ if target == 'base' \

View File

@ -125,3 +125,26 @@ def modules_available(*names):
if not fnmatch.filter(list(__salt__), name): if not fnmatch.filter(list(__salt__), name):
not_found.append(name) not_found.append(name)
return not_found return not_found
def nonzero_retcode_return_true():
'''
Sets a nonzero retcode before returning. Designed to test orchestration.
'''
__context__['retcode'] = 1
return True
def nonzero_retcode_return_false():
'''
Sets a nonzero retcode before returning. Designed to test orchestration.
'''
__context__['retcode'] = 1
return False
def fail_function(*args, **kwargs): # pylint: disable=unused-argument
'''
Return False no matter what is passed to it
'''
return False

View File

@ -0,0 +1,2 @@
test fail with changes:
test.fail_with_changes

View File

@ -0,0 +1,11 @@
Step01:
salt.state:
- tgt: 'minion'
- sls:
- orch.issue43204.fail_with_changes
Step02:
salt.function:
- name: runtests_helpers.nonzero_retcode_return_false
- tgt: 'minion'
- fail_function: runtests_helpers.fail_function

View File

@ -6,6 +6,7 @@ Tests for the state runner
# Import Python Libs # Import Python Libs
from __future__ import absolute_import from __future__ import absolute_import
import errno import errno
import json
import os import os
import shutil import shutil
import signal import signal
@ -81,6 +82,58 @@ class StateRunnerTest(ShellCase):
self.assertFalse(os.path.exists('/tmp/ewu-2016-12-13')) self.assertFalse(os.path.exists('/tmp/ewu-2016-12-13'))
self.assertNotEqual(code, 0) self.assertNotEqual(code, 0)
def test_orchestrate_state_and_function_failure(self):
'''
Ensure that returns from failed minions are in the changes dict where
they belong, so they can be programatically analyzed.
See https://github.com/saltstack/salt/issues/43204
'''
self.run_run('saltutil.sync_modules')
ret = json.loads(
'\n'.join(
self.run_run(u'state.orchestrate orch.issue43204 --out=json')
)
)
# Drill down to the changes dict
state_ret = ret[u'data'][u'master'][u'salt_|-Step01_|-Step01_|-state'][u'changes']
func_ret = ret[u'data'][u'master'][u'salt_|-Step02_|-runtests_helpers.nonzero_retcode_return_false_|-function'][u'changes']
# Remove duration and start time from the results, since they would
# vary with each run and that would make it impossible to test.
for item in ('duration', 'start_time'):
state_ret['ret']['minion']['test_|-test fail with changes_|-test fail with changes_|-fail_with_changes'].pop(item)
self.assertEqual(
state_ret,
{
u'out': u'highstate',
u'ret': {
u'minion': {
u'test_|-test fail with changes_|-test fail with changes_|-fail_with_changes': {
u'__id__': u'test fail with changes',
u'__run_num__': 0,
u'__sls__': u'orch.issue43204.fail_with_changes',
u'changes': {
u'testing': {
u'new': u'Something pretended to change',
u'old': u'Unchanged'
}
},
u'comment': u'Failure!',
u'name': u'test fail with changes',
u'result': False,
}
}
}
}
)
self.assertEqual(
func_ret,
{u'out': u'highstate', u'ret': {u'minion': False}}
)
def test_orchestrate_target_exists(self): def test_orchestrate_target_exists(self):
''' '''
test orchestration when target exists test orchestration when target exists

View File

@ -37,11 +37,15 @@ _PKG_TARGETS = {
'Debian': ['python-plist', 'apg'], 'Debian': ['python-plist', 'apg'],
'RedHat': ['units', 'zsh-html'], 'RedHat': ['units', 'zsh-html'],
'FreeBSD': ['aalib', 'pth'], 'FreeBSD': ['aalib', 'pth'],
'Suse': ['aalib', 'python-pssh'], 'Suse': ['aalib', 'rpm-python'],
'MacOS': ['libpng', 'jpeg'], 'MacOS': ['libpng', 'jpeg'],
'Windows': ['firefox', '7zip'], 'Windows': ['firefox', '7zip'],
} }
_PKG_CAP_TARGETS = {
'Suse': [('w3m_ssl', 'w3m')],
}
_PKG_TARGETS_32 = { _PKG_TARGETS_32 = {
'CentOS': 'xz-devel.i686' 'CentOS': 'xz-devel.i686'
} }
@ -793,3 +797,260 @@ class PkgTest(ModuleCase, SaltReturnAssertsMixin):
self.assertEqual(ret_comment, 'An error was encountered while installing/updating group ' self.assertEqual(ret_comment, 'An error was encountered while installing/updating group '
'\'handle_missing_pkg_group\': Group \'handle_missing_pkg_group\' ' '\'handle_missing_pkg_group\': Group \'handle_missing_pkg_group\' '
'not found.') 'not found.')
@skipIf(salt.utils.platform.is_windows(), 'minion is windows')
@requires_system_grains
def test_pkg_cap_001_installed(self, grains=None):
'''
This is a destructive test as it installs and then removes a package
'''
# Skip test if package manager not available
if not pkgmgr_avail(self.run_function, self.run_function('grains.items')):
self.skipTest('Package manager is not available')
os_family = grains.get('os_family', '')
pkg_cap_targets = _PKG_CAP_TARGETS.get(os_family, [])
if not len(pkg_cap_targets) > 0:
self.skipTest('Capability not provided')
target, realpkg = pkg_cap_targets[0]
version = self.run_function('pkg.version', [target])
realver = self.run_function('pkg.version', [realpkg])
# If this assert fails, we need to find new targets, this test needs to
# be able to test successful installation of packages, so this package
# needs to not be installed before we run the states below
self.assertFalse(version)
self.assertFalse(realver)
ret = self.run_state('pkg.installed', name=target, refresh=False, resolve_capabilities=True, test=True)
self.assertInSaltComment("The following packages would be installed/updated: {0}".format(realpkg), ret)
ret = self.run_state('pkg.installed', name=target, refresh=False, resolve_capabilities=True)
self.assertSaltTrueReturn(ret)
ret = self.run_state('pkg.removed', name=realpkg)
self.assertSaltTrueReturn(ret)
@skipIf(salt.utils.platform.is_windows(), 'minion is windows')
@requires_system_grains
def test_pkg_cap_002_already_installed(self, grains=None):
'''
This is a destructive test as it installs and then removes a package
'''
# Skip test if package manager not available
if not pkgmgr_avail(self.run_function, self.run_function('grains.items')):
self.skipTest('Package manager is not available')
os_family = grains.get('os_family', '')
pkg_cap_targets = _PKG_CAP_TARGETS.get(os_family, [])
if not len(pkg_cap_targets) > 0:
self.skipTest('Capability not provided')
target, realpkg = pkg_cap_targets[0]
version = self.run_function('pkg.version', [target])
realver = self.run_function('pkg.version', [realpkg])
# If this assert fails, we need to find new targets, this test needs to
# be able to test successful installation of packages, so this package
# needs to not be installed before we run the states below
self.assertFalse(version)
self.assertFalse(realver)
# install the package already
ret = self.run_state('pkg.installed', name=realpkg, refresh=False)
ret = self.run_state('pkg.installed', name=target, refresh=False, resolve_capabilities=True, test=True)
self.assertInSaltComment("All specified packages are already installed", ret)
ret = self.run_state('pkg.installed', name=target, refresh=False, resolve_capabilities=True)
self.assertSaltTrueReturn(ret)
self.assertInSaltComment("packages are already installed", ret)
ret = self.run_state('pkg.removed', name=realpkg)
self.assertSaltTrueReturn(ret)
@skipIf(salt.utils.platform.is_windows(), 'minion is windows')
@requires_system_grains
def test_pkg_cap_003_installed_multipkg_with_version(self, grains=None):
'''
This is a destructive test as it installs and then removes two packages
'''
# Skip test if package manager not available
if not pkgmgr_avail(self.run_function, self.run_function('grains.items')):
self.skipTest('Package manager is not available')
os_family = grains.get('os_family', '')
pkg_cap_targets = _PKG_CAP_TARGETS.get(os_family, [])
if not len(pkg_cap_targets) > 0:
self.skipTest('Capability not provided')
pkg_targets = _PKG_TARGETS.get(os_family, [])
# Don't perform this test on FreeBSD since version specification is not
# supported.
if os_family == 'FreeBSD':
return
# Make sure that we have targets that match the os_family. If this
# fails then the _PKG_TARGETS dict above needs to have an entry added,
# with two packages that are not installed before these tests are run
self.assertTrue(bool(pkg_cap_targets))
self.assertTrue(bool(pkg_targets))
if os_family == 'Arch':
for idx in range(13):
if idx == 12:
raise Exception('Package database locked after 60 seconds, '
'bailing out')
if not os.path.isfile('/var/lib/pacman/db.lck'):
break
time.sleep(5)
capability, realpkg = pkg_cap_targets[0]
version = latest_version(self.run_function, pkg_targets[0])
realver = latest_version(self.run_function, realpkg)
# If this assert fails, we need to find new targets, this test needs to
# be able to test successful installation of packages, so these
# packages need to not be installed before we run the states below
self.assertTrue(bool(version))
self.assertTrue(bool(realver))
pkgs = [{pkg_targets[0]: version}, pkg_targets[1], {capability: realver}]
ret = self.run_state('pkg.installed',
name='test_pkg_cap_003_installed_multipkg_with_version-install',
pkgs=pkgs,
refresh=False)
self.assertSaltFalseReturn(ret)
ret = self.run_state('pkg.installed',
name='test_pkg_cap_003_installed_multipkg_with_version-install-capability',
pkgs=pkgs,
refresh=False, resolve_capabilities=True, test=True)
self.assertInSaltComment("packages would be installed/updated", ret)
self.assertInSaltComment("{0}={1}".format(realpkg, realver), ret)
ret = self.run_state('pkg.installed',
name='test_pkg_cap_003_installed_multipkg_with_version-install-capability',
pkgs=pkgs,
refresh=False, resolve_capabilities=True)
self.assertSaltTrueReturn(ret)
cleanup_pkgs = pkg_targets
cleanup_pkgs.append(realpkg)
ret = self.run_state('pkg.removed',
name='test_pkg_cap_003_installed_multipkg_with_version-remove',
pkgs=cleanup_pkgs)
self.assertSaltTrueReturn(ret)
@skipIf(salt.utils.platform.is_windows(), 'minion is windows')
@requires_system_grains
def test_pkg_cap_004_latest(self, grains=None):
'''
This tests pkg.latest with a package that has no epoch (or a zero
epoch).
'''
# Skip test if package manager not available
if not pkgmgr_avail(self.run_function, self.run_function('grains.items')):
self.skipTest('Package manager is not available')
os_family = grains.get('os_family', '')
pkg_cap_targets = _PKG_CAP_TARGETS.get(os_family, [])
if not len(pkg_cap_targets) > 0:
self.skipTest('Capability not provided')
target, realpkg = pkg_cap_targets[0]
version = self.run_function('pkg.version', [target])
realver = self.run_function('pkg.version', [realpkg])
# If this assert fails, we need to find new targets, this test needs to
# be able to test successful installation of packages, so this package
# needs to not be installed before we run the states below
self.assertFalse(version)
self.assertFalse(realver)
ret = self.run_state('pkg.latest', name=target, refresh=False, resolve_capabilities=True, test=True)
self.assertInSaltComment("The following packages would be installed/upgraded: {0}".format(realpkg), ret)
ret = self.run_state('pkg.latest', name=target, refresh=False, resolve_capabilities=True)
self.assertSaltTrueReturn(ret)
ret = self.run_state('pkg.latest', name=target, refresh=False, resolve_capabilities=True)
self.assertSaltTrueReturn(ret)
self.assertInSaltComment("is already up-to-date", ret)
ret = self.run_state('pkg.removed', name=realpkg)
self.assertSaltTrueReturn(ret)
@skipIf(salt.utils.platform.is_windows(), 'minion is windows')
@requires_system_grains
def test_pkg_cap_005_downloaded(self, grains=None):
'''
This is a destructive test as it installs and then removes a package
'''
# Skip test if package manager not available
if not pkgmgr_avail(self.run_function, self.run_function('grains.items')):
self.skipTest('Package manager is not available')
os_family = grains.get('os_family', '')
pkg_cap_targets = _PKG_CAP_TARGETS.get(os_family, [])
if not len(pkg_cap_targets) > 0:
self.skipTest('Capability not provided')
target, realpkg = pkg_cap_targets[0]
version = self.run_function('pkg.version', [target])
realver = self.run_function('pkg.version', [realpkg])
# If this assert fails, we need to find new targets, this test needs to
# be able to test successful installation of packages, so this package
# needs to not be installed before we run the states below
self.assertFalse(version)
self.assertFalse(realver)
ret = self.run_state('pkg.downloaded', name=target, refresh=False)
self.assertSaltFalseReturn(ret)
ret = self.run_state('pkg.downloaded', name=target, refresh=False, resolve_capabilities=True, test=True)
self.assertInSaltComment("The following packages would be downloaded: {0}".format(realpkg), ret)
ret = self.run_state('pkg.downloaded', name=target, refresh=False, resolve_capabilities=True)
self.assertSaltTrueReturn(ret)
@skipIf(salt.utils.platform.is_windows(), 'minion is windows')
@requires_system_grains
def test_pkg_cap_006_uptodate(self, grains=None):
'''
This is a destructive test as it installs and then removes a package
'''
# Skip test if package manager not available
if not pkgmgr_avail(self.run_function, self.run_function('grains.items')):
self.skipTest('Package manager is not available')
os_family = grains.get('os_family', '')
pkg_cap_targets = _PKG_CAP_TARGETS.get(os_family, [])
if not len(pkg_cap_targets) > 0:
self.skipTest('Capability not provided')
target, realpkg = pkg_cap_targets[0]
version = self.run_function('pkg.version', [target])
realver = self.run_function('pkg.version', [realpkg])
# If this assert fails, we need to find new targets, this test needs to
# be able to test successful installation of packages, so this package
# needs to not be installed before we run the states below
self.assertFalse(version)
self.assertFalse(realver)
ret = self.run_state('pkg.installed', name=target,
refresh=False, resolve_capabilities=True)
self.assertSaltTrueReturn(ret)
ret = self.run_state('pkg.uptodate',
name='test_pkg_cap_006_uptodate',
pkgs=[target],
refresh=False,
resolve_capabilities=True)
self.assertSaltTrueReturn(ret)
self.assertInSaltComment("System is already up-to-date", ret)
ret = self.run_state('pkg.removed', name=realpkg)
self.assertSaltTrueReturn(ret)
ret = self.run_state('pkg.uptodate',
name='test_pkg_cap_006_uptodate',
refresh=False,
test=True)
self.assertInSaltComment("System update will be performed", ret)

View File

@ -90,7 +90,7 @@ def get_salt_vars():
__opts__, __opts__,
__grains__, __grains__,
__opts__.get('id'), __opts__.get('id'),
__opts__.get('environment'), __opts__.get('saltenv'),
).compile_pillar() ).compile_pillar()
else: else:
__pillar__ = {} __pillar__ = {}

View File

@ -278,6 +278,38 @@ class LazyLoaderWhitelistTest(TestCase):
self.assertNotIn('grains.get', self.loader) self.assertNotIn('grains.get', self.loader)
class LazyLoaderSingleItem(TestCase):
'''
Test loading a single item via the _load() function
'''
@classmethod
def setUpClass(cls):
cls.opts = salt.config.minion_config(None)
cls.opts['grains'] = grains(cls.opts)
def setUp(self):
self.loader = LazyLoader(_module_dirs(copy.deepcopy(self.opts), 'modules', 'module'),
copy.deepcopy(self.opts),
tag='module')
def tearDown(self):
del self.loader
def test_single_item_no_dot(self):
'''
Checks that a KeyError is raised when the function key does not contain a '.'
'''
with self.assertRaises(KeyError) as err:
inspect.isfunction(self.loader['testing_no_dot'])
if six.PY2:
self.assertEqual(err.exception[0],
'The key \'%s\' should contain a \'.\'')
else:
self.assertEqual(str(err.exception),
str(("The key '%s' should contain a '.'", 'testing_no_dot')))
module_template = ''' module_template = '''
__load__ = ['test', 'test_alias'] __load__ = ['test', 'test_alias']
__func_alias__ = dict(test_alias='working_alias') __func_alias__ = dict(test_alias='working_alias')

View File

@ -25,7 +25,7 @@ import salt.utils.hashutils
import salt.utils.odict import salt.utils.odict
import salt.utils.platform import salt.utils.platform
import salt.modules.state as state import salt.modules.state as state
from salt.exceptions import SaltInvocationError from salt.exceptions import CommandExecutionError, SaltInvocationError
from salt.ext import six from salt.ext import six
@ -362,7 +362,7 @@ class StateTestCase(TestCase, LoaderModuleMockMixin):
state: { state: {
'__opts__': { '__opts__': {
'cachedir': '/D', 'cachedir': '/D',
'environment': None, 'saltenv': None,
'__cli': 'salt', '__cli': 'salt',
}, },
'__utils__': utils, '__utils__': utils,
@ -632,7 +632,7 @@ class StateTestCase(TestCase, LoaderModuleMockMixin):
with patch.dict(state.__opts__, {"test": "A"}): with patch.dict(state.__opts__, {"test": "A"}):
mock = MagicMock( mock = MagicMock(
return_value={'test': True, return_value={'test': True,
'environment': None} 'saltenv': None}
) )
with patch.object(state, '_get_opts', mock): with patch.object(state, '_get_opts', mock):
mock = MagicMock(return_value=True) mock = MagicMock(return_value=True)
@ -659,7 +659,7 @@ class StateTestCase(TestCase, LoaderModuleMockMixin):
with patch.dict(state.__opts__, {"test": "A"}): with patch.dict(state.__opts__, {"test": "A"}):
mock = MagicMock( mock = MagicMock(
return_value={'test': True, return_value={'test': True,
'environment': None} 'saltenv': None}
) )
with patch.object(state, '_get_opts', mock): with patch.object(state, '_get_opts', mock):
MockState.State.flag = True MockState.State.flag = True
@ -681,7 +681,7 @@ class StateTestCase(TestCase, LoaderModuleMockMixin):
with patch.dict(state.__opts__, {"test": "A"}): with patch.dict(state.__opts__, {"test": "A"}):
mock = MagicMock( mock = MagicMock(
return_value={'test': True, return_value={'test': True,
'environment': None} 'saltenv': None}
) )
with patch.object(state, '_get_opts', mock): with patch.object(state, '_get_opts', mock):
mock = MagicMock(return_value=True) mock = MagicMock(return_value=True)
@ -881,7 +881,7 @@ class StateTestCase(TestCase, LoaderModuleMockMixin):
with patch.dict(state.__opts__, {"test": None}): with patch.dict(state.__opts__, {"test": None}):
mock = MagicMock(return_value={"test": "", mock = MagicMock(return_value={"test": "",
"environment": None}) "saltenv": None})
with patch.object(state, '_get_opts', mock): with patch.object(state, '_get_opts', mock):
mock = MagicMock(return_value=True) mock = MagicMock(return_value=True)
with patch.object(salt.utils, with patch.object(salt.utils,
@ -993,3 +993,82 @@ class StateTestCase(TestCase, LoaderModuleMockMixin):
else: else:
with patch('salt.utils.files.fopen', mock_open()): with patch('salt.utils.files.fopen', mock_open()):
self.assertTrue(state.pkg(tar_file, 0, "md5")) self.assertTrue(state.pkg(tar_file, 0, "md5"))
def test_lock_saltenv(self):
'''
Tests lock_saltenv in each function which accepts saltenv on the CLI
'''
lock_msg = 'lock_saltenv is enabled, saltenv cannot be changed'
empty_list_mock = MagicMock(return_value=[])
with patch.dict(state.__opts__, {'lock_saltenv': True}), \
patch.dict(state.__salt__, {'grains.get': empty_list_mock}), \
patch.object(state, 'running', empty_list_mock):
# Test high
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.high(
[{"vim": {"pkg": ["installed"]}}], saltenv='base')
# Test template
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.template('foo', saltenv='base')
# Test template_str
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.template_str('foo', saltenv='base')
# Test apply_ with SLS
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.apply_('foo', saltenv='base')
# Test apply_ with Highstate
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.apply_(saltenv='base')
# Test highstate
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.highstate(saltenv='base')
# Test sls
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.sls('foo', saltenv='base')
# Test top
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.top('foo.sls', saltenv='base')
# Test show_highstate
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.show_highstate(saltenv='base')
# Test show_lowstate
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.show_lowstate(saltenv='base')
# Test sls_id
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.sls_id('foo', 'bar', saltenv='base')
# Test show_low_sls
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.show_low_sls('foo', saltenv='base')
# Test show_sls
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.show_sls('foo', saltenv='base')
# Test show_top
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.show_top(saltenv='base')
# Test single
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.single('foo.bar', name='baz', saltenv='base')
# Test pkg
with self.assertRaisesRegex(CommandExecutionError, lock_msg):
state.pkg(
'/tmp/salt_state.tgz',
'760a9353810e36f6d81416366fc426dc',
'md5',
saltenv='base')

View File

@ -669,8 +669,8 @@ Repository 'DUMMY' not found by its alias, number, or URI.
zypper_mock.assert_called_once_with( zypper_mock.assert_called_once_with(
'--no-refresh', '--no-refresh',
'install', 'install',
'--name',
'--auto-agree-with-licenses', '--auto-agree-with-licenses',
'--name',
'--download-only', '--download-only',
'vim' 'vim'
) )
@ -699,8 +699,8 @@ Repository 'DUMMY' not found by its alias, number, or URI.
zypper_mock.assert_called_once_with( zypper_mock.assert_called_once_with(
'--no-refresh', '--no-refresh',
'install', 'install',
'--name',
'--auto-agree-with-licenses', '--auto-agree-with-licenses',
'--name',
'--download-only', '--download-only',
'vim' 'vim'
) )
@ -724,8 +724,8 @@ Repository 'DUMMY' not found by its alias, number, or URI.
zypper_mock.assert_called_once_with( zypper_mock.assert_called_once_with(
'--no-refresh', '--no-refresh',
'install', 'install',
'--name',
'--auto-agree-with-licenses', '--auto-agree-with-licenses',
'--name',
'patch:SUSE-PATCH-1234' 'patch:SUSE-PATCH-1234'
) )
self.assertDictEqual(ret, {"vim": {"old": "1.1", "new": "1.2"}}) self.assertDictEqual(ret, {"vim": {"old": "1.1", "new": "1.2"}})

View File

@ -55,7 +55,7 @@ class PillarTestCase(TestCase):
'os': 'Ubuntu', 'os': 'Ubuntu',
} }
pillar = salt.pillar.Pillar(opts, grains, 'mocked-minion', 'dev') pillar = salt.pillar.Pillar(opts, grains, 'mocked-minion', 'dev')
self.assertEqual(pillar.opts['environment'], 'dev') self.assertEqual(pillar.opts['saltenv'], 'dev')
self.assertEqual(pillar.opts['pillarenv'], 'dev') self.assertEqual(pillar.opts['pillarenv'], 'dev')
def test_ext_pillar_no_extra_minion_data_val_dict(self): def test_ext_pillar_no_extra_minion_data_val_dict(self):
@ -416,7 +416,7 @@ class PillarTestCase(TestCase):
'state_top': '', 'state_top': '',
'pillar_roots': [], 'pillar_roots': [],
'extension_modules': '', 'extension_modules': '',
'environment': 'base', 'saltenv': 'base',
'file_roots': [], 'file_roots': [],
} }
grains = { grains = {
@ -584,7 +584,7 @@ class RemotePillarTestCase(TestCase):
salt.pillar.RemotePillar({}, self.grains, 'mocked-minion', 'dev') salt.pillar.RemotePillar({}, self.grains, 'mocked-minion', 'dev')
mock_get_extra_minion_data.assert_called_once_with( mock_get_extra_minion_data.assert_called_once_with(
{'environment': 'dev'}) {'saltenv': 'dev'})
def test_multiple_keys_in_opts_added_to_pillar(self): def test_multiple_keys_in_opts_added_to_pillar(self):
opts = { opts = {
@ -702,7 +702,7 @@ class AsyncRemotePillarTestCase(TestCase):
salt.pillar.RemotePillar({}, self.grains, 'mocked-minion', 'dev') salt.pillar.RemotePillar({}, self.grains, 'mocked-minion', 'dev')
mock_get_extra_minion_data.assert_called_once_with( mock_get_extra_minion_data.assert_called_once_with(
{'environment': 'dev'}) {'saltenv': 'dev'})
def test_pillar_send_extra_minion_data_from_config(self): def test_pillar_send_extra_minion_data_from_config(self):
opts = { opts = {