From 068afc369d1d13ae306d0d4e1e145d268e9175e4 Mon Sep 17 00:00:00 2001 From: Sebastian Marsching Date: Sun, 26 Feb 2017 17:18:24 +0100 Subject: [PATCH 001/633] Added the saltmod.parallel_runners state. This new state is intended for use with the orchestrate runner. It is used in a way very similar to saltmod.runner, except that it executes multiple runners in parallel. --- salt/states/saltmod.py | 174 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index 0b3735fb28..bc1186d1d0 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -26,6 +26,8 @@ from __future__ import absolute_import # Import python libs import fnmatch import logging +import sys +import threading import time # Import salt libs @@ -60,6 +62,47 @@ def _fire_args(tag_data): ) +def _parallel_map(func, inputs): + ''' + Applies a function to each element of a list, returning the resulting list. + + A separate thread is created for each element in the input list and the + passed function is called for each of the elements. When all threads have + finished execution a list with the results corresponding to the inputs is + returned. + + If one of the threads fails (because the function throws an exception), + that exception is reraised. If more than one thread fails, the exception + from the first thread (according to the index of the input element) is + reraised. + + func: + function that is applied on each input element. + inputs: + list of elements that shall be processed. The length of this list also + defines the number of threads created. + ''' + outputs = len(inputs) * [None] + errors = len(inputs) * [None] + def create_thread(index): + def run_thread(): + try: + outputs[index] = func(inputs[index]) + except: + errors[index] = sys.exc_info() + thread = threading.Thread(target=run_thread) + thread.start() + return thread + threads = list(six.moves.map(create_thread, six.moves.range(len(inputs)))) + for thread in threads: + thread.join() + for error in errors: + if error is not None: + exc_type, exc_value, exc_traceback = error + six.reraise(exc_type, exc_value, exc_traceback) + return outputs + + def state(name, tgt, ssh=False, @@ -689,6 +732,137 @@ def runner(name, **kwargs): return ret +def parallel_runners(name, runners): + ''' + Executes multiple runner modules on the master in parallel. + + .. versionadded:: 2017.x.0 (Nitrogen) + + A separate process is spawned for each runner. This state is intended to be + used with the orchestrate runner in place of the ``saltmod.runner`` state + when different tasks should be run in parallel. In general, Salt states are + not safe when used concurrently, so ensure that they are used in a safe way + (e.g. by only targeting separate minions in parallel tasks). + + name: + name identifying this state. The name is provided as part of the + output, but not used for anything else. + + runners: + list of runners that should be run in parallel. Each element of the + list has to be a dictionary. This dictionary's name entry stores the + name of the runner function that shall be invoked. The optional kwarg + entry stores a dictionary of named arguments that are passed to the + runner function. + + .. code-block:: yaml + + parallel-state: + saltext.parallel-runner: + - runners: + - name: state.orchestrate + kwarg: + mods: orchestrate_state_1 + - name: state.orcestrate + kwarg: + mods: orchestrate_state_2 + ''' + try: + jid = __orchestration_jid__ + except NameError: + log.debug( + 'Unable to fire args event due to missing __orchestration_jid__') + jid = None + + def call_runner(runner_config): + return __salt__['saltutil.runner'](runner_config['name'], + __orchestration_jid__=jid, + __env__=__env__, + full_return=True, + **(runner_config['kwarg'])) + + outputs = _parallel_map(call_runner, runners) + + success = six.moves.reduce( + lambda x, y: x and y, + [not ('success' in out and not out['success']) for out in outputs], + True) + + def find_new_and_old_in_changes(data, prefix): + if isinstance(data, dict) and data: + if 'new' in data and 'old' in data: + return [(prefix, {'new': data['new'], 'old': data['old']})] + else: + return [ + change_item + for key, value in six.iteritems(data) + for change_item in find_new_and_old_in_changes( + value, prefix + '[' + str(key) + ']') + ] + if isinstance(data, list) and list: + return [ + change_item + for index, value in six.moves.zip( + six.moves.range(len(data)), data) + for change_item in find_new_and_old_in_changes( + value, prefix + '[' + str(index) + ']') + ] + else: + return [] + def find_changes(data, prefix): + if isinstance(data, dict) and data: + if 'changes' in data: + return find_new_and_old_in_changes(data['changes'], prefix) + else: + return [ + change_item + for key, value in six.iteritems(data) + for change_item in find_changes( + value, prefix + '[' + str(key) + ']') + ] + else: + return [] + def find_changes_in_output(output, index): + try: + data = output['return']['data'] + except KeyError: + data = {} + return find_changes(data, '[' + str(index) + ']') + changes = dict([ + change_item + for change_items in six.moves.map( + find_changes_in_output, outputs, six.moves.range(len(outputs))) + for change_item in change_items + ]) + + def generate_comment(index, out): + runner_failed = 'success' in out and not out['success'] + runner_return = out.get('return') + comment = ( + 'Runner ' + str(index) + ' was ' + + ('not ' if runner_failed else '') + + 'successful and returned ' + + (str(runner_return) if runner_return else ' nothing') + '.') + return comment + comment = '\n'.join(six.moves.map(generate_comment, + six.moves.range(len(outputs)), + outputs)) + + ret = { + 'name': name, + 'result': success, + 'changes': changes, + 'comment': comment + } + + ret['__orchestration__'] = True + # The 'runner' function includes out['jid'] as '__jid__' in the returned + # dict, but we cannot do this here because we have more than one JID if + # we have more than one runner. + + return ret + + def wheel(name, **kwargs): ''' Execute a wheel module on the master From 1582e92e8eb0dee0e3502e9a134a448f47ec3353 Mon Sep 17 00:00:00 2001 From: Sebastian Marsching Date: Mon, 27 Feb 2017 21:29:22 +0100 Subject: [PATCH 002/633] Fixed the documentation (thread vs. process). The documentation erroneously used the word process in one place where thread would actually have been correct. This commit fixes this issue. --- salt/states/saltmod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index bc1186d1d0..4dca00fb45 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -738,7 +738,7 @@ def parallel_runners(name, runners): .. versionadded:: 2017.x.0 (Nitrogen) - A separate process is spawned for each runner. This state is intended to be + A separate thread is spawned for each runner. This state is intended to be used with the orchestrate runner in place of the ``saltmod.runner`` state when different tasks should be run in parallel. In general, Salt states are not safe when used concurrently, so ensure that they are used in a safe way From c3b9035e41433fe212cbc6dfe781e07b75757823 Mon Sep 17 00:00:00 2001 From: Sebastian Marsching Date: Sun, 5 Mar 2017 21:11:34 +0100 Subject: [PATCH 003/633] Fix two typos in docs for salt.parallel_runners. --- salt/states/saltmod.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index 4dca00fb45..ddbf8bcb24 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -758,12 +758,12 @@ def parallel_runners(name, runners): .. code-block:: yaml parallel-state: - saltext.parallel-runner: + salt.parallel-runner: - runners: - name: state.orchestrate kwarg: mods: orchestrate_state_1 - - name: state.orcestrate + - name: state.orchestrate kwarg: mods: orchestrate_state_2 ''' From 7155bdf563a2d934c18803351f84cf0768fb5dbe Mon Sep 17 00:00:00 2001 From: Sebastian Marsching Date: Sun, 5 Mar 2017 22:30:10 +0100 Subject: [PATCH 004/633] Allow for a missing kwarg parameter. The code in saltmod.parallel_runners would fail if the (optional) kwarg argument was missing. This is fixed by using an empty dictionary for kwarg by default. --- salt/states/saltmod.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index ddbf8bcb24..fffdd507d5 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -779,7 +779,8 @@ def parallel_runners(name, runners): __orchestration_jid__=jid, __env__=__env__, full_return=True, - **(runner_config['kwarg'])) + **(runner_config.get(['kwarg'], + {}))) outputs = _parallel_map(call_runner, runners) From 536093b696fef1c9952268d78966202559235dac Mon Sep 17 00:00:00 2001 From: Sebastian Marsching Date: Thu, 9 Mar 2017 14:14:48 +0100 Subject: [PATCH 005/633] Fixed incorrect use of list as dict key. The name parameter in a call to dict.get(...) was accidentally wrapped in brackets, leading to a TypeError ("unhashable type: 'list'"). --- salt/states/saltmod.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index fffdd507d5..2900382f83 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -779,8 +779,7 @@ def parallel_runners(name, runners): __orchestration_jid__=jid, __env__=__env__, full_return=True, - **(runner_config.get(['kwarg'], - {}))) + **(runner_config.get('kwarg', {}))) outputs = _parallel_map(call_runner, runners) From 7804a95480384808a987af5bbb7f12ed782257c1 Mon Sep 17 00:00:00 2001 From: Sebastian Marsching Date: Wed, 15 Mar 2017 17:26:09 +0100 Subject: [PATCH 006/633] Improve configuration format and merging of outputs. The configuration format for specifying the list of runners has been changed so that it matches the format used in other places. The merging of outputs from the runners has been improved so that the outputs are correctly passed on regardless of the format used by the runner. --- salt/states/saltmod.py | 197 ++++++++++++++++++++++++++--------------- 1 file changed, 125 insertions(+), 72 deletions(-) diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index 2900382f83..1f6794972b 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -32,6 +32,8 @@ import time # Import salt libs import salt.syspaths +import salt.exceptions +import salt.output import salt.utils import salt.utils.event import salt.ext.six as six @@ -88,7 +90,7 @@ def _parallel_map(func, inputs): def run_thread(): try: outputs[index] = func(inputs[index]) - except: + except: # pylint: disable=bare-except errors[index] = sys.exc_info() thread = threading.Thread(target=run_thread) thread.start() @@ -758,15 +760,49 @@ def parallel_runners(name, runners): .. code-block:: yaml parallel-state: - salt.parallel-runner: + salt.parallel_runners: - runners: - - name: state.orchestrate - kwarg: - mods: orchestrate_state_1 - - name: state.orchestrate - kwarg: - mods: orchestrate_state_2 + my_runner_1: + - name: state.orchestrate + - kwarg: + mods: orchestrate_state_1 + my_runner_2: + - name: state.orchestrate + - kwarg: + mods: orchestrate_state_2 ''' + # For the sake of consistency, we treat a single string in the same way as + # a key without a value. This allows something like + # salt.parallel_runners: + # - runners: + # state.orchestrate + # Obviously, this will only work if the specified runner does not need any + # arguments. + if isinstance(runners, six.string_types): + runners = {runners: [{name: runners}]} + # If the runners argument is not a string, it must be a dict. Everything + # else is considered an error. + if not isinstance(runners, dict): + return { + 'name': name, + 'result': False, + 'changes': {}, + 'comment': 'The runners parameter must be a string or dict.' + } + # The configuration for each runner is given as a list of key-value pairs. + # This is not very useful for what we want to do, but it is the typical + # style used in Salt. For further processing, we convert each of these + # lists to a dict. This also makes it easier to check whether a name has + # been specified explicitly. + for runner_id, runner_config in six.iteritems(runners): + if runner_config is None: + runner_config = {} + else: + runner_config = salt.utils.repack_dictlist(runner_config) + if 'name' not in runner_config: + runner_config['name'] = runner_id + runners[runner_id] = runner_config + try: jid = __orchestration_jid__ except NameError: @@ -781,81 +817,98 @@ def parallel_runners(name, runners): full_return=True, **(runner_config.get('kwarg', {}))) - outputs = _parallel_map(call_runner, runners) + try: + outputs = _parallel_map(call_runner, list(six.itervalues(runners))) + except salt.exceptions.SaltException as exc: + return { + 'name': name, + 'result': False, + 'success': False, + 'changes': {}, + 'comment': 'One of the runners raised an exception: {0}'.format( + exc) + } + # We bundle the results of the runners with the IDs of the runners so that + # we can easily identify which output belongs to which runner. At the same + # time we exctract the actual return value of the runner (saltutil.runner + # adds some extra information that is not interesting to us). + outputs = { + runner_id: out['return']for runner_id, out in + six.moves.zip(six.iterkeys(runners), outputs) + } - success = six.moves.reduce( - lambda x, y: x and y, - [not ('success' in out and not out['success']) for out in outputs], - True) - - def find_new_and_old_in_changes(data, prefix): - if isinstance(data, dict) and data: - if 'new' in data and 'old' in data: - return [(prefix, {'new': data['new'], 'old': data['old']})] + # If each of the runners returned its output in the format compatible with + # the 'highstate' outputter, we can leverage this fact when merging the + # outputs. + highstate_output = all( + [out.get('outputter', '') == 'highstate' and 'data' in out for out in + six.itervalues(outputs)] + ) + # The following helper function is used to extract changes from highstate + # output. + def extract_changes(obj): + if not isinstance(obj, dict): + return {} + elif 'changes' in obj: + if (isinstance(obj['changes'], dict) + and obj['changes'].get('out', '') == 'highstate' + and 'ret' in obj['changes']): + return obj['changes']['ret'] else: - return [ - change_item - for key, value in six.iteritems(data) - for change_item in find_new_and_old_in_changes( - value, prefix + '[' + str(key) + ']') - ] - if isinstance(data, list) and list: - return [ - change_item - for index, value in six.moves.zip( - six.moves.range(len(data)), data) - for change_item in find_new_and_old_in_changes( - value, prefix + '[' + str(index) + ']') + return obj['changes'] + else: + found_changes = {} + for key, value in six.iteritems(obj): + change = extract_changes(value) + if change: + found_changes[key] = change + return found_changes + if highstate_output: + failed_runners = [runner_id for runner_id, out in + six.iteritems(outputs) if + out['data'].get('retcode', 0) != 0] + all_successful = not failed_runners + if all_successful: + comment = 'All runner functions executed successfully.' + else: + runner_comments = [ + 'Runner {0} failed with return value:\n{1}'.format( + runner_id, + salt.output.out_format(outputs[runner_id], + 'nested', + __opts__, + nested_indent=2) + ) for runner_id in failed_runners ] + comment = '\n'.join(runner_comments) + changes = {} + for runner_id, out in six.iteritems(outputs): + runner_changes = extract_changes(out['data']) + if runner_changes: + changes[runner_id] = runner_changes + else: + failed_runners = [runner_id for runner_id, out in + six.iteritems(outputs) if + out.get('exit_code', 0) != 0] + all_successful = not failed_runners + if all_successful: + comment = 'All runner functions executed successfully.' else: - return [] - def find_changes(data, prefix): - if isinstance(data, dict) and data: - if 'changes' in data: - return find_new_and_old_in_changes(data['changes'], prefix) + if len(failed_runners) == 1: + comment = 'Runner {0} failed.'.format(failed_runners[0]) else: - return [ - change_item - for key, value in six.iteritems(data) - for change_item in find_changes( - value, prefix + '[' + str(key) + ']') - ] - else: - return [] - def find_changes_in_output(output, index): - try: - data = output['return']['data'] - except KeyError: - data = {} - return find_changes(data, '[' + str(index) + ']') - changes = dict([ - change_item - for change_items in six.moves.map( - find_changes_in_output, outputs, six.moves.range(len(outputs))) - for change_item in change_items - ]) - - def generate_comment(index, out): - runner_failed = 'success' in out and not out['success'] - runner_return = out.get('return') - comment = ( - 'Runner ' + str(index) + ' was ' - + ('not ' if runner_failed else '') - + 'successful and returned ' - + (str(runner_return) if runner_return else ' nothing') + '.') - return comment - comment = '\n'.join(six.moves.map(generate_comment, - six.moves.range(len(outputs)), - outputs)) - + comment =\ + 'Runners {0} failed.'.format(', '.join(failed_runners)) + changes = {'ret': { + runner_id: out for runner_id, out in six.iteritems(outputs) + }} ret = { 'name': name, - 'result': success, + 'result': all_successful, 'changes': changes, 'comment': comment } - ret['__orchestration__'] = True # The 'runner' function includes out['jid'] as '__jid__' in the returned # dict, but we cannot do this here because we have more than one JID if # we have more than one runner. From 924d039ca48dfaadcfca012cee3452a89592ca74 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Tue, 21 Mar 2017 11:48:12 -0600 Subject: [PATCH 007/633] Newlines for lint compliance --- salt/states/saltmod.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index 1f6794972b..6ed9d2b6dc 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -86,6 +86,7 @@ def _parallel_map(func, inputs): ''' outputs = len(inputs) * [None] errors = len(inputs) * [None] + def create_thread(index): def run_thread(): try: @@ -844,8 +845,10 @@ def parallel_runners(name, runners): [out.get('outputter', '') == 'highstate' and 'data' in out for out in six.itervalues(outputs)] ) + # The following helper function is used to extract changes from highstate # output. + def extract_changes(obj): if not isinstance(obj, dict): return {} From 567e5ea2575710d2989362b89329d1b8bd21c15b Mon Sep 17 00:00:00 2001 From: "dnABic (Andreja Babic)" Date: Sun, 14 May 2017 15:10:45 +0200 Subject: [PATCH 008/633] added parameter apply_to for rabbitmq policy --- salt/modules/rabbitmq.py | 4 +++- salt/states/rabbitmq_policy.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/salt/modules/rabbitmq.py b/salt/modules/rabbitmq.py index b55f2dd173..0a3d798567 100644 --- a/salt/modules/rabbitmq.py +++ b/salt/modules/rabbitmq.py @@ -848,7 +848,7 @@ def list_policies(vhost="/", runas=None): return ret -def set_policy(vhost, name, pattern, definition, priority=None, runas=None): +def set_policy(vhost, name, pattern, definition, apply_to=None, priority=None, runas=None): ''' Set a policy based on rabbitmqctl set_policy. @@ -871,6 +871,8 @@ def set_policy(vhost, name, pattern, definition, priority=None, runas=None): cmd = [RABBITMQCTL, 'set_policy', '-p', vhost] if priority: cmd.extend(['--priority', priority]) + if apply_to: + cmd.extend(['--apply-to', apply_to]) cmd.extend([name, pattern, definition]) res = __salt__['cmd.run_all'](cmd, runas=runas, python_shell=False) log.debug('Set policy: {0}'.format(res['stdout'])) diff --git a/salt/states/rabbitmq_policy.py b/salt/states/rabbitmq_policy.py index 37c4c8ff77..16801abe25 100644 --- a/salt/states/rabbitmq_policy.py +++ b/salt/states/rabbitmq_policy.py @@ -36,6 +36,7 @@ def __virtual__(): def present(name, pattern, definition, + apply_to=None, priority=0, vhost='/', runas=None): @@ -52,6 +53,8 @@ def present(name, A json dict describing the policy priority Priority (defaults to 0) + apply_to + Apply policy to 'queues', 'exchanges' or 'all' (defailt to 'all') vhost Virtual host to apply to (defaults to '/') runas @@ -68,6 +71,8 @@ def present(name, updates.append('Pattern') if policy.get('definition') != definition: updates.append('Definition') + if apply_to and (policy.get('apply-to') != apply_to): + updates.append('Applyto') if int(policy.get('priority')) != priority: updates.append('Priority') @@ -85,6 +90,7 @@ def present(name, name, pattern, definition, + apply_to, priority=priority, runas=runas) elif updates: @@ -97,6 +103,7 @@ def present(name, name, pattern, definition, + apply_to, priority=priority, runas=runas) From b1dc0fc29927e64e9e61dd8280fa1cc478206cc2 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Fri, 18 Aug 2017 17:03:17 +0300 Subject: [PATCH 009/633] Group minion events in Syndic. --- salt/master.py | 67 +++++++++++---------- salt/minion.py | 157 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 154 insertions(+), 70 deletions(-) diff --git a/salt/master.py b/salt/master.py index 59388bc0b5..0045954570 100644 --- a/salt/master.py +++ b/salt/master.py @@ -1417,40 +1417,43 @@ class AESFuncs(object): :param dict load: The minion payload ''' - # Verify the load - if any(key not in load for key in (u'return', u'jid', u'id')): - return None - # if we have a load, save it - if load.get(u'load'): - fstr = u'{0}.save_load'.format(self.opts[u'master_job_cache']) - self.mminion.returners[fstr](load[u'jid'], load[u'load']) + loads = load.get(u'load') + if not isinstance(loads, list): + loads = [loads] + for load in loads: + # Verify the load + if any(key not in load for key in (u'return', u'jid', u'id')): + continue + # if we have a load, save it + if load.get(u'load'): + fstr = u'{0}.save_load'.format(self.opts[u'master_job_cache']) + self.mminion.returners[fstr](load[u'jid'], load[u'load']) - # Register the syndic - syndic_cache_path = os.path.join(self.opts[u'cachedir'], u'syndics', load[u'id']) - if not os.path.exists(syndic_cache_path): - path_name = os.path.split(syndic_cache_path)[0] - if not os.path.exists(path_name): - os.makedirs(path_name) - with salt.utils.files.fopen(syndic_cache_path, u'w') as wfh: - wfh.write(u'') + # Register the syndic + syndic_cache_path = os.path.join(self.opts[u'cachedir'], u'syndics', load[u'id']) + if not os.path.exists(syndic_cache_path): + path_name = os.path.split(syndic_cache_path)[0] + if not os.path.exists(path_name): + os.makedirs(path_name) + with salt.utils.fopen(syndic_cache_path, u'w') as wfh: + wfh.write(u'') - # Format individual return loads - for key, item in six.iteritems(load[u'return']): - ret = {u'jid': load[u'jid'], - u'id': key} - ret.update(item) - if u'master_id' in load: - ret[u'master_id'] = load[u'master_id'] - if u'fun' in load: - ret[u'fun'] = load[u'fun'] - if u'arg' in load: - ret[u'fun_args'] = load[u'arg'] - if u'out' in load: - ret[u'out'] = load[u'out'] - if u'sig' in load: - ret[u'sig'] = load[u'sig'] - - self._return(ret) + # Format individual return loads + for key, item in six.iteritems(load[u'return']): + ret = {u'jid': load[u'jid'], + u'id': key} + ret.update(item) + if u'master_id' in load: + ret[u'master_id'] = load[u'master_id'] + if u'fun' in load: + ret[u'fun'] = load[u'fun'] + if u'arg' in load: + ret[u'fun_args'] = load[u'arg'] + if u'out' in load: + ret[u'out'] = load[u'out'] + if u'sig' in load: + ret[u'sig'] = load[u'sig'] + self._return(ret) def minion_runner(self, clear_load): ''' diff --git a/salt/minion.py b/salt/minion.py index 5713a0edb6..8233315bd9 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -643,6 +643,34 @@ class MinionBase(object): self.connected = False raise exc + def _return_retry_timer(self): + ''' + Based on the minion configuration, either return a randomized timer or + just return the value of the return_retry_timer. + ''' + msg = u'Minion return retry timer set to {0} seconds' + # future lint: disable=str-format-in-logging + if self.opts.get(u'return_retry_timer_max'): + try: + random_retry = randint(self.opts[u'return_retry_timer'], self.opts[u'return_retry_timer_max']) + log.debug(msg.format(random_retry) + u' (randomized)') + return random_retry + except ValueError: + # Catch wiseguys using negative integers here + log.error( + u'Invalid value (return_retry_timer: %s or ' + u'return_retry_timer_max: %s). Both must be positive ' + u'integers.', + self.opts[u'return_retry_timer'], + self.opts[u'return_retry_timer_max'], + ) + log.debug(msg.format(DEFAULT_MINION_OPTS[u'return_retry_timer'])) + return DEFAULT_MINION_OPTS[u'return_retry_timer'] + else: + log.debug(msg.format(self.opts.get(u'return_retry_timer'))) + return self.opts.get(u'return_retry_timer') + # future lint: enable=str-format-in-logging + class SMinion(MinionBase): ''' @@ -1157,34 +1185,6 @@ class Minion(MinionBase): self.grains_cache = self.opts[u'grains'] self.ready = True - def _return_retry_timer(self): - ''' - Based on the minion configuration, either return a randomized timer or - just return the value of the return_retry_timer. - ''' - msg = u'Minion return retry timer set to {0} seconds' - # future lint: disable=str-format-in-logging - if self.opts.get(u'return_retry_timer_max'): - try: - random_retry = randint(self.opts[u'return_retry_timer'], self.opts[u'return_retry_timer_max']) - log.debug(msg.format(random_retry) + u' (randomized)') - return random_retry - except ValueError: - # Catch wiseguys using negative integers here - log.error( - u'Invalid value (return_retry_timer: %s or ' - u'return_retry_timer_max: %s). Both must be positive ' - u'integers.', - self.opts[u'return_retry_timer'], - self.opts[u'return_retry_timer_max'], - ) - log.debug(msg.format(DEFAULT_MINION_OPTS[u'return_retry_timer'])) - return DEFAULT_MINION_OPTS[u'return_retry_timer'] - else: - log.debug(msg.format(self.opts.get(u'return_retry_timer'))) - return self.opts.get(u'return_retry_timer') - # future lint: enable=str-format-in-logging - def _prep_mod_opts(self): ''' Returns a copy of the opts with key bits stripped out @@ -1773,6 +1773,92 @@ class Minion(MinionBase): log.trace(u'ret_val = %s', ret_val) # pylint: disable=no-member return ret_val + def _return_pub_multi(self, rets, ret_cmd='_return', timeout=60, sync=True): + ''' + Return the data from the executed command to the master server + ''' + if not isinstance(rets, list): + rets = [rets] + jids = {} + for ret in rets: + jid = ret.get(u'jid', ret.get(u'__jid__')) + fun = ret.get(u'fun', ret.get(u'__fun__')) + if self.opts[u'multiprocessing']: + fn_ = os.path.join(self.proc_dir, jid) + if os.path.isfile(fn_): + try: + os.remove(fn_) + except (OSError, IOError): + # The file is gone already + pass + log.info(u'Returning information for job: %s', jid) + load = jids.setdefault(jid, {}) + if ret_cmd == u'_syndic_return': + if not load: + load.update({u'id': self.opts[u'id'], + u'jid': jid, + u'fun': fun, + u'arg': ret.get(u'arg'), + u'tgt': ret.get(u'tgt'), + u'tgt_type': ret.get(u'tgt_type'), + u'load': ret.get(u'__load__'), + u'return': {}}) + if u'__master_id__' in ret: + load[u'master_id'] = ret[u'__master_id__'] + for key, value in six.iteritems(ret): + if key.startswith(u'__'): + continue + load[u'return'][key] = value + else: + load.update({u'id': self.opts[u'id']}) + for key, value in six.iteritems(ret): + load[key] = value + + if u'out' in ret: + if isinstance(ret[u'out'], six.string_types): + load[u'out'] = ret[u'out'] + else: + log.error( + u'Invalid outputter %s. This is likely a bug.', + ret[u'out'] + ) + else: + try: + oput = self.functions[fun].__outputter__ + except (KeyError, AttributeError, TypeError): + pass + else: + if isinstance(oput, six.string_types): + load[u'out'] = oput + if self.opts[u'cache_jobs']: + # Local job cache has been enabled + salt.utils.minion.cache_jobs(self.opts, load[u'jid'], ret) + + load = {u'cmd': ret_cmd, + u'load': jids.values()} + + def timeout_handler(*_): + log.warning( + u'The minion failed to return the job information for job %s. ' + u'This is often due to the master being shut down or ' + u'overloaded. If the master is running, consider increasing ' + u'the worker_threads value.', jid + ) + return True + + if sync: + try: + ret_val = self._send_req_sync(load, timeout=timeout) + except SaltReqTimeoutError: + timeout_handler() + return u'' + else: + with tornado.stack_context.ExceptionStackContext(timeout_handler): + ret_val = self._send_req_async(load, timeout=timeout, callback=lambda f: None) # pylint: disable=unexpected-keyword-arg + + log.trace(u'ret_val = %s', ret_val) # pylint: disable=no-member + return ret_val + def _state_run(self): ''' Execute a state run based on information set in the minion config file @@ -2469,14 +2555,6 @@ class Syndic(Minion): # In the future, we could add support for some clearfuncs, but # the syndic currently has no need. - @tornado.gen.coroutine - def _return_pub_multi(self, values): - for value in values: - yield self._return_pub(value, - u'_syndic_return', - timeout=self._return_retry_timer(), - sync=False) - @tornado.gen.coroutine def reconnect(self): if hasattr(self, u'pub_channel'): @@ -2704,7 +2782,10 @@ class SyndicManager(MinionBase): # Add not sent data to the delayed list and try the next master self.delayed.extend(data) continue - future = getattr(syndic_future.result(), func)(values) + future = getattr(syndic_future.result(), func)(values, + u'_syndic_return', + timeout=self._return_retry_timer(), + sync=False) self.pub_futures[master] = (future, values) return True # Loop done and didn't exit: wasn't sent, try again later @@ -2827,7 +2908,7 @@ class SyndicManager(MinionBase): self._call_syndic(u'_fire_master', kwargs={u'events': events, u'pretag': tagify(self.opts[u'id'], base=u'syndic'), - u'timeout': self.SYNDIC_EVENT_TIMEOUT, + u'timeout': self._return_retry_timer(), u'sync': False, }, ) From eb73bac25fec711ae494cf855888d837fedc1a8e Mon Sep 17 00:00:00 2001 From: Mike Place Date: Tue, 22 Aug 2017 15:11:42 -0600 Subject: [PATCH 010/633] Switch order of apply_to and priority --- salt/modules/rabbitmq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/rabbitmq.py b/salt/modules/rabbitmq.py index 0a3d798567..0a5cb82df8 100644 --- a/salt/modules/rabbitmq.py +++ b/salt/modules/rabbitmq.py @@ -848,7 +848,7 @@ def list_policies(vhost="/", runas=None): return ret -def set_policy(vhost, name, pattern, definition, apply_to=None, priority=None, runas=None): +def set_policy(vhost, name, pattern, definition, priority=None, apply_to=None, runas=None): ''' Set a policy based on rabbitmqctl set_policy. From 32d7d34fe599821297a0c5b9ef842b3dac9b808c Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Wed, 23 Aug 2017 21:31:28 +0200 Subject: [PATCH 011/633] First simple draft for the deletion verification --- salt/modules/kubernetes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index 2e17b11444..ba530db26b 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -40,6 +40,7 @@ import base64 import logging import yaml import tempfile +from time import sleep from salt.exceptions import CommandExecutionError from salt.ext.six import iteritems @@ -692,7 +693,12 @@ def delete_deployment(name, namespace='default', **kwargs): name=name, namespace=namespace, body=body) - return api_response.to_dict() + mutable_api_response = api_response.to_dict() + while show_deployment(name, namespace) is not None: + sleep(0.5) + else: + mutable_api_response['code'] = 200 + return mutable_api_response except (ApiException, HTTPError) as exc: if isinstance(exc, ApiException) and exc.status == 404: return None From 767af9bb4fc2c1a5916612c75ff4abb274a9b268 Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Mon, 28 Aug 2017 08:53:52 +0200 Subject: [PATCH 012/633] Added timeout for checking the deployment If the time limit is hit, the checking is aborted and we return with return-code None. --- salt/modules/kubernetes.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index ba530db26b..a842c7ccf1 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -40,7 +40,9 @@ import base64 import logging import yaml import tempfile +import signal from time import sleep +from contextlib import contextmanager from salt.exceptions import CommandExecutionError from salt.ext.six import iteritems @@ -68,6 +70,9 @@ log = logging.getLogger(__name__) __virtualname__ = 'kubernetes' +_polling_time_limit = 20 + + def __virtual__(): ''' @@ -79,6 +84,21 @@ def __virtual__(): return False, 'python kubernetes library not found' +class TimeoutException(Exception): + pass + + +@contextmanager +def _time_limit(seconds): + def signal_handler(signum, frame): + raise TimeoutException, "Timed out!" + signal.signal(signal.SIGALRM, signal_handler) + signal.alarm(seconds) + try: + yield + finally: + signal.alarm(0) + # pylint: disable=no-member def _setup_conn(**kwargs): ''' @@ -694,10 +714,14 @@ def delete_deployment(name, namespace='default', **kwargs): namespace=namespace, body=body) mutable_api_response = api_response.to_dict() - while show_deployment(name, namespace) is not None: - sleep(0.5) - else: - mutable_api_response['code'] = 200 + try: + with _time_limit(_polling_time_limit): + while show_deployment(name, namespace) is not None: + sleep(1) + else: + mutable_api_response['code'] = 200 + except TimeoutException: + pass return mutable_api_response except (ApiException, HTTPError) as exc: if isinstance(exc, ApiException) and exc.status == 404: From 52b1cb814752e534c9faefbc3748484619f31a0f Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Mon, 28 Aug 2017 17:01:43 +0200 Subject: [PATCH 013/633] Compatibility with Python3.6 --- salt/modules/kubernetes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index a842c7ccf1..39a4da473e 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -91,7 +91,7 @@ class TimeoutException(Exception): @contextmanager def _time_limit(seconds): def signal_handler(signum, frame): - raise TimeoutException, "Timed out!" + raise(TimeoutException, "Timed out!") signal.signal(signal.SIGALRM, signal_handler) signal.alarm(seconds) try: @@ -99,6 +99,7 @@ def _time_limit(seconds): finally: signal.alarm(0) + # pylint: disable=no-member def _setup_conn(**kwargs): ''' From 3fe623778e9f7cce5a020b43e111627cc8c42e55 Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Tue, 29 Aug 2017 09:25:04 +0200 Subject: [PATCH 014/633] Added Windows fallback Linux uses signal.alarm to just terminate the polling when a time limit is hit.For Windows are are just counting the loop cycles. --- salt/modules/kubernetes.py | 51 +++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index 39a4da473e..c089bab05d 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -41,6 +41,7 @@ import logging import yaml import tempfile import signal +from sys import platform from time import sleep from contextlib import contextmanager @@ -70,9 +71,6 @@ log = logging.getLogger(__name__) __virtualname__ = 'kubernetes' -_polling_time_limit = 20 - - def __virtual__(): ''' @@ -88,16 +86,19 @@ class TimeoutException(Exception): pass -@contextmanager -def _time_limit(seconds): - def signal_handler(signum, frame): - raise(TimeoutException, "Timed out!") - signal.signal(signal.SIGALRM, signal_handler) - signal.alarm(seconds) - try: - yield - finally: - signal.alarm(0) +if not platform.startswith("win"): + @contextmanager + def _time_limit(seconds): + def signal_handler(signum, frame): + raise(TimeoutException, "Timed out!") + signal.signal(signal.SIGALRM, signal_handler) + signal.alarm(seconds) + try: + yield + finally: + signal.alarm(0) + + _polling_time_limit = 30 # pylint: disable=no-member @@ -715,14 +716,24 @@ def delete_deployment(name, namespace='default', **kwargs): namespace=namespace, body=body) mutable_api_response = api_response.to_dict() - try: - with _time_limit(_polling_time_limit): - while show_deployment(name, namespace) is not None: - sleep(1) - else: + if not platform.startswith("win"): + try: + with _time_limit(_polling_time_limit): + while show_deployment(name, namespace) is not None: + sleep(1) + else: + mutable_api_response['code'] = 200 + except TimeoutException: + pass + else: + # Windows has not signal.alarm implementation, so we are just falling + # back to loop-counting. + for i in range(60): + if show_deployment(name, namespace) is None: mutable_api_response['code'] = 200 - except TimeoutException: - pass + break + else: + sleep(1) return mutable_api_response except (ApiException, HTTPError) as exc: if isinstance(exc, ApiException) and exc.status == 404: From 78afa6e58de80dd7c77fcede7718186c141167ba Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Tue, 29 Aug 2017 11:48:35 +0300 Subject: [PATCH 015/633] Support old syndics not aggregating returns. --- salt/master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/master.py b/salt/master.py index 0045954570..1bb7f04356 100644 --- a/salt/master.py +++ b/salt/master.py @@ -1419,7 +1419,7 @@ class AESFuncs(object): ''' loads = load.get(u'load') if not isinstance(loads, list): - loads = [loads] + loads = [load] # support old syndics not aggregating returns for load in loads: # Verify the load if any(key not in load for key in (u'return', u'jid', u'id')): From 702a058c38ea570d8fc323fd32132088f814d16f Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Tue, 29 Aug 2017 11:33:37 +0200 Subject: [PATCH 016/633] Fixed linting * Python3 error for exception raising and * Disabled linting for while-loop since the loop is broken via timeout. --- salt/modules/kubernetes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index c089bab05d..7e01242e49 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -90,7 +90,7 @@ if not platform.startswith("win"): @contextmanager def _time_limit(seconds): def signal_handler(signum, frame): - raise(TimeoutException, "Timed out!") + raise TimeoutException signal.signal(signal.SIGALRM, signal_handler) signal.alarm(seconds) try: @@ -719,7 +719,7 @@ def delete_deployment(name, namespace='default', **kwargs): if not platform.startswith("win"): try: with _time_limit(_polling_time_limit): - while show_deployment(name, namespace) is not None: + while show_deployment(name, namespace) is not None: # pylint: disable=useless-else-on-loop sleep(1) else: mutable_api_response['code'] = 200 From 5de8f9ce3ebbbf3e65b7f508d2989bc750659b8b Mon Sep 17 00:00:00 2001 From: vernoncole Date: Tue, 29 Aug 2017 17:47:41 -0600 Subject: [PATCH 017/633] Oxygen release notes for vagrant and saltify drivers --- doc/topics/releases/oxygen.rst | 56 +++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 9396a1d74b..327ebb6b81 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -97,6 +97,57 @@ file. For example: These commands will run in sequence **before** the bootstrap script is executed. +New salt-cloud Grains +===================== + +When salt cloud creates a new minon, it will now add grain information +to the minion configuration file, identifying the resources originally used +to create it. + +The generated grain information will appear similar to: + +.. code-block:: yaml + + grains: + salt-cloud: + driver: ec2 + provider: my_ec2:ec2 + profile: ec2-web + +The generation of salt-cloud grains can be surpressed by the +option ``enable_cloud_grains: 'False'`` in the cloud configuration file. + +Upgraded Saltify Driver +======================= + +The salt-cloud Saltify driver is used to provision machines which +are not controlled by a dedicated cloud supervisor (such as typical hardware +machines) by pushing a salt-bootstrap command to them and accepting them on +the salt master. Creation of a node has been its only function and no other +salt-cloud commands were implemented. + +With this upgrade, it can use the salt-api to provide advanced control, +such as rebooting a machine, querying it along with conventional cloud minions, +and, ultimately, disconnecting it from its master. + +After disconnection from ("destroying" on) one master, a machine can be +re-purposed by connecting to ("creating" on) a subsequent master. + +New Vagrant Driver +================== + +The salt-cloud Vagrant driver brings virtual machines running in a limited +environment, such as a programmer's workstation, under salt-cloud control. +This can be useful for experimentation, instruction, or testing salt configurations. + +Using salt-api on the master, and a salt-minion running on the host computer, +the Vagrant driver can create (``vagrant up``), restart (``vagrant reload``), +and destroy (``vagrant destroy``) VMs, as controlled by salt-cloud profiles +which designate a ``Vagrantfile`` on the host machine. + +The master can be a very limited machine, such as a Raspberry Pi, or a small +VagrantBox VM. + Newer PyWinRM Versions ---------------------- @@ -617,11 +668,6 @@ Profitbricks Cloud Updated Dependency The minimum version of the ``profitbrick`` python package for the ``profitbricks`` cloud driver has changed from 3.0.0 to 3.1.0. -Azure Cloud Updated Dependency ------------------------------- - -The azure sdk used for the ``azurearm`` cloud driver now depends on ``azure-cli>=2.0.12`` - Module Deprecations =================== From 56938d5bf28fd829f0d5dd03ac20efd04790c619 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Thu, 31 Aug 2017 16:50:22 +0300 Subject: [PATCH 018/633] Fix ldap token groups auth. --- salt/auth/__init__.py | 34 +++++++++++++++++++++------------- salt/auth/ldap.py | 4 ++-- salt/daemons/masterapi.py | 21 +++------------------ salt/master.py | 21 +++------------------ 4 files changed, 29 insertions(+), 51 deletions(-) diff --git a/salt/auth/__init__.py b/salt/auth/__init__.py index f90488e153..e39ecf8373 100644 --- a/salt/auth/__init__.py +++ b/salt/auth/__init__.py @@ -200,7 +200,7 @@ class LoadAuth(object): ''' if not self.authenticate_eauth(load): return {} - fstr = '{0}.auth'.format(load['eauth']) + hash_type = getattr(hashlib, self.opts.get('hash_type', 'md5')) tok = str(hash_type(os.urandom(512)).hexdigest()) t_path = os.path.join(self.opts['token_dir'], tok) @@ -224,8 +224,9 @@ class LoadAuth(object): acl_ret = self.__get_acl(load) tdata['auth_list'] = acl_ret - if 'groups' in load: - tdata['groups'] = load['groups'] + groups = self.get_groups(load) + if groups: + tdata['groups'] = groups try: with salt.utils.files.set_umask(0o177): @@ -345,7 +346,7 @@ class LoadAuth(object): return False return True - def get_auth_list(self, load): + def get_auth_list(self, load, token=None): ''' Retrieve access list for the user specified in load. The list is built by eauth module or from master eauth configuration. @@ -353,30 +354,37 @@ class LoadAuth(object): list if the user has no rights to execute anything on this master and returns non-empty list if user is allowed to execute particular functions. ''' + # Get auth list from token + if token and self.opts['keep_acl_in_token'] and 'auth_list' in token: + return token['auth_list'] # Get acl from eauth module. auth_list = self.__get_acl(load) if auth_list is not None: return auth_list - if load['eauth'] not in self.opts['external_auth']: + eauth = token['eauth'] if token else load['eauth'] + if eauth not in self.opts['external_auth']: # No matching module is allowed in config log.warning('Authorization failure occurred.') return None - name = self.load_name(load) # The username we are attempting to auth with - groups = self.get_groups(load) # The groups this user belongs to - eauth_config = self.opts['external_auth'][load['eauth']] - if groups is None or groups is False: + if token: + name = token['name'] + groups = token['groups'] + else: + name = self.load_name(load) # The username we are attempting to auth with + groups = self.get_groups(load) # The groups this user belongs to + eauth_config = self.opts['external_auth'][eauth] + if not groups: groups = [] group_perm_keys = [item for item in eauth_config if item.endswith('%')] # The configured auth groups # First we need to know if the user is allowed to proceed via any of their group memberships. group_auth_match = False for group_config in group_perm_keys: - group_config = group_config.rstrip('%') - for group in groups: - if group == group_config: - group_auth_match = True + if group_config.rstrip('%') in groups: + group_auth_match = True + break # If a group_auth_match is set it means only that we have a # user which matches at least one or more of the groups defined # in the configuration file. diff --git a/salt/auth/ldap.py b/salt/auth/ldap.py index 396c1d00a2..3065429815 100644 --- a/salt/auth/ldap.py +++ b/salt/auth/ldap.py @@ -306,7 +306,7 @@ def groups(username, **kwargs): ''' group_list = [] - bind = _bind(username, kwargs['password'], + bind = _bind(username, kwargs.get('password'), anonymous=_config('anonymous', mandatory=False)) if bind: log.debug('ldap bind to determine group membership succeeded!') @@ -371,7 +371,7 @@ def groups(username, **kwargs): search_results = bind.search_s(search_base, ldap.SCOPE_SUBTREE, search_string, - [_config('accountattributename'), 'cn']) + [_config('accountattributename'), 'cn', _config('groupattribute')]) for _, entry in search_results: if username in entry[_config('accountattributename')]: group_list.append(entry['cn'][0]) diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index 9ca6c582fb..d47a5c3aa6 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -1055,12 +1055,7 @@ class LocalFuncs(object): return dict(error=dict(name=err_name, message='Authentication failure of type "token" occurred.')) username = token['name'] - if self.opts['keep_acl_in_token'] and 'auth_list' in token: - auth_list = token['auth_list'] - else: - load['eauth'] = token['eauth'] - load['username'] = username - auth_list = self.loadauth.get_auth_list(load) + auth_list = self.loadauth.get_auth_list(load, token) else: auth_type = 'eauth' err_name = 'EauthAuthenticationError' @@ -1102,12 +1097,7 @@ class LocalFuncs(object): return dict(error=dict(name=err_name, message='Authentication failure of type "token" occurred.')) username = token['name'] - if self.opts['keep_acl_in_token'] and 'auth_list' in token: - auth_list = token['auth_list'] - else: - load['eauth'] = token['eauth'] - load['username'] = username - auth_list = self.loadauth.get_auth_list(load) + auth_list = self.loadauth.get_auth_list(load, token) elif 'eauth' in load: auth_type = 'eauth' err_name = 'EauthAuthenticationError' @@ -1217,12 +1207,7 @@ class LocalFuncs(object): return '' # Get acl from eauth module. - if self.opts['keep_acl_in_token'] and 'auth_list' in token: - auth_list = token['auth_list'] - else: - extra['eauth'] = token['eauth'] - extra['username'] = token['name'] - auth_list = self.loadauth.get_auth_list(extra) + auth_list = self.loadauth.get_auth_list(extra, token) # Authorize the request if not self.ckminions.auth_check( diff --git a/salt/master.py b/salt/master.py index 649a89a072..b913aeb1e5 100644 --- a/salt/master.py +++ b/salt/master.py @@ -1705,12 +1705,7 @@ class ClearFuncs(object): message='Authentication failure of type "token" occurred.')) # Authorize - if self.opts['keep_acl_in_token'] and 'auth_list' in token: - auth_list = token['auth_list'] - else: - clear_load['eauth'] = token['eauth'] - clear_load['username'] = token['name'] - auth_list = self.loadauth.get_auth_list(clear_load) + auth_list = self.loadauth.get_auth_list(clear_load, token) if not self.ckminions.runner_check(auth_list, clear_load['fun']): return dict(error=dict(name='TokenAuthenticationError', @@ -1774,12 +1769,7 @@ class ClearFuncs(object): message='Authentication failure of type "token" occurred.')) # Authorize - if self.opts['keep_acl_in_token'] and 'auth_list' in token: - auth_list = token['auth_list'] - else: - clear_load['eauth'] = token['eauth'] - clear_load['username'] = token['name'] - auth_list = self.loadauth.get_auth_list(clear_load) + auth_list = self.loadauth.get_auth_list(clear_load, token) if not self.ckminions.wheel_check(auth_list, clear_load['fun']): return dict(error=dict(name='TokenAuthenticationError', message=('Authentication failure of type "token" occurred for ' @@ -1900,12 +1890,7 @@ class ClearFuncs(object): return '' # Get acl - if self.opts['keep_acl_in_token'] and 'auth_list' in token: - auth_list = token['auth_list'] - else: - extra['eauth'] = token['eauth'] - extra['username'] = token['name'] - auth_list = self.loadauth.get_auth_list(extra) + auth_list = self.loadauth.get_auth_list(extra, token) # Authorize the request if not self.ckminions.auth_check( From f29f5b0cce79f7ef00c52fc7474f8724606030a6 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Thu, 31 Aug 2017 20:39:35 +0300 Subject: [PATCH 019/633] Fix for tests: don't require 'groups' in the eauth token. --- salt/auth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/auth/__init__.py b/salt/auth/__init__.py index e39ecf8373..73e4c98f8a 100644 --- a/salt/auth/__init__.py +++ b/salt/auth/__init__.py @@ -370,7 +370,7 @@ class LoadAuth(object): if token: name = token['name'] - groups = token['groups'] + groups = token.get('groups') else: name = self.load_name(load) # The username we are attempting to auth with groups = self.get_groups(load) # The groups this user belongs to From eb526c93ca03b7594139fbb518063ff7f5017cd5 Mon Sep 17 00:00:00 2001 From: Joaquin Veira Date: Mon, 4 Sep 2017 14:13:40 +0200 Subject: [PATCH 020/633] Update zabbix_return.py test ServerActive IP addresses before using them as it may be that your host is not being monitored by one of them and current returner fails to complete this action --- salt/returners/zabbix_return.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/salt/returners/zabbix_return.py b/salt/returners/zabbix_return.py index 6470fe31ff..2415f37514 100644 --- a/salt/returners/zabbix_return.py +++ b/salt/returners/zabbix_return.py @@ -54,9 +54,22 @@ def zbx(): return False -def zabbix_send(key, host, output): - cmd = zbx()['sender'] + " -c " + zbx()['config'] + " -s " + host + " -k " + key + " -o \"" + output +"\"" - __salt__['cmd.shell'](cmd) +def zabbix_send(key, host, output): + f = open('/etc/zabbix/zabbix_agentd.conf','r') + for line in f: + if "ServerActive" in line: + flag = "true" + server = line.rsplit('=') + server = server[1].rsplit(',') + for s in server: + cmd = zbx()['sender'] + " -z " + s.replace('\n','') + " -s " + host + " -k " + key + " -o \"" + output +"\"" + __salt__['cmd.shell'](cmd) + break + else: + flag = "false" + if flag == 'false': + cmd = zbx()['sender'] + " -c " + zbx()['config'] + " -s " + host + " -k " + key + " -o \"" + output +"\"" + f.close() def returner(ret): From 227a753454875a48836b86a097305bb1a285055e Mon Sep 17 00:00:00 2001 From: Levi Dahl Michelsen Date: Tue, 5 Sep 2017 13:21:22 +0200 Subject: [PATCH 021/633] Added RethinkDB external pillar module --- salt/pillar/rethinkdb_pillar.py | 160 ++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 salt/pillar/rethinkdb_pillar.py diff --git a/salt/pillar/rethinkdb_pillar.py b/salt/pillar/rethinkdb_pillar.py new file mode 100644 index 0000000000..377192a6de --- /dev/null +++ b/salt/pillar/rethinkdb_pillar.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +''' +Provide external pillar data from RethinkDB + +:depends: rethinkdb (on the salt-master) + + +salt master rethinkdb configuration +=================================== +These variables must be configured in your master configuration file. + * ``rethinkdb.host`` - The RethinkDB server. Defaults to ``'salt'`` + * ``rethinkdb.port`` - The port the RethinkDB server listens on. + Defaults to ``'28015'`` + * ``rethinkdb.database`` - The database to connect to. + Defaults to ``'salt'`` + * ``rethinkdb.username`` - The username for connecting to RethinkDB. + Defaults to ``''`` + * ``rethinkdb.password`` - The password for connecting to RethinkDB. + Defaults to ``''`` + + +salt-master ext_pillar configuration +==================================== + +The ext_pillar function arguments are given in single line dictionary notation. + +.. code-block:: yaml + + ext_pillar: + - rethinkdb: {table: ext_pillar, id_field: minion_id, field: pillar_root, pillar_key: external_pillar} + +In the example above the following happens. + * The salt-master will look for external pillars in the 'ext_pillar' table + on the RethinkDB host + * The minion id will be matched against the 'minion_id' field + * Pillars will be retrieved from the nested field 'pillar_root' + * Found pillars will be merged inside a key called 'external_pillar' + + +Module Documentation +==================== +''' +from __future__ import absolute_import + +# Import python libraries +import logging + +# Import 3rd party libraries +try: + import rethinkdb as r + HAS_RETHINKDB = True +except ImportError: + HAS_RETHINKDB = False + +__virtualname__ = 'rethinkdb' + +__opts__ = { + 'rethinkdb.host': 'salt', + 'rethinkdb.port': '28015', + 'rethinkdb.database': 'salt', + 'rethinkdb.username': None, + 'rethinkdb.password': None +} + + +def __virtual__(): + if not HAS_RETHINKDB: + return False + return True + + +# Configure logging +log = logging.getLogger(__name__) + + +def ext_pillar(minion_id, + pillar, + table='pillar', + id_field=None, + field=None, + pillar_key=None): + ''' + Collect minion external pillars from a RethinkDB database + +Arguments: + * `table`: The RethinkDB table containing external pillar information. + Defaults to ``'pillar'`` + * `id_field`: Field in document containing the minion id. + If blank then we assume the table index matches minion ids + * `field`: Specific field in the document used for pillar data, if blank + then the entire document will be used + * `pillar_key`: The salt-master will nest found external pillars under + this key before merging into the minion pillars. If blank, external + pillars will be merged at top level + ''' + host = __opts__['rethinkdb.host'] + port = __opts__['rethinkdb.port'] + database = __opts__['rethinkdb.database'] + username = __opts__['rethinkdb.username'] + password = __opts__['rethinkdb.password'] + + log.debug('Connecting to {0}:{1} as user \'{2}\' for RethinkDB ext_pillar' + .format(host, port, username)) + + # Connect to the database + conn = r.connect(host=host, + port=port, + db=database, + user=username, + password=password) + + data = None + + try: + + if id_field: + log.debug('ext_pillar.rethinkdb: looking up pillar. ' + 'table: {0}, field: {1}, minion: {2}'.format( + table, id_field, minion_id)) + + if field: + data = r.table(table).filter( + {id_field: minion_id}).pluck(field).run(conn) + else: + data = r.table(table).filter({id_field: minion_id}).run(conn) + + else: + log.debug('ext_pillar.rethinkdb: looking up pillar. ' + 'table: {0}, field: id, minion: {1}'.format( + table, minion_id)) + + if field: + data = r.table(table).get(minion_id).pluck(field).run(conn) + else: + data = r.table(table).get(minion_id).run(conn) + + finally: + if conn.is_open(): + conn.close() + + if data.items: + + # Return nothing if multiple documents are found for a minion + if len(data.items) > 1: + log.error('ext_pillar.rethinkdb: ambiguous documents found for ' + 'minion {0}'.format(minion_id)) + return {} + + else: + for document in data: + result = document + + if pillar_key: + return {pillar_key: result} + return result + + else: + # No document found in the database + log.debug('ext_pillar.rethinkdb: no document found') + return {} From 09dfa1d8006f2e6cdedb0aebbd7d5abcaa6f9e96 Mon Sep 17 00:00:00 2001 From: Levi Dahl Michelsen Date: Tue, 5 Sep 2017 19:53:57 +0200 Subject: [PATCH 022/633] Replaced for loop on result cursor with data.items.pop() --- salt/pillar/rethinkdb_pillar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/pillar/rethinkdb_pillar.py b/salt/pillar/rethinkdb_pillar.py index 377192a6de..5156b23d50 100644 --- a/salt/pillar/rethinkdb_pillar.py +++ b/salt/pillar/rethinkdb_pillar.py @@ -147,8 +147,7 @@ Arguments: return {} else: - for document in data: - result = document + result = data.items.pop() if pillar_key: return {pillar_key: result} @@ -158,3 +157,4 @@ Arguments: # No document found in the database log.debug('ext_pillar.rethinkdb: no document found') return {} + From b3934c8431afec250ba502eb5eb1cad25fea09a0 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Tue, 5 Sep 2017 16:22:08 -0600 Subject: [PATCH 023/633] Remove trailing newlines --- salt/pillar/rethinkdb_pillar.py | 1 - 1 file changed, 1 deletion(-) diff --git a/salt/pillar/rethinkdb_pillar.py b/salt/pillar/rethinkdb_pillar.py index 5156b23d50..0a4793205f 100644 --- a/salt/pillar/rethinkdb_pillar.py +++ b/salt/pillar/rethinkdb_pillar.py @@ -157,4 +157,3 @@ Arguments: # No document found in the database log.debug('ext_pillar.rethinkdb: no document found') return {} - From 99fe1383254188417697fbea62648e1976edc488 Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Wed, 6 Sep 2017 08:52:23 +0200 Subject: [PATCH 024/633] Code styling and added log message for timeout --- salt/modules/kubernetes.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index 7e01242e49..dcc365a471 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -98,7 +98,7 @@ if not platform.startswith("win"): finally: signal.alarm(0) - _polling_time_limit = 30 + POLLING_TIME_LIMIT = 30 # pylint: disable=no-member @@ -718,7 +718,7 @@ def delete_deployment(name, namespace='default', **kwargs): mutable_api_response = api_response.to_dict() if not platform.startswith("win"): try: - with _time_limit(_polling_time_limit): + with _time_limit(POLLING_TIME_LIMIT): while show_deployment(name, namespace) is not None: # pylint: disable=useless-else-on-loop sleep(1) else: @@ -734,6 +734,10 @@ def delete_deployment(name, namespace='default', **kwargs): break else: sleep(1) + if mutable_api_response['code'] != 200: + log.warning("Reached polling time limit. Deployment is not yet " + "deleted, but we are backing off. Sorry, but you'll " + "have to check manually.") return mutable_api_response except (ApiException, HTTPError) as exc: if isinstance(exc, ApiException) and exc.status == 404: From daf4948b3d2fef2a9f58b1810464664aae3c9337 Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Wed, 6 Sep 2017 10:16:51 +0200 Subject: [PATCH 025/633] Catching error when PIDfile cannot be deleted Usually the PIDfile is locate in /run. If Salt is not started with root permissions, it is not able to delete the PIDfile in /run. It should be safe to just log and ignore this error, since Salt overwrites the PIDfile on the next start. --- salt/utils/parsers.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/salt/utils/parsers.py b/salt/utils/parsers.py index ac96bec4d6..f05949f7d2 100644 --- a/salt/utils/parsers.py +++ b/salt/utils/parsers.py @@ -882,7 +882,14 @@ class DaemonMixIn(six.with_metaclass(MixInMeta, object)): # We've loaded and merged options into the configuration, it's safe # to query about the pidfile if self.check_pidfile(): - os.unlink(self.config['pidfile']) + try: + os.unlink(self.config['pidfile']) + except OSError as err: + self.info( + 'PIDfile could not be deleted: {0}'.format( + self.config['pidfile'], traceback.format_exc(err) + ) + ) def set_pidfile(self): from salt.utils.process import set_pidfile From 6e3eb76c7953f252e2fdf0ab6105c564b79af0ef Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Wed, 6 Sep 2017 13:16:10 +0200 Subject: [PATCH 026/633] Removed unused format argument --- salt/utils/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/utils/parsers.py b/salt/utils/parsers.py index f05949f7d2..b89a0e45c7 100644 --- a/salt/utils/parsers.py +++ b/salt/utils/parsers.py @@ -887,7 +887,7 @@ class DaemonMixIn(six.with_metaclass(MixInMeta, object)): except OSError as err: self.info( 'PIDfile could not be deleted: {0}'.format( - self.config['pidfile'], traceback.format_exc(err) + self.config['pidfile'] ) ) From 7b600e283297bd5fa473c79984ae3b97869fcd90 Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Wed, 6 Sep 2017 17:03:29 +0200 Subject: [PATCH 027/633] Added pylint-disable statements and import for salt.ext.six.moves.range --- salt/modules/kubernetes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index dcc365a471..ab238edc59 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -49,6 +49,7 @@ from salt.exceptions import CommandExecutionError from salt.ext.six import iteritems import salt.utils import salt.utils.templates +from salt.ext.six.moves import range # pylint: disable=import-error try: import kubernetes # pylint: disable=import-self @@ -719,9 +720,9 @@ def delete_deployment(name, namespace='default', **kwargs): if not platform.startswith("win"): try: with _time_limit(POLLING_TIME_LIMIT): - while show_deployment(name, namespace) is not None: # pylint: disable=useless-else-on-loop + while show_deployment(name, namespace) is not None: sleep(1) - else: + else: # pylint: disable=useless-else-on-loop mutable_api_response['code'] = 200 except TimeoutException: pass From 842b07fd257d53046a6d968a74f66c7f015550c4 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Sun, 20 Aug 2017 09:06:39 -0400 Subject: [PATCH 028/633] Prevent spurious "Template does not exist" error This was merged previously (though slightly differently) in #39516 Took me a second to track it down and then realized that I fixed this in 2016.x --- salt/pillar/__init__.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py index a62e11dc77..8d5eb7e998 100644 --- a/salt/pillar/__init__.py +++ b/salt/pillar/__init__.py @@ -405,20 +405,19 @@ class Pillar(object): self.opts['pillarenv'], ', '.join(self.opts['file_roots']) ) else: - tops[self.opts['pillarenv']] = [ - compile_template( - self.client.cache_file( - self.opts['state_top'], - self.opts['pillarenv'] - ), - self.rend, - self.opts['renderer'], - self.opts['renderer_blacklist'], - self.opts['renderer_whitelist'], - self.opts['pillarenv'], - _pillar_rend=True, - ) - ] + top = self.client.cache_file(self.opts['state_top'], self.opts['pillarenv']) + if top: + tops[self.opts['pillarenv']] = [ + compile_template( + top, + self.rend, + self.opts['renderer'], + self.opts['renderer_blacklist'], + self.opts['renderer_whitelist'], + self.opts['pillarenv'], + _pillar_rend=True, + ) + ] else: for saltenv in self._get_envs(): if self.opts.get('pillar_source_merging_strategy', None) == "none": From 20619b24c458f56b8d690e912feed6474d63db8a Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Thu, 7 Sep 2017 10:20:46 +0200 Subject: [PATCH 029/633] Fixed test for delete_deployment Due to implementation change, we need to mock the return value. --- tests/unit/modules/test_kubernetes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/test_kubernetes.py b/tests/unit/modules/test_kubernetes.py index 1de939f6b0..46ac760158 100644 --- a/tests/unit/modules/test_kubernetes.py +++ b/tests/unit/modules/test_kubernetes.py @@ -104,9 +104,9 @@ class KubernetesTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}): mock_kubernetes_lib.client.V1DeleteOptions = Mock(return_value="") mock_kubernetes_lib.client.ExtensionsV1beta1Api.return_value = Mock( - **{"delete_namespaced_deployment.return_value.to_dict.return_value": {}} + **{"delete_namespaced_deployment.return_value.to_dict.return_value": {'code': 200}} ) - self.assertEqual(kubernetes.delete_deployment("test"), {}) + self.assertEqual(kubernetes.delete_deployment("test"), {'code': 200}) self.assertTrue( kubernetes.kubernetes.client.ExtensionsV1beta1Api(). delete_namespaced_deployment().to_dict.called) From ac3386fae6c070bb05f58df750f53343f4f0c316 Mon Sep 17 00:00:00 2001 From: assaf shapira Date: Thu, 7 Sep 2017 14:11:32 +0300 Subject: [PATCH 030/633] handle cases where a vm doesn't have "base_template_name" attribute for example, when a VM was imported from another XEN cluster etc' modified: salt/cloud/clouds/xen.py --- salt/cloud/clouds/xen.py | 59 +++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/salt/cloud/clouds/xen.py b/salt/cloud/clouds/xen.py index d1caaab282..87f5175aa4 100644 --- a/salt/cloud/clouds/xen.py +++ b/salt/cloud/clouds/xen.py @@ -129,6 +129,9 @@ def get_configured_provider(): def _get_session(): ''' Get a connection to the XenServer host + note: a session can be opened only to the pool master + if the a connection attempmt is made to a non pool master machine + an exception will be raised ''' api_version = '1.0' originator = 'salt_cloud_{}_driver'.format(__virtualname__) @@ -157,13 +160,29 @@ def _get_session(): default=False, search_global=False ) - session = XenAPI.Session(url, ignore_ssl=ignore_ssl) - log.debug('url: {} user: {} password: {}, originator: {}'.format( - url, - user, - 'XXX-pw-redacted-XXX', - originator)) - session.xenapi.login_with_password(user, password, api_version, originator) + try: + session = XenAPI.Session(url, ignore_ssl=ignore_ssl) + log.debug('url: {} user: {} password: {}, originator: {}'.format( + url, + user, + 'XXX-pw-redacted-XXX', + originator)) + session.xenapi.login_with_password(user, password, api_version, originator) + except XenAPI.Failure as ex: + ''' + if the server on the url is not the pool master, + the pool master's address will be rturned in the exception message + ''' + pool_master_addr = str(ex.__dict__['details'][1]) + slash_parts = url.split('/') + new_url = '/'.join(slash_parts[:2]) + '/' + pool_master_addr + session = XenAPI.Session(new_url, ignore_ssl=ignore_ssl) + log.debug('url: {} user: {} password: {}, originator: {}'.format( + url, + user, + 'XXX-pw-redacted-XXX', + originator)) + session.xenapi.login_with_password(user, password, api_version, originator) return session @@ -182,9 +201,15 @@ def list_nodes(): for vm in vms: record = session.xenapi.VM.get_record(vm) if not record['is_a_template'] and not record['is_control_domain']: + try: + base_template_name = record['other_config']['base_template_name'] + except Exception as KeyError: + base_template_name = None + log.debug( + 'VM returned no base template name: {}'.format(name)) ret[record['name_label']] = { 'id': record['uuid'], - 'image': record['other_config']['base_template_name'], + 'image': base_template_name, 'name': record['name_label'], 'size': record['memory_dynamic_max'], 'state': record['power_state'], @@ -296,10 +321,17 @@ def list_nodes_full(session=None): for vm in vms: record = session.xenapi.VM.get_record(vm) if not record['is_a_template'] and not record['is_control_domain']: + # catch cases where vm doesn't have a base template value + try: + base_template_name = record['other_config']['base_template_name'] + except Exception as KeyError: + base_template_name = None + log.debug( + 'VM returned no base template name: {}'.format(name)) vm_cfg = session.xenapi.VM.get_record(vm) vm_cfg['id'] = record['uuid'] vm_cfg['name'] = record['name_label'] - vm_cfg['image'] = record['other_config']['base_template_name'] + vm_cfg['image'] = base_template_name vm_cfg['size'] = None vm_cfg['state'] = record['power_state'] vm_cfg['private_ips'] = get_vm_ip(record['name_label'], session) @@ -455,8 +487,15 @@ def show_instance(name, session=None, call=None): vm = _get_vm(name, session=session) record = session.xenapi.VM.get_record(vm) if not record['is_a_template'] and not record['is_control_domain']: + # catch cases where the VM doesn't have 'base_template_name' attribute + try: + base_template_name = record['other_config']['base_template_name'] + log.debug( + 'VM returned no base template name: {}'.format(name)) + except Exception as KeyError: + base_template_name = None ret = {'id': record['uuid'], - 'image': record['other_config']['base_template_name'], + 'image': base_template_name, 'name': record['name_label'], 'size': record['memory_dynamic_max'], 'state': record['power_state'], From 0c71da95f67197bd339f5179e206813c49bef06a Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Thu, 7 Sep 2017 15:47:13 +0200 Subject: [PATCH 031/633] Using salt method to identify MS Windows, single instead of double quotes --- salt/modules/kubernetes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index ab238edc59..f26630edc7 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -87,7 +87,7 @@ class TimeoutException(Exception): pass -if not platform.startswith("win"): +if salt.utils.is_windows(): @contextmanager def _time_limit(seconds): def signal_handler(signum, frame): @@ -717,7 +717,7 @@ def delete_deployment(name, namespace='default', **kwargs): namespace=namespace, body=body) mutable_api_response = api_response.to_dict() - if not platform.startswith("win"): + if salt.utils.is_windows(): try: with _time_limit(POLLING_TIME_LIMIT): while show_deployment(name, namespace) is not None: @@ -736,9 +736,9 @@ def delete_deployment(name, namespace='default', **kwargs): else: sleep(1) if mutable_api_response['code'] != 200: - log.warning("Reached polling time limit. Deployment is not yet " - "deleted, but we are backing off. Sorry, but you'll " - "have to check manually.") + log.warning('Reached polling time limit. Deployment is not yet ' + 'deleted, but we are backing off. Sorry, but you\'ll ' + 'have to check manually.') return mutable_api_response except (ApiException, HTTPError) as exc: if isinstance(exc, ApiException) and exc.status == 404: From 7431ec64e3d8dbc1dd524300c0f4bcaebd88fdf8 Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Fri, 8 Sep 2017 08:20:14 +0200 Subject: [PATCH 032/633] Removed unused sys import --- salt/modules/kubernetes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index f26630edc7..aa06645660 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -41,7 +41,6 @@ import logging import yaml import tempfile import signal -from sys import platform from time import sleep from contextlib import contextmanager From 7ead7fc48abd7d32ad3591bc109050954460e022 Mon Sep 17 00:00:00 2001 From: assaf shapira Date: Sun, 10 Sep 2017 13:10:37 +0300 Subject: [PATCH 033/633] * if trying to connect to a XEN server which is not the pool master, the module will now switch the connection to the pool master (pool master info is returned as part of the exception raised by the XENapi when trying to get a session from a pool member) * handle a case wares a VM doesn't have a base image attribute this is the case with default system templates and imported VMs --- salt/cloud/clouds/xen.py | 49 ++++++++-------------------------------- 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/salt/cloud/clouds/xen.py b/salt/cloud/clouds/xen.py index 87f5175aa4..49a0202c70 100644 --- a/salt/cloud/clouds/xen.py +++ b/salt/cloud/clouds/xen.py @@ -129,9 +129,6 @@ def get_configured_provider(): def _get_session(): ''' Get a connection to the XenServer host - note: a session can be opened only to the pool master - if the a connection attempmt is made to a non pool master machine - an exception will be raised ''' api_version = '1.0' originator = 'salt_cloud_{}_driver'.format(__virtualname__) @@ -153,15 +150,8 @@ def _get_session(): __opts__, search_global=False ) - ignore_ssl = config.get_cloud_config_value( - 'ignore_ssl', - get_configured_provider(), - __opts__, - default=False, - search_global=False - ) try: - session = XenAPI.Session(url, ignore_ssl=ignore_ssl) + session = XenAPI.Session(url) log.debug('url: {} user: {} password: {}, originator: {}'.format( url, user, @@ -170,19 +160,18 @@ def _get_session(): session.xenapi.login_with_password(user, password, api_version, originator) except XenAPI.Failure as ex: ''' - if the server on the url is not the pool master, - the pool master's address will be rturned in the exception message + if the server on the url is not the pool master, the pool master's address will be rturned in the exception message ''' pool_master_addr = str(ex.__dict__['details'][1]) slash_parts = url.split('/') new_url = '/'.join(slash_parts[:2]) + '/' + pool_master_addr - session = XenAPI.Session(new_url, ignore_ssl=ignore_ssl) - log.debug('url: {} user: {} password: {}, originator: {}'.format( - url, + session = XenAPI.Session(new_url) + log.debug('session is -> url: {} user: {} password: {}, originator:{}'.format( + new_url, user, 'XXX-pw-redacted-XXX', originator)) - session.xenapi.login_with_password(user, password, api_version, originator) + session.xenapi.login_with_password(user,password,api_version,originator) return session @@ -201,15 +190,9 @@ def list_nodes(): for vm in vms: record = session.xenapi.VM.get_record(vm) if not record['is_a_template'] and not record['is_control_domain']: - try: - base_template_name = record['other_config']['base_template_name'] - except Exception as KeyError: - base_template_name = None - log.debug( - 'VM returned no base template name: {}'.format(name)) ret[record['name_label']] = { 'id': record['uuid'], - 'image': base_template_name, + 'image': record['other_config']['base_template_name'], 'name': record['name_label'], 'size': record['memory_dynamic_max'], 'state': record['power_state'], @@ -321,17 +304,10 @@ def list_nodes_full(session=None): for vm in vms: record = session.xenapi.VM.get_record(vm) if not record['is_a_template'] and not record['is_control_domain']: - # catch cases where vm doesn't have a base template value - try: - base_template_name = record['other_config']['base_template_name'] - except Exception as KeyError: - base_template_name = None - log.debug( - 'VM returned no base template name: {}'.format(name)) vm_cfg = session.xenapi.VM.get_record(vm) vm_cfg['id'] = record['uuid'] vm_cfg['name'] = record['name_label'] - vm_cfg['image'] = base_template_name + vm_cfg['image'] = record['other_config']['base_template_name'] vm_cfg['size'] = None vm_cfg['state'] = record['power_state'] vm_cfg['private_ips'] = get_vm_ip(record['name_label'], session) @@ -487,15 +463,8 @@ def show_instance(name, session=None, call=None): vm = _get_vm(name, session=session) record = session.xenapi.VM.get_record(vm) if not record['is_a_template'] and not record['is_control_domain']: - # catch cases where the VM doesn't have 'base_template_name' attribute - try: - base_template_name = record['other_config']['base_template_name'] - log.debug( - 'VM returned no base template name: {}'.format(name)) - except Exception as KeyError: - base_template_name = None ret = {'id': record['uuid'], - 'image': base_template_name, + 'image': record['other_config']['base_template_name'], 'name': record['name_label'], 'size': record['memory_dynamic_max'], 'state': record['power_state'], From c471a29527551e7b3fccabd47ce4e98f1e050e80 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Fri, 8 Sep 2017 14:10:46 -0600 Subject: [PATCH 034/633] make cache dirs when spm starts --- salt/cli/spm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/salt/cli/spm.py b/salt/cli/spm.py index 3d347c80a8..303e5ce65f 100644 --- a/salt/cli/spm.py +++ b/salt/cli/spm.py @@ -14,7 +14,7 @@ from __future__ import absolute_import # Import Salt libs import salt.spm import salt.utils.parsers as parsers -from salt.utils.verify import verify_log +from salt.utils.verify import verify_log, verify_env class SPM(parsers.SPMParser): @@ -29,6 +29,10 @@ class SPM(parsers.SPMParser): ui = salt.spm.SPMCmdlineInterface() self.parse_args() self.setup_logfile_logger() + v_dirs = [ + self.config['cachedir'], + ] + verify_env(v_dirs, self.config['user'],) verify_log(self.config) client = salt.spm.SPMClient(ui, self.config) client.run(self.args) From 68f529ee5ea891db9e8c7d32791710c9160a04aa Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 11 Sep 2017 10:58:16 -0400 Subject: [PATCH 035/633] Add 2016.11.8 release notes --- doc/topics/releases/2016.11.8.rst | 1719 +++++++++++++++++++++++++++++ 1 file changed, 1719 insertions(+) create mode 100644 doc/topics/releases/2016.11.8.rst diff --git a/doc/topics/releases/2016.11.8.rst b/doc/topics/releases/2016.11.8.rst new file mode 100644 index 0000000000..9f4eb68dab --- /dev/null +++ b/doc/topics/releases/2016.11.8.rst @@ -0,0 +1,1719 @@ +============================ +Salt 2016.11.8 Release Notes +============================ + +Version 2016.11.8 is a bugfix release for :ref:`2016.11.0 `.] + +Changes for v2016.11.7..v2016.11.8 +---------------------------------- + +Extended changelog courtesy of Todd Stansell (https://github.com/tjstansell/salt-changelogs): + +*Generated at: 2017-09-11T14:52:27Z* + +Statistics: + +- Total Merges: **169** +- Total Issue references: **70** +- Total PR references: **206** + +Changes: + + +- **PR** `#43271`_: (*twangboy*) Fix minor formatting issue + @ *2017-08-30T18:35:12Z* + + * cf21f91 Merge pull request `#43271`_ from twangboy/win_fix_pkg.install + * 91b062f Fix formatting issue, spaces surrounding + + +- **PR** `#43228`_: (*twangboy*) Win fix pkg.install + @ *2017-08-30T14:26:21Z* + + * 3a0b02f Merge pull request `#43228`_ from twangboy/win_fix_pkg.install + * 13dfabb Fix regex statement, add `.` + + * 31ff69f Add underscore to regex search + + * 3cf2b65 Fix spelling + + * ed030a3 Use regex to detect salt-minion install + + * e5daff4 Fix pkg.install + +- **PR** `#43191`_: (*viktorkrivak*) Fix apache.config with multiple statement + @ *2017-08-28T18:13:44Z* + + * b4c689d Merge pull request `#43191`_ from viktorkrivak/fix-apache-config-multi-entity + * c15bcbe Merge remote-tracking branch 'upstream/2016.11' into fix-apache-config-multi-entity + + * 4164047 Fix apache.config with multiple statement At this moment when you post more than one statement in config only last is used. Also file is rewrited multiple times until last statement is written. Example: salt '*' apache.config /etc/httpd/conf.d/ports.conf config="[{'Listen': '8080'}, {'Proxy': "Something"}]" Ends only with Proxy Something and ignore Listen 8080, This patch fix this issue. + +- **PR** `#43154`_: (*lomeroe*) Backport `#43116`_ to 2016.11 + @ *2017-08-28T16:40:41Z* + + - **ISSUE** `#42279`_: (*dafyddj*) win_lgpo matches multiple policies due to startswith() + | refs: `#43116`_ `#43116`_ `#43154`_ + - **PR** `#43116`_: (*lomeroe*) Fix 42279 in develop + | refs: `#43154`_ + * b90e59e Merge pull request `#43154`_ from lomeroe/`bp-43116`_-2016.11 + * 8f593b0 verify that files exist before trying to remove them, win_file.remove raises an exception if the file does not exist + + * 33a30ba correcting bad format statement in search for policy to be disabled + + * acc3d7a correct fopen calls from salt.utils for 2016.11's utils function + + * 2da1cdd lint fix + + * 61bd12c track xml namespace to ensure policies w/duplicate IDs or Names do not conflict + + * f232bed add additional checks for ADM policies that have the same ADMX policy ID (`#42279`_) + +- **PR** `#43202`_: (*garethgreenaway*) Reverting previous augeas module changes + @ *2017-08-28T13:14:27Z* + + - **ISSUE** `#42642`_: (*githubcdr*) state.augeas + | refs: `#42669`_ `#43202`_ + * 5308c27 Merge pull request `#43202`_ from garethgreenaway/42642_2016_11_augeas_module_revert_fix + * ef7e93e Reverting this change due to it breaking other uses. + +- **PR** `#43103`_: (*aogier*) genesis.bootstrap deboostrap fix + @ *2017-08-25T20:48:23Z* + + - **ISSUE** `#43101`_: (*aogier*) genesis.bootstrap fails if no pkg AND exclude_pkgs (which can't be a string) + | refs: `#43103`_ + * f16b724 Merge pull request `#43103`_ from aogier/43101-genesis-bootstrap + * db94f3b better formatting + + * e5cc667 tests: fix a leftover and simplify some parts + + * 13e5997 lint + + * 216ced6 allow comma-separated pkgs lists, quote args, test deb behaviour + + * d8612ae fix debootstrap and enhance packages selection/deletion via cmdline + +- **PR** `#42663`_: (*jagguli*) Check remote tags before deciding to do a fetch `#42329`_ + @ *2017-08-25T20:14:32Z* + + - **ISSUE** `#42329`_: (*jagguli*) State git.latest does not pull latest tags + | refs: `#42663`_ + * 4863771 Merge pull request `#42663`_ from StreetHawkInc/fix_git_tag_check + * 2b5af5b Remove refs/tags prefix from remote tags + + * 3f2e96e Convert set to list for serializer + + * 2728e5d Only include new tags in changes + + * 4b1df2f Exclude annotated tags from checks + + * 389c037 Check remote tags before deciding to do a fetch `#42329`_ + +- **PR** `#43199`_: (*corywright*) Add `disk.format` alias for `disk.format_` + @ *2017-08-25T19:21:07Z* + + - **ISSUE** `#43198`_: (*corywright*) disk.format_ needs to be aliased to disk.format + | refs: `#43199`_ + * 4193e7f Merge pull request `#43199`_ from corywright/disk-format-alias + * f00d3a9 Add `disk.format` alias for `disk.format_` + +- **PR** `#43196`_: (*gtmanfred*) Pin request install to version for npm tests + @ *2017-08-25T18:43:06Z* + + - **ISSUE** `#495`_: (*syphernl*) mysql.* without having MySQL installed/configured gives traceback + | refs: `#43196`_ + * 5471f9f Merge pull request `#43196`_ from gtmanfred/2016.11 + * ccd2241 Pin request install to version + +- **PR** `#43178`_: (*terminalmage*) git.detached: Fix traceback when rev is a SHA and is not present locally + @ *2017-08-25T13:58:37Z* + + - **ISSUE** `#43143`_: (*abulford*) git.detached does not fetch if rev is missing from local + | refs: `#43178`_ + * ace2715 Merge pull request `#43178`_ from terminalmage/issue43143 + * 2640833 git.detached: Fix traceback when rev is a SHA and is not present locally + +- **PR** `#43179`_: (*terminalmage*) Fix missed deprecation + @ *2017-08-24T22:52:34Z* + + * 12e9507 Merge pull request `#43179`_ from terminalmage/old-deprecation + * 3adf8ad Fix missed deprecation + +- **PR** `#43171`_: (*terminalmage*) Add warning about adding new functions to salt/utils/__init__.py + @ *2017-08-24T19:10:23Z* + + * b595440 Merge pull request `#43171`_ from terminalmage/salt-utils-warning + * 7b5943a Add warning about adding new functions to salt/utils/__init__.py + +- **PR** `#43173`_: (*Ch3LL*) Add New Release Branch Strategy to Contribution Docs + @ *2017-08-24T19:04:56Z* + + * 4f273ca Merge pull request `#43173`_ from Ch3LL/add_branch_docs + * 1b24244 Add New Release Branch Strategy to Contribution Docs + +- **PR** `#43151`_: (*ushmodin*) state.sls hangs on file.recurse with clean: True on windows + @ *2017-08-23T17:25:33Z* + + - **PR** `#42969`_: (*ushmodin*) state.sls hangs on file.recurse with clean: True on windows + | refs: `#43151`_ + * 669b376 Merge pull request `#43151`_ from ushmodin/2016.11 + * c5841e2 state.sls hangs on file.recurse with clean: True on windows + +- **PR** `#42986`_: (*renner*) Notify systemd synchronously (via NOTIFY_SOCKET) + @ *2017-08-22T16:52:56Z* + + * ae9d2b7 Merge pull request `#42986`_ from renner/systemd-notify + * 79c53f3 Fallback to systemd_notify_call() in case of socket.error + + * f176547 Notify systemd synchronously (via NOTIFY_SOCKET) + +- **PR** `#43037`_: (*mcarlton00*) Issue `#43036`_ Bhyve virtual grain in Linux VMs + @ *2017-08-22T16:43:40Z* + + - **ISSUE** `#43036`_: (*mcarlton00*) Linux VMs in Bhyve aren't displayed properly in grains + | refs: `#43037`_ + * b420fbe Merge pull request `#43037`_ from mcarlton00/fix-bhyve-grains + * 73315f0 Issue `#43036`_ Bhyve virtual grain in Linux VMs + +- **PR** `#43100`_: (*vutny*) [DOCS] Add missing `utils` sub-dir listed for `extension_modules` + @ *2017-08-22T15:40:09Z* + + * 0a86f2d Merge pull request `#43100`_ from vutny/doc-add-missing-utils-ext + * af743ff [DOCS] Add missing `utils` sub-dir listed for `extension_modules` + +- **PR** `#42985`_: (*DmitryKuzmenko*) Properly handle `prereq` having lost requisites. + @ *2017-08-21T22:49:39Z* + + - **ISSUE** `#15171`_: (*JensRantil*) Maximum recursion limit hit related to requisites + | refs: `#42985`_ + * e2bf2f4 Merge pull request `#42985`_ from DSRCorporation/bugs/15171_recursion_limit + * 651b1ba Properly handle `prereq` having lost requisites. + +- **PR** `#43092`_: (*blarghmatey*) Fixed issue with silently passing all tests in Testinfra module + @ *2017-08-21T20:22:08Z* + + * e513333 Merge pull request `#43092`_ from mitodl/2016.11 + * d4b113a Fixed issue with silently passing all tests in Testinfra module + +- **PR** `#43060`_: (*twangboy*) Osx update pkg scripts + @ *2017-08-21T20:06:12Z* + + * 77a443c Merge pull request `#43060`_ from twangboy/osx_update_pkg_scripts + * ef8a14c Remove /opt/salt instead of /opt/salt/bin + + * 2dd62aa Add more information to the description + + * f44f5b7 Only stop services if they are running + + * 3b62bf9 Remove salt from the path + + * ebdca3a Update pkg-scripts + +- **PR** `#43064`_: (*terminalmage*) Fix race condition in git.latest + @ *2017-08-21T14:29:52Z* + + - **ISSUE** `#42869`_: (*abednarik*) Git Module : Failed to update repository + | refs: `#43064`_ + * 1b1b6da Merge pull request `#43064`_ from terminalmage/issue42869 + * 093c0c2 Fix race condition in git.latest + +- **PR** `#43054`_: (*lorengordon*) Uses ConfigParser to read yum config files + @ *2017-08-18T20:49:44Z* + + - **ISSUE** `#42041`_: (*lorengordon*) pkg.list_repo_pkgs fails to find pkgs with spaces around yum repo enabled value + | refs: `#43054`_ + - **PR** `#42045`_: (*arount*) Fix: salt.modules.yumpkg: ConfigParser to read ini like files. + | refs: `#43054`_ + * 96e8e83 Merge pull request `#43054`_ from lorengordon/fix/yumpkg/config-parser + * 3b2cb81 fix typo in salt.modules.yumpkg + + * 38add0e break if leading comments are all fetched + + * d7f65dc fix configparser import & log if error was raised + + * ca1b1bb use configparser to parse yum repo file + +- **PR** `#43048`_: (*rallytime*) Back-port `#43031`_ to 2016.11 + @ *2017-08-18T12:56:04Z* + + - **PR** `#43031`_: (*gtmanfred*) use a ruby gem that doesn't have dependencies + | refs: `#43048`_ + * 43aa46f Merge pull request `#43048`_ from rallytime/`bp-43031`_ + * 35e4504 use a ruby gem that doesn't have dependencies + +- **PR** `#43023`_: (*terminalmage*) Fixes/improvements to Jenkins state/module + @ *2017-08-18T01:33:10Z* + + * ad89ff3 Merge pull request `#43023`_ from terminalmage/fix-jenkins-xml-caching + * 33fd8ff Update jenkins.py + + * fc306fc Add missing colon in `if` statement + + * 822eabc Catch exceptions raised when making changes to jenkins + + * 91b583b Improve and correct execption raising + + * f096917 Raise an exception if we fail to cache the config xml + +- **PR** `#43026`_: (*rallytime*) Back-port `#43020`_ to 2016.11 + @ *2017-08-17T23:19:46Z* + + - **PR** `#43020`_: (*gtmanfred*) test with gem that appears to be abandoned + | refs: `#43026`_ + * 2957467 Merge pull request `#43026`_ from rallytime/`bp-43020`_ + * 0eb15a1 test with gem that appears to be abandoned + +- **PR** `#43033`_: (*rallytime*) Back-port `#42760`_ to 2016.11 + @ *2017-08-17T22:24:43Z* + + - **ISSUE** `#40490`_: (*alxwr*) saltstack x509 incompatible to m2crypto 0.26.0 + | refs: `#42760`_ + - **PR** `#42760`_: (*AFriemann*) Catch TypeError thrown by m2crypto when parsing missing subjects in c… + | refs: `#43033`_ + * 4150b09 Merge pull request `#43033`_ from rallytime/`bp-42760`_ + * 3e3f7f5 Catch TypeError thrown by m2crypto when parsing missing subjects in certificate files. + +- **PR** `#43032`_: (*rallytime*) Back-port `#42547`_ to 2016.11 + @ *2017-08-17T21:53:50Z* + + - **PR** `#42547`_: (*blarghmatey*) Updated testinfra modules to work with more recent versions + | refs: `#43032`_ + * b124d36 Merge pull request `#43032`_ from rallytime/`bp-42547`_ + * ea4d7f4 Updated testinfra modules to work with more recent versions + +- **PR** `#43027`_: (*pabloh007*) Fixes ignore push flag for docker.push module issue `#42992`_ + @ *2017-08-17T19:55:37Z* + + - **ISSUE** `#42992`_: (*pabloh007*) docker.save flag push does is ignored + * a88386a Merge pull request `#43027`_ from pabloh007/fix-docker-save-push-2016-11 + * d0fd949 Fixes ignore push flag for docker.push module issue `#42992`_ + +- **PR** `#42890`_: (*DmitryKuzmenko*) Make chunked mode in salt-cp optional + @ *2017-08-17T18:37:44Z* + + - **ISSUE** `#42627`_: (*taigrrr8*) salt-cp no longer works. Was working a few months back. + | refs: `#42890`_ + * 51d1684 Merge pull request `#42890`_ from DSRCorporation/bugs/42627_salt-cp + * cfddbf1 Apply code review: update the doc + + * afedd3b Typos and version fixes in the doc. + + * 9fedf60 Fixed 'test_valid_docs' test. + + * 9993886 Make chunked mode in salt-cp optional (disabled by default). + +- **PR** `#43009`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + @ *2017-08-17T18:00:09Z* + + - **PR** `#42954`_: (*Ch3LL*) [2016.3] Bump latest and previous versions + - **PR** `#42949`_: (*Ch3LL*) Add Security Notice to 2016.3.7 Release Notes + - **PR** `#42942`_: (*Ch3LL*) [2016.3] Add clean_id function to salt.utils.verify.py + * b3c253c Merge pull request `#43009`_ from rallytime/merge-2016.11 + * 566ba4f Merge branch '2016.3' into '2016.11' + + * 13b8637 Merge pull request `#42942`_ from Ch3LL/2016.3.6_follow_up + + * f281e17 move additional minion config options to 2016.3.8 release notes + + * 168604b remove merge conflict + + * 8a07d95 update release notes with cve number + + * 149633f Add release notes for 2016.3.7 release + + * 7a4cddc Add clean_id function to salt.utils.verify.py + + * bbb1b29 Merge pull request `#42954`_ from Ch3LL/latest_2016.3 + + * b551e66 [2016.3] Bump latest and previous versions + + * 5d5edc5 Merge pull request `#42949`_ from Ch3LL/2016.3.7_docs + + * d75d374 Add Security Notice to 2016.3.7 Release Notes + +- **PR** `#43021`_: (*terminalmage*) Use socket.AF_INET6 to get the correct value instead of doing an OS check + @ *2017-08-17T17:57:09Z* + + - **PR** `#43014`_: (*Ch3LL*) Change AF_INET6 family for mac in test_host_to_ips + | refs: `#43021`_ + * 37c63e7 Merge pull request `#43021`_ from terminalmage/fix-network-test + * 4089b7b Use socket.AF_INET6 to get the correct value instead of doing an OS check + +- **PR** `#43019`_: (*rallytime*) Update bootstrap script to latest stable: v2017.08.17 + @ *2017-08-17T17:56:41Z* + + * 8f64232 Merge pull request `#43019`_ from rallytime/bootstrap_2017.08.17 + * 2f762b3 Update bootstrap script to latest stable: v2017.08.17 + +- **PR** `#43014`_: (*Ch3LL*) Change AF_INET6 family for mac in test_host_to_ips + | refs: `#43021`_ + @ *2017-08-17T16:17:51Z* + + * ff1caeee Merge pull request `#43014`_ from Ch3LL/fix_network_mac + * b8eee44 Change AF_INET6 family for mac in test_host_to_ips + +- **PR** `#42968`_: (*vutny*) [DOCS] Fix link to Salt Cloud Feature Matrix + @ *2017-08-16T13:16:16Z* + + * 1ee9499 Merge pull request `#42968`_ from vutny/doc-salt-cloud-ref + * 44ed53b [DOCS] Fix link to Salt Cloud Feature Matrix + +- **PR** `#42291`_: (*vutny*) Fix `#38839`_: remove `state` from Reactor runner kwags + @ *2017-08-15T23:01:08Z* + + - **ISSUE** `#38839`_: (*DaveOHenry*) Invoking runner.cloud.action via reactor sls fails + | refs: `#42291`_ + * 923f974 Merge pull request `#42291`_ from vutny/`fix-38839`_ + * 5f8f98a Fix `#38839`_: remove `state` from Reactor runner kwags + +- **PR** `#42940`_: (*gtmanfred*) create new ip address before checking list of allocated ips + @ *2017-08-15T21:47:18Z* + + - **ISSUE** `#42644`_: (*stamak*) nova salt-cloud -P Private IPs returned, but not public. Checking for misidentified IPs + | refs: `#42940`_ + * c20bc7d Merge pull request `#42940`_ from gtmanfred/2016.11 + * 253e216 fix IP address spelling + + * bd63074 create new ip address before checking list of allocated ips + +- **PR** `#42959`_: (*rallytime*) Back-port `#42883`_ to 2016.11 + @ *2017-08-15T21:25:48Z* + + - **PR** `#42883`_: (*rallytime*) Fix failing boto tests + | refs: `#42959`_ + * d6496ec Merge pull request `#42959`_ from rallytime/`bp-42883`_ + * c6b9ca4 Lint fix: add missing space + + * 5597b1a Skip 2 failing tests in Python 3 due to upstream bugs + + * a0b19bd Update account id value in boto_secgroup module unit test + + * 60b406e @mock_elb needs to be changed to @mock_elb_deprecated as well + + * 6ae1111 Replace @mock_ec2 calls with @mock_ec2_deprecated calls + +- **PR** `#42944`_: (*Ch3LL*) [2016.11] Add clean_id function to salt.utils.verify.py + @ *2017-08-15T18:06:12Z* + + * 6366e05 Merge pull request `#42944`_ from Ch3LL/2016.11.6_follow_up + * 7e0a20a Add release notes for 2016.11.7 release + + * 63823f8 Add clean_id function to salt.utils.verify.py + +- **PR** `#42952`_: (*Ch3LL*) [2016.11] Bump latest and previous versions + @ *2017-08-15T17:23:02Z* + + * 49d339c Merge pull request `#42952`_ from Ch3LL/latest_2016.11 + * 74e7055 [2016.11] Bump latest and previous versions + +- **PR** `#42950`_: (*Ch3LL*) Add Security Notice to 2016.11.7 Release Notes + @ *2017-08-15T16:50:23Z* + + * b0d2e05 Merge pull request `#42950`_ from Ch3LL/2016.11.7_docs + * a6f902d Add Security Notice to 2016.11.77 Release Notes + +- **PR** `#42836`_: (*aneeshusa*) Backport salt.utils.versions from develop to 2016.11 + @ *2017-08-14T20:56:54Z* + + - **PR** `#42835`_: (*aneeshusa*) Fix typo in utils/versions.py module + | refs: `#42836`_ + * c0ff69f Merge pull request `#42836`_ from lyft/backport-utils.versions-to-2016.11 + * 86ce700 Backport salt.utils.versions from develop to 2016.11 + +- **PR** `#42919`_: (*rallytime*) Back-port `#42871`_ to 2016.11 + @ *2017-08-14T20:44:00Z* + + - **PR** `#42871`_: (*amalleo25*) Update joyent.rst + | refs: `#42919`_ + * 64a79dd Merge pull request `#42919`_ from rallytime/`bp-42871`_ + * 4e46c96 Update joyent.rst + +- **PR** `#42918`_: (*rallytime*) Back-port `#42848`_ to 2016.11 + @ *2017-08-14T20:43:43Z* + + - **ISSUE** `#42803`_: (*gmcwhistler*) master_type: str, not working as expected, parent salt-minion process dies. + | refs: `#42848`_ + - **ISSUE** `#42753`_: (*grichmond-salt*) SaltReqTimeout Error on Some Minions when One Master in a Multi-Master Configuration is Unavailable + | refs: `#42848`_ + - **PR** `#42848`_: (*DmitryKuzmenko*) Execute fire_master asynchronously in the main minion thread. + | refs: `#42918`_ + * bea8ec1 Merge pull request `#42918`_ from rallytime/`bp-42848`_ + * cdb4812 Make lint happier. + + * 62eca9b Execute fire_master asynchronously in the main minion thread. + +- **PR** `#42861`_: (*twangboy*) Fix pkg.install salt-minion using salt-call + @ *2017-08-14T19:07:22Z* + + * 52bce32 Merge pull request `#42861`_ from twangboy/win_pkg_install_salt + * 0d3789f Fix pkg.install salt-minion using salt-call + +- **PR** `#42798`_: (*s-sebastian*) Update return data before calling returners + @ *2017-08-14T15:51:30Z* + + * b9f4f87 Merge pull request `#42798`_ from s-sebastian/2016.11 + * 1cc8659 Update return data before calling returners + +- **PR** `#41977`_: (*abulford*) Fix dockerng.network_* ignoring of tests=True + @ *2017-08-11T18:37:20Z* + + - **ISSUE** `#41976`_: (*abulford*) dockerng network states do not respect test=True + | refs: `#41977`_ `#41977`_ + * c15d003 Merge pull request `#41977`_ from redmatter/fix-dockerng-network-ignores-test + * 1cc2aa5 Fix dockerng.network_* ignoring of tests=True + +- **PR** `#42886`_: (*sarcasticadmin*) Adding missing output flags to salt cli docs + @ *2017-08-11T18:35:19Z* + + * 3b9c3c5 Merge pull request `#42886`_ from sarcasticadmin/adding_docs_salt_outputs + * 744bf95 Adding missing output flags to salt cli + +- **PR** `#42882`_: (*gtmanfred*) make sure cmd is not run when npm isn't installed + @ *2017-08-11T17:53:14Z* + + * e5b98c8 Merge pull request `#42882`_ from gtmanfred/2016.11 + * da3402a make sure cmd is not run when npm isn't installed + +- **PR** `#42788`_: (*amendlik*) Remove waits and retries from Saltify deployment + @ *2017-08-11T15:38:05Z* + + * 5962c95 Merge pull request `#42788`_ from amendlik/saltify-timeout + * 928b523 Remove waits and retries from Saltify deployment + +- **PR** `#42877`_: (*terminalmage*) Add virtual func for cron state module + @ *2017-08-11T15:33:09Z* + + * 227ecdd Merge pull request `#42877`_ from terminalmage/add-cron-state-virtual + * f1de196 Add virtual func for cron state module + +- **PR** `#42859`_: (*terminalmage*) Add note about git CLI requirement for GitPython to GitFS tutorial + @ *2017-08-11T14:53:03Z* + + * ab9f6ce Merge pull request `#42859`_ from terminalmage/gitpython-git-cli-note + * 35e05c9 Add note about git CLI requirement for GitPython to GitFS tutorial + +- **PR** `#42856`_: (*gtmanfred*) skip cache_clean test if npm version is >= 5.0.0 + @ *2017-08-11T13:39:20Z* + + - **ISSUE** `#41770`_: (*Ch3LL*) NPM v5 incompatible with salt.modules.cache_list + | refs: `#42856`_ + - **ISSUE** `#475`_: (*thatch45*) Change yaml to use C bindings + | refs: `#42856`_ + * 682b4a8 Merge pull request `#42856`_ from gtmanfred/2016.11 + * b458b89 skip cache_clean test if npm version is >= 5.0.0 + +- **PR** `#42864`_: (*whiteinge*) Make syndic_log_file respect root_dir setting + @ *2017-08-11T13:28:21Z* + + * 01ea854 Merge pull request `#42864`_ from whiteinge/syndic-log-root_dir + * 4b1f55d Make syndic_log_file respect root_dir setting + +- **PR** `#42851`_: (*terminalmage*) Backport `#42651`_ to 2016.11 + @ *2017-08-10T18:02:39Z* + + - **PR** `#42651`_: (*gtmanfred*) python2- prefix for fedora 26 packages + * 2dde1f7 Merge pull request `#42851`_ from terminalmage/`bp-42651`_ + * a3da86e fix syntax + + * 6ecdbce make sure names are correct + + * f83b553 add py3 for versionlock + + * 21934f6 python2- prefix for fedora 26 packages + +- **PR** `#42806`_: (*rallytime*) Update doc references in glusterfs.volume_present + @ *2017-08-10T14:10:16Z* + + - **ISSUE** `#42683`_: (*rgcosma*) Gluster module broken in 2017.7 + | refs: `#42806`_ + * c746f79 Merge pull request `#42806`_ from rallytime/`fix-42683`_ + * 8c8640d Update doc references in glusterfs.volume_present + +- **PR** `#42829`_: (*twangboy*) Fix passing version in pkgs as shown in docs + @ *2017-08-10T14:07:24Z* + + * 27a8a26 Merge pull request `#42829`_ from twangboy/win_pkg_fix_install + * 83b9b23 Add winrepo to docs about supporting versions in pkgs + + * 81fefa6 Add ability to pass version in pkgs list + +- **PR** `#42838`_: (*twangboy*) Document requirements for win_pki + @ *2017-08-10T13:59:46Z* + + * 3c3ac6a Merge pull request `#42838`_ from twangboy/win_doc_pki + * f0a1d06 Standardize PKI Client + + * 7de687a Document requirements for win_pki + +- **PR** `#42805`_: (*rallytime*) Back-port `#42552`_ to 2016.11 + @ *2017-08-09T22:37:56Z* + + - **PR** `#42552`_: (*remijouannet*) update consul module following this documentation https://www.consul.… + | refs: `#42805`_ + * b3e2ae3 Merge pull request `#42805`_ from rallytime/`bp-42552`_ + * 5a91c1f update consul module following this documentation https://www.consul.io/api/acl.html + +- **PR** `#42804`_: (*rallytime*) Back-port `#42784`_ to 2016.11 + @ *2017-08-09T22:37:40Z* + + - **ISSUE** `#42731`_: (*infoveinx*) http.query template_data render exception + | refs: `#42804`_ + - **PR** `#42784`_: (*gtmanfred*) only read file if ret is not a string in http.query + | refs: `#42804`_ + * d2ee793 Merge pull request `#42804`_ from rallytime/`bp-42784`_ + * dbd29e4 only read file if it is not a string + +- **PR** `#42826`_: (*terminalmage*) Fix misspelling of "versions" + @ *2017-08-09T19:39:43Z* + + * 4cbf805 Merge pull request `#42826`_ from terminalmage/fix-spelling + * 00f9314 Fix misspelling of "versions" + +- **PR** `#42786`_: (*Ch3LL*) Fix typo for template_dict in http docs + @ *2017-08-08T18:14:50Z* + + * de997ed Merge pull request `#42786`_ from Ch3LL/fix_typo + * 90a2fb6 Fix typo for template_dict in http docs + +- **PR** `#42795`_: (*lomeroe*) backport `#42744`_ to 2016.11 + @ *2017-08-08T17:17:15Z* + + - **ISSUE** `#42600`_: (*twangboy*) Unable to set 'Not Configured' using win_lgpo execution module + | refs: `#42744`_ `#42795`_ + - **PR** `#42744`_: (*lomeroe*) fix `#42600`_ in develop + | refs: `#42795`_ + * bf6153e Merge pull request `#42795`_ from lomeroe/`bp-42744`__201611 + * 695f8c1 fix `#42600`_ in develop + +- **PR** `#42748`_: (*whiteinge*) Workaround Orchestrate problem that highstate outputter mutates data + @ *2017-08-07T21:11:33Z* + + - **ISSUE** `#42747`_: (*whiteinge*) Outputters mutate data which can be a problem for Runners and perhaps other things + | refs: `#42748`_ + * 61fad97 Merge pull request `#42748`_ from whiteinge/save-before-output + * de60b77 Workaround Orchestrate problem that highstate outputter mutates data + +- **PR** `#42764`_: (*amendlik*) Fix infinite loop with salt-cloud and Windows nodes + @ *2017-08-07T20:47:07Z* + + * a4e3e7e Merge pull request `#42764`_ from amendlik/cloud-win-loop + * f3dcfca Fix infinite loops on failed Windows deployments + +- **PR** `#42694`_: (*gtmanfred*) allow adding extra remotes to a repository + @ *2017-08-07T18:08:11Z* + + - **ISSUE** `#42690`_: (*ChristianBeer*) git.latest state with remote set fails on first try + | refs: `#42694`_ + * da85326 Merge pull request `#42694`_ from gtmanfred/2016.11 + * 1a0457a allow adding extra remotes to a repository + +- **PR** `#42669`_: (*garethgreenaway*) [2016.11] Fixes to augeas module + @ *2017-08-06T17:58:03Z* + + - **ISSUE** `#42642`_: (*githubcdr*) state.augeas + | refs: `#42669`_ `#43202`_ + * 7b2119f Merge pull request `#42669`_ from garethgreenaway/42642_2016_11_augeas_module_fix + * 2441308 Updating the call to shlex_split to pass the posix=False argument so that quotes are preserved. + +- **PR** `#42629`_: (*xiaoanyunfei*) tornado api + @ *2017-08-03T22:21:20Z* + + * 3072576 Merge pull request `#42629`_ from xiaoanyunfei/tornadoapi + * 1e13383 tornado api + +- **PR** `#42655`_: (*whiteinge*) Reenable cpstats for rest_cherrypy + @ *2017-08-03T20:44:10Z* + + - **PR** `#33806`_: (*cachedout*) Work around upstream cherrypy bug + | refs: `#42655`_ + * f0f00fc Merge pull request `#42655`_ from whiteinge/rest_cherrypy-reenable-stats + * deb6316 Fix lint errors + + * 6bd91c8 Reenable cpstats for rest_cherrypy + +- **PR** `#42693`_: (*gilbsgilbs*) Fix RabbitMQ tags not properly set. + @ *2017-08-03T20:23:08Z* + + - **ISSUE** `#42686`_: (*gilbsgilbs*) Unable to set multiple RabbitMQ tags + | refs: `#42693`_ `#42693`_ + * 21cf15f Merge pull request `#42693`_ from gilbsgilbs/fix-rabbitmq-tags + * 78fccdc Cast to list in case tags is a tuple. + + * 287b57b Fix RabbitMQ tags not properly set. + +- **PR** `#42574`_: (*sbojarski*) Fixed error reporting in "boto_cfn.present" function. + @ *2017-08-01T17:55:29Z* + + - **ISSUE** `#41433`_: (*sbojarski*) boto_cfn.present fails when reporting error for failed state + | refs: `#42574`_ + * f2b0c9b Merge pull request `#42574`_ from sbojarski/boto-cfn-error-reporting + * 5c945f1 Fix debug message in "boto_cfn._validate" function. + + * 181a1be Fixed error reporting in "boto_cfn.present" function. + +- **PR** `#42623`_: (*terminalmage*) Fix unicode constructor in custom YAML loader + @ *2017-07-31T19:25:18Z* + + * bc1effc Merge pull request `#42623`_ from terminalmage/fix-unicode-constructor + * fcf4588 Fix unicode constructor in custom YAML loader + +- **PR** `#42515`_: (*gtmanfred*) Allow not interpreting backslashes in the repl + @ *2017-07-28T16:00:09Z* + + * cbf752c Merge pull request `#42515`_ from gtmanfred/backslash + * cc4e456 Allow not interpreting backslashes in the repl + +- **PR** `#42586`_: (*gdubroeucq*) [Fix] yumpkg.py: add option to the command "check-update" + @ *2017-07-27T23:52:00Z* + + - **ISSUE** `#42456`_: (*gdubroeucq*) Use yum lib + | refs: `#42586`_ + * 5494958 Merge pull request `#42586`_ from gdubroeucq/2016.11 + * 9c0b5cc Remove extra newline + + * d2ef448 yumpkg.py: clean + + * a96f7c0 yumpkg.py: add option to the command "check-update" + +- **PR** `#41988`_: (*abulford*) Fix dockerng.network_* name matching + @ *2017-07-27T21:25:06Z* + + - **ISSUE** `#41982`_: (*abulford*) dockerng.network_* matches too easily + | refs: `#41988`_ `#41988`_ + * 6b45deb Merge pull request `#41988`_ from redmatter/fix-dockerng-network-matching + * 9eea796 Add regression tests for `#41982`_ + + * 3369f00 Fix broken unit test test_network_absent + + * 0ef6cf6 Add trace logging of dockerng.networks result + + * 515c612 Fix dockerng.network_* name matching + +- **PR** `#42339`_: (*isbm*) Bugfix: Jobs scheduled to run at a future time stay pending for Salt minions (bsc`#1036125`_) + @ *2017-07-27T19:05:51Z* + + - **ISSUE** `#1036125`_: (**) + * 4b16109 Merge pull request `#42339`_ from isbm/isbm-jobs-scheduled-in-a-future-bsc1036125 + * bbba84c Bugfix: Jobs scheduled to run at a future time stay pending for Salt minions (bsc`#1036125`_) + +- **PR** `#42077`_: (*vutny*) Fix scheduled job run on Master if `when` parameter is a list + @ *2017-07-27T19:04:23Z* + + - **ISSUE** `#23516`_: (*dkiser*) BUG: cron job scheduler sporadically works + | refs: `#42077`_ + - **PR** `#41973`_: (*vutny*) Fix Master/Minion scheduled jobs based on Cron expressions + | refs: `#42077`_ + * 6c5a7c6 Merge pull request `#42077`_ from vutny/fix-jobs-scheduled-with-whens + * b1960ce Fix scheduled job run on Master if `when` parameter is a list + +- **PR** `#42414`_: (*vutny*) DOCS: unify hash sum with hash type format + @ *2017-07-27T18:48:40Z* + + * f9cb536 Merge pull request `#42414`_ from vutny/unify-hash-params-format + * d1f2a93 DOCS: unify hash sum with hash type format + +- **PR** `#42523`_: (*rallytime*) Add a mention of the True/False returns with __virtual__() + @ *2017-07-27T18:13:07Z* + + - **ISSUE** `#42375`_: (*dragonpaw*) salt.modules.*.__virtualname__ doens't work as documented. + | refs: `#42523`_ + * 535c922 Merge pull request `#42523`_ from rallytime/`fix-42375`_ + * 685c2cc Add information about returning a tuple with an error message + + * fa46651 Add a mention of the True/False returns with __virtual__() + +- **PR** `#42527`_: (*twangboy*) Document changes to Windows Update in Windows 10/Server 2016 + @ *2017-07-27T17:45:38Z* + + * 0df0e7e Merge pull request `#42527`_ from twangboy/win_wua + * 0373791 Correct capatlization + + * af3bcc9 Document changes to Windows Update in 10/2016 + +- **PR** `#42551`_: (*binocvlar*) Remove '-s' (--script) argument to parted within align_check function + @ *2017-07-27T17:35:31Z* + + * 69b0658 Merge pull request `#42551`_ from binocvlar/fix-lack-of-align-check-output + * c4fabaa Remove '-s' (--script) argument to parted within align_check function + +- **PR** `#42573`_: (*rallytime*) Back-port `#42433`_ to 2016.11 + @ *2017-07-27T13:51:21Z* + + - **ISSUE** `#42403`_: (*astronouth7303*) [2017.7] Pillar empty when state is applied from orchestrate + | refs: `#42433`_ + - **PR** `#42433`_: (*terminalmage*) Only force saltenv/pillarenv to be a string when not None + | refs: `#42573`_ + * 9e0b4e9 Merge pull request `#42573`_ from rallytime/`bp-42433`_ + * 0293429 Only force saltenv/pillarenv to be a string when not None + +- **PR** `#42571`_: (*twangboy*) Avoid loading system PYTHON* environment vars + @ *2017-07-26T22:48:55Z* + + * e931ed2 Merge pull request `#42571`_ from twangboy/win_add_pythonpath + * d55a44d Avoid loading user site packages + + * 9af1eb2 Ignore any PYTHON* environment vars already on the system + + * 4e2fb03 Add pythonpath to batch files and service + +- **PR** `#42387`_: (*DmitryKuzmenko*) Fix race condition in usage of weakvaluedict + @ *2017-07-25T20:57:42Z* + + - **ISSUE** `#42371`_: (*tsaridas*) Minion unresponsive after trying to failover + | refs: `#42387`_ + * de2f397 Merge pull request `#42387`_ from DSRCorporation/bugs/42371_KeyError_WeakValueDict + * e721c7e Don't use `key in weakvaluedict` because it could lie. + +- **PR** `#41968`_: (*root360-AndreasUlm*) Fix rabbitmqctl output sanitizer for version 3.6.10 + @ *2017-07-25T19:12:36Z* + + - **ISSUE** `#41955`_: (*root360-AndreasUlm*) rabbitmq 3.6.10 changed output => rabbitmq-module broken + | refs: `#41968`_ + * 641a9d7 Merge pull request `#41968`_ from root360-AndreasUlm/fix-rabbitmqctl-output-handler + * 76fd941 added tests for rabbitmq 3.6.10 output handler + + * 3602af1 Fix rabbitmqctl output handler for 3.6.10 + +- **PR** `#42479`_: (*gtmanfred*) validate ssh_interface for ec2 + @ *2017-07-25T18:37:18Z* + + - **ISSUE** `#42477`_: (*aikar*) Invalid ssh_interface value prevents salt-cloud provisioning without reason of why + | refs: `#42479`_ + * 66fede3 Merge pull request `#42479`_ from gtmanfred/interface + * c32c1b2 fix pylint + + * 99ec634 validate ssh_interface for ec2 + +- **PR** `#42516`_: (*rallytime*) Add info about top file to pillar walk-through example to include edit.vim + @ *2017-07-25T17:01:12Z* + + - **ISSUE** `#42405`_: (*felrivero*) The documentation is incorrectly compiled (PILLAR section) + | refs: `#42516`_ + * a925c70 Merge pull request `#42516`_ from rallytime/`fix-42405`_ + * e3a6717 Add info about top file to pillar walk-through example to include edit.vim + +- **PR** `#42509`_: (*clem-compilatio*) Fix _assign_floating_ips in openstack.py + @ *2017-07-24T17:14:13Z* + + - **ISSUE** `#42417`_: (*clem-compilatio*) salt-cloud - openstack - "no more floating IP addresses" error - but public_ip in node + | refs: `#42509`_ + * 1bd5bbc Merge pull request `#42509`_ from clem-compilatio/`fix-42417`_ + * 72924b0 Fix _assign_floating_ips in openstack.py + +- **PR** `#42464`_: (*garethgreenaway*) [2016.11] Small fix to modules/git.py + @ *2017-07-21T21:28:57Z* + + * 4bf35a7 Merge pull request `#42464`_ from garethgreenaway/2016_11_remove_tmp_identity_file + * ff24102 Uncomment the line that removes the temporary identity file. + +- **PR** `#42443`_: (*garethgreenaway*) [2016.11] Fix to slack engine + @ *2017-07-21T15:48:57Z* + + - **ISSUE** `#42357`_: (*Giandom*) Salt pillarenv problem with slack engine + | refs: `#42443`_ + * e2120db Merge pull request `#42443`_ from garethgreenaway/42357_pass_args_kwargs_correctly + * 635810b Updating the slack engine in 2016.11 to pass the args and kwrags correctly to LocalClient + +- **PR** `#42200`_: (*shengis*) Fix `#42198`_ + @ *2017-07-21T14:47:29Z* + + - **ISSUE** `#42198`_: (*shengis*) state sqlite3.row_absent fail with "parameters are of unsupported type" + | refs: `#42200`_ + * 8262cc9 Merge pull request `#42200`_ from shengis/sqlite3_fix_row_absent_2016.11 + * 407b8f4 Fix `#42198`_ If where_args is not set, not using it in the delete request. + +- **PR** `#42424`_: (*goten4*) Fix error message when tornado or pycurl is not installed + @ *2017-07-20T21:53:40Z* + + - **ISSUE** `#42413`_: (*goten4*) Invalid error message when proxy_host is set and tornado not installed + | refs: `#42424`_ + * d9df97e Merge pull request `#42424`_ from goten4/2016.11 + * 1c0574d Fix error message when tornado or pycurl is not installed + +- **PR** `#42350`_: (*twangboy*) Fixes problem with Version and OS Release related grains on certain versions of Python (2016.11) + @ *2017-07-19T17:07:26Z* + + * 42bb1a6 Merge pull request `#42350`_ from twangboy/win_fix_ver_grains_2016.11 + * 8c04840 Detect Server OS with a desktop release name + +- **PR** `#42356`_: (*meaksh*) Allow to check whether a function is available on the AliasesLoader wrapper + @ *2017-07-19T16:56:41Z* + + * 0a72e56 Merge pull request `#42356`_ from meaksh/2016.11-AliasesLoader-wrapper-fix + * 915d942 Allow to check whether a function is available on the AliasesLoader wrapper + +- **PR** `#42368`_: (*twangboy*) Remove build and dist directories before install (2016.11) + @ *2017-07-19T16:47:28Z* + + * 10eb7b7 Merge pull request `#42368`_ from twangboy/win_fix_build_2016.11 + * a7c910c Remove build and dist directories before install + +- **PR** `#42370`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + @ *2017-07-18T22:39:41Z* + + - **PR** `#42359`_: (*Ch3LL*) [2016.3] Update version numbers in doc config for 2017.7.0 release + * 016189f Merge pull request `#42370`_ from rallytime/merge-2016.11 + * 0aa5dde Merge branch '2016.3' into '2016.11' + + * e9b0f20 Merge pull request `#42359`_ from Ch3LL/doc-update-2016.3 + + * dc85b5e [2016.3] Update version numbers in doc config for 2017.7.0 release + +- **PR** `#42360`_: (*Ch3LL*) [2016.11] Update version numbers in doc config for 2017.7.0 release + @ *2017-07-18T19:23:30Z* + + * f06a6f1 Merge pull request `#42360`_ from Ch3LL/doc-update-2016.11 + * b90b7a7 [2016.11] Update version numbers in doc config for 2017.7.0 release + +- **PR** `#42319`_: (*rallytime*) Add more documentation for config options that are missing from master/minion docs + @ *2017-07-18T18:02:32Z* + + - **ISSUE** `#32400`_: (*rallytime*) Document Default Config Values + | refs: `#42319`_ + * e0595b0 Merge pull request `#42319`_ from rallytime/config-docs + * b40f980 Add more documentation for config options that are missing from master/minion docs + +- **PR** `#42352`_: (*CorvinM*) Multiple documentation fixes + @ *2017-07-18T15:10:37Z* + + - **ISSUE** `#42333`_: (*b3hni4*) Getting "invalid type of dict, a list is required" when trying to configure engines in master config file + | refs: `#42352`_ + * 7894040 Merge pull request `#42352`_ from CorvinM/issue42333 + * 526b6ee Multiple documentation fixes + +- **PR** `#42353`_: (*terminalmage*) is_windows is a function, not a propery/attribute + @ *2017-07-18T14:38:51Z* + + * b256001 Merge pull request `#42353`_ from terminalmage/fix-git-test + * 14cf6ce is_windows is a function, not a propery/attribute + +- **PR** `#42264`_: (*rallytime*) Update minion restart section in FAQ doc for windows + @ *2017-07-17T17:40:40Z* + + - **ISSUE** `#41116`_: (*hrumph*) FAQ has wrong instructions for upgrading Windows minion. + | refs: `#42264`_ + * 866a1fe Merge pull request `#42264`_ from rallytime/`fix-41116`_ + * bd63888 Add mono-spacing to salt-minion reference for consistency + + * 30d62f4 Update minion restart section in FAQ doc for windows + +- **PR** `#42275`_: (*terminalmage*) pkg.installed: pack name/version into pkgs argument + @ *2017-07-17T17:38:39Z* + + - **ISSUE** `#42194`_: (*jryberg*) pkg version: latest are now broken, appending -latest to filename + | refs: `#42275`_ + * 9a70708 Merge pull request `#42275`_ from terminalmage/issue42194 + * 6638749 pkg.installed: pack name/version into pkgs argument + +- **PR** `#42269`_: (*rallytime*) Add some clarity to "multiple quotes" section of yaml docs + @ *2017-07-17T17:38:18Z* + + - **ISSUE** `#41721`_: (*sazaro*) state.sysrc broken when setting the value to YES or NO + | refs: `#42269`_ + * e588f23 Merge pull request `#42269`_ from rallytime/`fix-41721`_ + * f2250d4 Add a note about using different styles of quotes. + + * 38d9b3d Add some clarity to "multiple quotes" section of yaml docs + +- **PR** `#42282`_: (*rallytime*) Handle libcloud objects that throw RepresenterErrors with --out=yaml + @ *2017-07-17T17:36:35Z* + + - **ISSUE** `#42152`_: (*dubb-b*) salt-cloud errors on Rackspace driver using -out=yaml + | refs: `#42282`_ + * 5aaa214 Merge pull request `#42282`_ from rallytime/`fix-42152`_ + * f032223 Handle libcloud objects that throw RepresenterErrors with --out=yaml + +- **PR** `#42308`_: (*lubyou*) Force file removal on Windows. Fixes `#42295`_ + @ *2017-07-17T17:12:13Z* + + - **ISSUE** `#42295`_: (*lubyou*) file.absent fails on windows if the file to be removed has the "readonly" attribute set + | refs: `#42308`_ + * fb5697a Merge pull request `#42308`_ from lubyou/42295-fix-file-absent-windows + * 026ccf4 Force file removal on Windows. Fixes `#42295`_ + +- **PR** `#42314`_: (*rallytime*) Add clarification to salt ssh docs about key auto-generation. + @ *2017-07-17T14:07:49Z* + + - **ISSUE** `#42267`_: (*gzcwnk*) salt-ssh not creating ssh keys automatically as per documentation + | refs: `#42314`_ + * da2a8a5 Merge pull request `#42314`_ from rallytime/`fix-42267`_ + * c406046 Add clarification to salt ssh docs about key auto-generation. + +- **PR** `#41945`_: (*garethgreenaway*) Fixes to modules/git.py + @ *2017-07-14T17:46:10Z* + + - **ISSUE** `#41936`_: (*michaelkarrer81*) git.latest identity does not set the correct user for the private key file on the minion + | refs: `#41945`_ + - **ISSUE** `#1`_: (*thatch45*) Enable regex on the salt cli + * acadd54 Merge pull request `#41945`_ from garethgreenaway/41936_allow_identity_files_with_user + * 44841e5 Moving the call to cp.get_file inside the with block to ensure the umask is preserved when we grab the file. + + * f9ba60e Merge pull request `#1`_ from terminalmage/pr-41945 + + * 1b60261 Restrict set_umask to mkstemp call only + + * 68549f3 Fixing umask to we can set files as executable. + + * 4949bf3 Updating to swap on the new salt.utils.files.set_umask context_manager + + * 8faa9f6 Updating PR with requested changes. + + * 494765e Updating the git module to allow an identity file to be used when passing the user parameter + +- **PR** `#42289`_: (*CorvinM*) Multiple empty_password fixes for state.user + @ *2017-07-14T16:14:02Z* + + - **ISSUE** `#42240`_: (*casselt*) empty_password in user.present always changes password, even with test=True + | refs: `#42289`_ + - **PR** `#41543`_: (*cri-epita*) Fix user creation with empty password + | refs: `#42289`_ `#42289`_ + * f90e04a Merge pull request `#42289`_ from CorvinM/`bp-41543`_ + * 357dc22 Fix user creation with empty password + +- **PR** `#42123`_: (*vutny*) DOCS: describe importing custom util classes + @ *2017-07-12T15:53:24Z* + + * a91a3f8 Merge pull request `#42123`_ from vutny/fix-master-utils-import + * 6bb8b8f Add missing doc for ``utils_dirs`` Minion config option + + * f1bc58f Utils: add example of module import + +- **PR** `#42261`_: (*rallytime*) Some minor doc fixes for dnsutil module so they'll render correctly + @ *2017-07-11T23:14:53Z* + + * e2aa511 Merge pull request `#42261`_ from rallytime/minor-doc-fix + * 8c76bbb Some minor doc fixes for dnsutil module so they'll render correctly + +- **PR** `#42262`_: (*rallytime*) Back-port `#42224`_ to 2016.11 + @ *2017-07-11T23:14:25Z* + + - **PR** `#42224`_: (*tdutrion*) Remove duplicate instruction in Openstack Rackspace config example + | refs: `#42262`_ + * 3e9dfbc Merge pull request `#42262`_ from rallytime/`bp-42224`_ + * c31ded3 Remove duplicate instruction in Openstack Rackspace config example + +- **PR** `#42181`_: (*garethgreenaway*) fixes to state.py for names parameter + @ *2017-07-11T21:21:32Z* + + - **ISSUE** `#42137`_: (*kiemlicz*) cmd.run with multiple commands - random order of execution + | refs: `#42181`_ + * 7780579 Merge pull request `#42181`_ from garethgreenaway/42137_backport_fix_from_2017_7 + * a34970b Back porting the fix for 2017.7 that ensures the order of the names parameter. + +- **PR** `#42253`_: (*gtmanfred*) Only use unassociated ips when unable to allocate + @ *2017-07-11T20:53:51Z* + + - **PR** `#38965`_: (*toanju*) salt-cloud will use list_floating_ips for OpenStack + | refs: `#42253`_ + - **PR** `#34280`_: (*kevinanderson1*) salt-cloud will use list_floating_ips for Openstack + | refs: `#38965`_ + * 7253786 Merge pull request `#42253`_ from gtmanfred/2016.11 + * 53e2576 Only use unassociated ips when unable to allocate + +- **PR** `#42252`_: (*UtahDave*) simple docstring updates + @ *2017-07-11T20:48:33Z* + + * b2a4698 Merge pull request `#42252`_ from UtahDave/2016.11local + * e6a9563 simple doc updates + +- **PR** `#42235`_: (*astronouth7303*) Abolish references to `dig` in examples. + @ *2017-07-10T20:06:11Z* + + - **ISSUE** `#42232`_: (*astronouth7303*) Half of dnsutil refers to dig + | refs: `#42235`_ + * 781fe13 Merge pull request `#42235`_ from astronouth7303/patch-1-2016.3 + * 4cb51bd Make note of dig partial requirement. + + * 08e7d83 Abolish references to `dig` in examples. + +- **PR** `#42215`_: (*twangboy*) Add missing config to example + @ *2017-07-07T20:18:44Z* + + * 83cbd76 Merge pull request `#42215`_ from twangboy/win_iis_docs + * c07e220 Add missing config to example + +- **PR** `#42211`_: (*terminalmage*) Only pass a saltenv in orchestration if one was explicitly passed (2016.11) + @ *2017-07-07T20:16:35Z* + + * 274946a Merge pull request `#42211`_ from terminalmage/issue40928 + * 22a18fa Only pass a saltenv in orchestration if one was explicitly passed (2016.11) + +- **PR** `#42173`_: (*rallytime*) Back-port `#37424`_ to 2016.11 + @ *2017-07-07T16:39:59Z* + + - **PR** `#37424`_: (*kojiromike*) Avoid Early Convert ret['comment'] to String + | refs: `#42173`_ + * 89261cf Merge pull request `#42173`_ from rallytime/`bp-37424`_ + * 01addb6 Avoid Early Convert ret['comment'] to String + +- **PR** `#42175`_: (*rallytime*) Back-port `#39366`_ to 2016.11 + @ *2017-07-06T19:51:47Z* + + - **ISSUE** `#39365`_: (*dglloyd*) service.running fails if sysv script has no status command and enable: True + | refs: `#39366`_ + - **PR** `#39366`_: (*dglloyd*) Pass sig to service.status in after_toggle + | refs: `#42175`_ + * 3b17fb7 Merge pull request `#42175`_ from rallytime/`bp-39366`_ + * 53f7b98 Pass sig to service.status in after_toggle + +- **PR** `#42172`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + @ *2017-07-06T18:16:29Z* + + - **PR** `#42155`_: (*phsteve*) Fix docs for puppet.plugin_sync + * ea16f47 Merge pull request `#42172`_ from rallytime/merge-2016.11 + * b1fa332 Merge branch '2016.3' into '2016.11' + + * 8fa1fa5 Merge pull request `#42155`_ from phsteve/doc-fix-puppet + + * fb2cb78 Fix docs for puppet.plugin_sync so code-block renders properly and sync is spelled consistently + +- **PR** `#42176`_: (*rallytime*) Back-port `#42109`_ to 2016.11 + @ *2017-07-06T18:15:35Z* + + - **PR** `#42109`_: (*arthurlogilab*) [doc] Update aws.rst - add Debian default username + | refs: `#42176`_ + * 6307b98 Merge pull request `#42176`_ from rallytime/`bp-42109`_ + * 686926d Update aws.rst - add Debian default username + +- **PR** `#42095`_: (*terminalmage*) Add debug logging to dockerng.login + @ *2017-07-06T17:13:05Z* + + * 28c4e4c Merge pull request `#42095`_ from terminalmage/docker-login-debugging + * bd27870 Add debug logging to dockerng.login + +- **PR** `#42119`_: (*terminalmage*) Fix regression in CLI pillar override for salt-call + @ *2017-07-06T17:02:52Z* + + - **ISSUE** `#42116`_: (*terminalmage*) CLI pillar override regression in 2017.7.0rc1 + | refs: `#42119`_ + * 2b754bc Merge pull request `#42119`_ from terminalmage/issue42116 + * 9a26894 Add integration test for 42116 + + * 1bb42bb Fix regression when CLI pillar override is used with salt-call + +- **PR** `#42121`_: (*terminalmage*) Fix pillar.get when saltenv is passed + @ *2017-07-06T16:52:34Z* + + - **ISSUE** `#42114`_: (*clallen*) saltenv bug in pillar.get execution module function + | refs: `#42121`_ + * 8c0a83c Merge pull request `#42121`_ from terminalmage/issue42114 + * d142912 Fix pillar.get when saltenv is passed + +- **PR** `#42094`_: (*terminalmage*) Prevent command from showing in exception when output_loglevel=quiet + @ *2017-07-06T16:18:09Z* + + * 687992c Merge pull request `#42094`_ from terminalmage/quiet-exception + * 47d61f4 Prevent command from showing in exception when output_loglevel=quiet + +- **PR** `#42163`_: (*vutny*) Fix `#42115`_: parse libcloud "rc" version correctly + @ *2017-07-06T16:15:07Z* + + - **ISSUE** `#42115`_: (*nomeelnoj*) Installing EPEL repo breaks salt-cloud + | refs: `#42163`_ + * dad2551 Merge pull request `#42163`_ from vutny/`fix-42115`_ + * b27b1e3 Fix `#42115`_: parse libcloud "rc" version correctly + +- **PR** `#42164`_: (*Ch3LL*) Fix kerberos create_keytab doc + @ *2017-07-06T15:55:33Z* + + * 2a8ae2b Merge pull request `#42164`_ from Ch3LL/fix_kerb_doc + * 7c0fb24 Fix kerberos create_keytab doc + +- **PR** `#42141`_: (*rallytime*) Back-port `#42098`_ to 2016.11 + @ *2017-07-06T15:11:49Z* + + - **PR** `#42098`_: (*twangboy*) Change repo_ng to repo-ng + | refs: `#42141`_ + * 678d4d4 Merge pull request `#42141`_ from rallytime/`bp-42098`_ + * bd80243 Change repo_ng to repo-ng + +- **PR** `#42140`_: (*rallytime*) Back-port `#42097`_ to 2016.11 + @ *2017-07-06T15:11:29Z* + + - **PR** `#42097`_: (*gtmanfred*) require large timediff for ipv6 warning + | refs: `#42140`_ + * c8afd7a Merge pull request `#42140`_ from rallytime/`bp-42097`_ + * 9c4e132 Import datetime + + * 1435bf1 require large timediff for ipv6 warning + +- **PR** `#42142`_: (*Ch3LL*) Update builds available for rc1 + @ *2017-07-05T21:11:56Z* + + * c239664 Merge pull request `#42142`_ from Ch3LL/change_builds + * e1694af Update builds available for rc1 + +- **PR** `#42078`_: (*damon-atkins*) pkg.install and pkg.remove fix version number input. + @ *2017-07-05T06:04:57Z* + + * 4780d78 Merge pull request `#42078`_ from damon-atkins/fix_convert_flt_str_version_on_cmd_line + * 09d37dd Fix comment typo + + * 7167549 Handle version=None when converted to a string it becomes 'None' parm should default to empty string rather than None, it would fix better with existing code. + + * 4fb2bb1 Fix typo + + * cf55c33 pkg.install and pkg.remove on the command line take number version numbers, store them within a float. However version is a string, to support versions numbers like 1.3.4 + +- **PR** `#42105`_: (*Ch3LL*) Update releasecanddiate doc with new 2017.7.0rc1 Release + @ *2017-07-04T03:14:42Z* + + * 46d575a Merge pull request `#42105`_ from Ch3LL/update_rc + * d4e7b91 Update releasecanddiate doc with new 2017.7.0rc1 Release + +- **PR** `#42099`_: (*rallytime*) Remove references in docs to pip install salt-cloud + @ *2017-07-03T22:13:44Z* + + - **ISSUE** `#41885`_: (*astronouth7303*) Recommended pip installation outdated? + | refs: `#42099`_ + * d38548b Merge pull request `#42099`_ from rallytime/`fix-41885`_ + * c2822e0 Remove references in docs to pip install salt-cloud + +- **PR** `#42086`_: (*abulford*) Make result=true if Docker volume already exists + @ *2017-07-03T15:48:33Z* + + - **ISSUE** `#42076`_: (*abulford*) dockerng.volume_present test looks as though it would cause a change + | refs: `#42086`_ `#42086`_ + * 81d606a Merge pull request `#42086`_ from redmatter/fix-dockerng-volume-present-result + * 8d54968 Make result=true if Docker volume already exists + +- **PR** `#42021`_: (*gtmanfred*) Set concurrent to True when running states with sudo + @ *2017-06-30T21:02:15Z* + + - **ISSUE** `#25842`_: (*shikhartanwar*) Running salt-minion as non-root user to execute sudo commands always returns an error + | refs: `#42021`_ + * 7160697 Merge pull request `#42021`_ from gtmanfred/2016.11 + * 26beb18 Set concurrent to True when running states with sudo + +- **PR** `#42029`_: (*terminalmage*) Mock socket.getaddrinfo in unit.utils.network_test.NetworkTestCase.test_host_to_ips + @ *2017-06-30T20:58:56Z* + + * b784fbb Merge pull request `#42029`_ from terminalmage/host_to_ips + * 26f848e Mock socket.getaddrinfo in unit.utils.network_test.NetworkTestCase.test_host_to_ips + +- **PR** `#42055`_: (*dmurphy18*) Upgrade support for gnupg v2.1 and higher + @ *2017-06-30T20:54:02Z* + + * e067020 Merge pull request `#42055`_ from dmurphy18/handle_gnupgv21 + * e20cea6 Upgrade support for gnupg v2.1 and higher + +- **PR** `#42048`_: (*Ch3LL*) Add initial 2016.11.7 Release Notes + @ *2017-06-30T16:00:05Z* + + * 74ba2ab Merge pull request `#42048`_ from Ch3LL/add_11.7 + * 1de5e00 Add initial 2016.11.7 Release Notes + +- **PR** `#42024`_: (*leeclemens*) doc: Specify versionadded for SELinux policy install/uninstall + @ *2017-06-29T23:29:50Z* + + * ca4e619 Merge pull request `#42024`_ from leeclemens/doc/selinux + * b63a3c0 doc: Specify versionadded for SELinux policy install/uninstall + +- **PR** `#42030`_: (*whiteinge*) Re-add msgpack to mocked imports + @ *2017-06-29T20:47:59Z* + + - **PR** `#42028`_: (*whiteinge*) Revert "Allow docs to be built under Python 3" + | refs: `#42030`_ + - **PR** `#41961`_: (*cachedout*) Allow docs to be built under Python 3 + | refs: `#42028`_ + * 50856d0 Merge pull request `#42030`_ from whiteinge/revert-py3-doc-chagnes-pt-2 + * 18dfa98 Re-add msgpack to mocked imports + +- **PR** `#42028`_: (*whiteinge*) Revert "Allow docs to be built under Python 3" + | refs: `#42030`_ + @ *2017-06-29T19:47:46Z* + + - **PR** `#41961`_: (*cachedout*) Allow docs to be built under Python 3 + | refs: `#42028`_ + * 53031d2 Merge pull request `#42028`_ from saltstack/revert-41961-py3_doc + * 5592e6e Revert "Allow docs to be built under Python 3" + +- **PR** `#42017`_: (*lorengordon*) Fixes typo "nozerconf" -> "nozeroconf" + @ *2017-06-29T17:30:48Z* + + - **ISSUE** `#42013`_: (*dusto*) Misspelled nozeroconf in salt/modules/rh_ip.py + | refs: `#42017`_ + * 1416bf7 Merge pull request `#42017`_ from lorengordon/issue-42013 + * b6cf5f2 Fixes typo nozerconf -> nozeroconf + +- **PR** `#41906`_: (*terminalmage*) Better support for numeric saltenvs + @ *2017-06-29T17:19:33Z* + + * 0ebb50b Merge pull request `#41906`_ from terminalmage/numeric-saltenv + * 2d798de Better support for numeric saltenvs + +- **PR** `#41995`_: (*terminalmage*) Temporarily set the umask before writing an auth token + @ *2017-06-29T01:09:48Z* + + * 6a3c03c Merge pull request `#41995`_ from terminalmage/token-umask + * 4f54b00 Temporarily set the umask before writing an auth token + +- **PR** `#41999`_: (*terminalmage*) Update IP address for unit.utils.network_test.NetworkTestCase.test_host_to_ips + @ *2017-06-29T01:01:31Z* + + * e3801b0 Merge pull request `#41999`_ from terminalmage/fix-network-test + * fb6a933 Update IP address for unit.utils.network_test.NetworkTestCase.test_host_to_ips + +- **PR** `#41991`_: (*Da-Juan*) Accept a list for state_aggregate global setting + @ *2017-06-29T00:58:59Z* + + - **ISSUE** `#18659`_: (*whiteinge*) mod_aggregate not working for list-form configuration + | refs: `#41991`_ + * a7f3892 Merge pull request `#41991`_ from Da-Juan/fix-state_aggregate-list + * c9075b8 Accept a list for state_aggregate setting + +- **PR** `#41993`_: (*UtahDave*) change out salt support link to SaltConf link + @ *2017-06-29T00:55:20Z* + + * 7424f87 Merge pull request `#41993`_ from UtahDave/2016.11local + * bff050a change out salt support link to SaltConf link + +- **PR** `#41987`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + @ *2017-06-28T20:19:11Z* + + - **PR** `#41981`_: (*Ch3LL*) [2016.3] Bump latest release version to 2016.11.6 + * 3b9ccf0 Merge pull request `#41987`_ from rallytime/merge-2016.11 + * 48867c4 Merge branch '2016.3' into '2016.11' + + * c589eae Merge pull request `#41981`_ from Ch3LL/11.6_3 + + * 2516ae1 [2016.3] Bump latest release version to 2016.11.6 + +- **PR** `#41985`_: (*rallytime*) Back-port `#41780`_ to 2016.11 + @ *2017-06-28T20:18:57Z* + + - **PR** `#41780`_: (*ferringb*) Fix salt.util.render_jinja_tmpl usage for when not used in an environmnet + | refs: `#41985`_ + * 768339d Merge pull request `#41985`_ from rallytime/`bp-41780`_ + * 8f8d3a4 Fix salt.util.render_jinja_tmpl usage for when not used in an environment. + +- **PR** `#41986`_: (*rallytime*) Back-port `#41820`_ to 2016.11 + @ *2017-06-28T20:18:43Z* + + - **ISSUE** `#34963`_: (*craigafinch*) Incorrect behavior or documentation for comments in salt.states.pkgrepo.managed + | refs: `#41820`_ + - **PR** `#41820`_: (*nhavens*) Fix yum repo file comments to work as documented in pkgrepo.managed + | refs: `#41986`_ + * bd9090c Merge pull request `#41986`_ from rallytime/`bp-41820`_ + * 72320e3 Fix yum repo file comments to work as documented in pkgrepo.managed + +- **PR** `#41973`_: (*vutny*) Fix Master/Minion scheduled jobs based on Cron expressions + | refs: `#42077`_ + @ *2017-06-28T16:39:02Z* + + * a31da52 Merge pull request `#41973`_ from vutny/fix-croniter-scheduled-jobs + * 148788e Fix Master/Minion scheduled jobs based on Cron expressions + +- **PR** `#41980`_: (*Ch3LL*) [2016.11] Bump latest release version to 2016.11.6 + @ *2017-06-28T15:35:11Z* + + * 689ff93 Merge pull request `#41980`_ from Ch3LL/11.6_11 + * fe4f571 [2016.11] Bump latest release version to 2016.11.6 + +- **PR** `#41961`_: (*cachedout*) Allow docs to be built under Python 3 + | refs: `#42028`_ + @ *2017-06-27T21:11:54Z* + + * 82b1eb2 Merge pull request `#41961`_ from cachedout/py3_doc + * 7aacddf Allow docs to be built under Python 3 + +- **PR** `#41948`_: (*davidjb*) Fix Composer state's `name` docs; formatting + @ *2017-06-27T17:51:29Z* + + - **PR** `#41933`_: (*davidjb*) Fix Composer state's `name` docs and improve formatting + | refs: `#41948`_ + * f0eb51d Merge pull request `#41948`_ from davidjb/patch-9 + * 0e4b3d9 Fix Composer state's `name` docs; formatting + +- **PR** `#41914`_: (*vutny*) archive.extracted: fix hash sum verification for local archives + @ *2017-06-26T17:59:27Z* + + * e28e10d Merge pull request `#41914`_ from vutny/fix-archive-extracted-local-file-hash + * 54910fe archive.extracted: fix hash sum verification for local archives + +- **PR** `#41912`_: (*Ch3LL*) Allow pacman module to run on Manjaro + @ *2017-06-26T15:35:20Z* + + * 76ad6ff Merge pull request `#41912`_ from Ch3LL/fix_manjaro + * e4dd72a Update os_name_map in core grains for new manjaro systems + + * aa7c839 Allow pacman module to run on Manjaro + +- **PR** `#41516`_: (*kstreee*) Implements MessageClientPool to avoid blocking waiting for zeromq and tcp communications. + @ *2017-06-26T14:41:38Z* + + - **ISSUE** `#38093`_: (*DmitryKuzmenko*) Make threads avoid blocking waiting while communicating using TCP transport. + | refs: `#41516`_ `#41516`_ + - **PR** `#37878`_: (*kstreee*) Makes threads avoid blocking waiting while communicating using Zeromq. + | refs: `#41516`_ `#41516`_ + * ff67d47 Merge pull request `#41516`_ from kstreee/fix-blocking-waiting-tcp-connection + * df96969 Removes redundant closing statements. + + * 94b9ea5 Implements MessageClientPool to avoid blocking waiting for zeromq and tcp communications. + +- **PR** `#41888`_: (*Ch3LL*) Add additional commits to 2016.11.6 release notes + @ *2017-06-22T16:19:00Z* + + * c90cb67 Merge pull request `#41888`_ from Ch3LL/change_release + * 4e1239d Add additional commits to 2016.11.6 release notes + +- **PR** `#41882`_: (*Ch3LL*) Add pycryptodome to crypt_test + @ *2017-06-21T19:51:10Z* + + * 4a32644 Merge pull request `#41882`_ from Ch3LL/fix_crypt_test + * 6f70dbd Add pycryptodome to crypt_test + +- **PR** `#41877`_: (*Ch3LL*) Fix netstat and routes test + @ *2017-06-21T16:16:58Z* + + * 13df29e Merge pull request `#41877`_ from Ch3LL/fix_netstat_test + * d2076a6 Patch salt.utils.which for test_route test + + * 51f7e10 Patch salt.utils.which for test_netstat test + +- **PR** `#41566`_: (*morganwillcock*) win_certutil: workaround for reading serial numbers with non-English languages + @ *2017-06-21T15:40:29Z* + + - **ISSUE** `#41367`_: (*lubyou*) certutil.add_store does not work on non english windows versions or on Windows 10 (localised or English) + | refs: `#41566`_ + * 66f8c83 Merge pull request `#41566`_ from morganwillcock/certutil + * c337d52 Fix test data for test_get_serial, and a typo + + * 7f69613 test and lint fixes + + * 8ee4843 Suppress output of crypt context and be more specifc with whitespace vs. serial + + * 61f817d Match serials based on output position (fix for non-English languages) + +- **PR** `#41679`_: (*terminalmage*) Prevent unnecessary duplicate pillar compilation + @ *2017-06-21T15:32:42Z* + + * 4d0f5c4 Merge pull request `#41679`_ from terminalmage/get-top-file-envs + * a916e8d Improve normalization of saltenv/pillarenv usage for states + + * 02f293a Update state unit tests to reflect recent changes + + * b7e5c11 Don't compile pillar data when getting top file envs + + * 8d6fdb7 Don't compile pillar twice for salt-call + + * d2abfbf Add initial_pillar argument to salt.state + + * 70186de salt.pillar: rename the "pillar" argument to "pillar_override" + +- **PR** `#41853`_: (*vutny*) Fix master side scheduled jobs to return events + @ *2017-06-20T22:06:29Z* + + - **ISSUE** `#39668`_: (*mirceaulinic*) Master scheduled job not recorded on the event bus + | refs: `#41658`_ + - **ISSUE** `#12653`_: (*pengyao*) salt schedule doesn't return jobs result info to master + | refs: `#41853`_ + - **PR** `#41695`_: (*xiaoanyunfei*) fix max RecursionError, Ellipsis + | refs: `#41853`_ + - **PR** `#41658`_: (*garethgreenaway*) Fixes to the salt scheduler + | refs: `#41853`_ + * 29b0acc Merge pull request `#41853`_ from vutny/fix-master-schedule-event + * e206c38 Fix master side scheduled jobs to return events + + +.. _`#1`: https://github.com/saltstack/salt/issues/1 +.. _`#1036125`: https://github.com/saltstack/salt/issues/1036125 +.. _`#12653`: https://github.com/saltstack/salt/issues/12653 +.. _`#15171`: https://github.com/saltstack/salt/issues/15171 +.. _`#18659`: https://github.com/saltstack/salt/issues/18659 +.. _`#23516`: https://github.com/saltstack/salt/issues/23516 +.. _`#25842`: https://github.com/saltstack/salt/issues/25842 +.. _`#32400`: https://github.com/saltstack/salt/issues/32400 +.. _`#33806`: https://github.com/saltstack/salt/pull/33806 +.. _`#34280`: https://github.com/saltstack/salt/pull/34280 +.. _`#34963`: https://github.com/saltstack/salt/issues/34963 +.. _`#37424`: https://github.com/saltstack/salt/pull/37424 +.. _`#37878`: https://github.com/saltstack/salt/pull/37878 +.. _`#38093`: https://github.com/saltstack/salt/issues/38093 +.. _`#38839`: https://github.com/saltstack/salt/issues/38839 +.. _`#38965`: https://github.com/saltstack/salt/pull/38965 +.. _`#39365`: https://github.com/saltstack/salt/issues/39365 +.. _`#39366`: https://github.com/saltstack/salt/pull/39366 +.. _`#39668`: https://github.com/saltstack/salt/issues/39668 +.. _`#40490`: https://github.com/saltstack/salt/issues/40490 +.. _`#41116`: https://github.com/saltstack/salt/issues/41116 +.. _`#41367`: https://github.com/saltstack/salt/issues/41367 +.. _`#41433`: https://github.com/saltstack/salt/issues/41433 +.. _`#41516`: https://github.com/saltstack/salt/pull/41516 +.. _`#41543`: https://github.com/saltstack/salt/pull/41543 +.. _`#41566`: https://github.com/saltstack/salt/pull/41566 +.. _`#41658`: https://github.com/saltstack/salt/pull/41658 +.. _`#41679`: https://github.com/saltstack/salt/pull/41679 +.. _`#41695`: https://github.com/saltstack/salt/pull/41695 +.. _`#41721`: https://github.com/saltstack/salt/issues/41721 +.. _`#41770`: https://github.com/saltstack/salt/issues/41770 +.. _`#41780`: https://github.com/saltstack/salt/pull/41780 +.. _`#41820`: https://github.com/saltstack/salt/pull/41820 +.. _`#41853`: https://github.com/saltstack/salt/pull/41853 +.. _`#41877`: https://github.com/saltstack/salt/pull/41877 +.. _`#41882`: https://github.com/saltstack/salt/pull/41882 +.. _`#41885`: https://github.com/saltstack/salt/issues/41885 +.. _`#41888`: https://github.com/saltstack/salt/pull/41888 +.. _`#41906`: https://github.com/saltstack/salt/pull/41906 +.. _`#41912`: https://github.com/saltstack/salt/pull/41912 +.. _`#41914`: https://github.com/saltstack/salt/pull/41914 +.. _`#41933`: https://github.com/saltstack/salt/pull/41933 +.. _`#41936`: https://github.com/saltstack/salt/issues/41936 +.. _`#41945`: https://github.com/saltstack/salt/pull/41945 +.. _`#41948`: https://github.com/saltstack/salt/pull/41948 +.. _`#41955`: https://github.com/saltstack/salt/issues/41955 +.. _`#41961`: https://github.com/saltstack/salt/pull/41961 +.. _`#41968`: https://github.com/saltstack/salt/pull/41968 +.. _`#41973`: https://github.com/saltstack/salt/pull/41973 +.. _`#41976`: https://github.com/saltstack/salt/issues/41976 +.. _`#41977`: https://github.com/saltstack/salt/pull/41977 +.. _`#41980`: https://github.com/saltstack/salt/pull/41980 +.. _`#41981`: https://github.com/saltstack/salt/pull/41981 +.. _`#41982`: https://github.com/saltstack/salt/issues/41982 +.. _`#41985`: https://github.com/saltstack/salt/pull/41985 +.. _`#41986`: https://github.com/saltstack/salt/pull/41986 +.. _`#41987`: https://github.com/saltstack/salt/pull/41987 +.. _`#41988`: https://github.com/saltstack/salt/pull/41988 +.. _`#41991`: https://github.com/saltstack/salt/pull/41991 +.. _`#41993`: https://github.com/saltstack/salt/pull/41993 +.. _`#41995`: https://github.com/saltstack/salt/pull/41995 +.. _`#41999`: https://github.com/saltstack/salt/pull/41999 +.. _`#42013`: https://github.com/saltstack/salt/issues/42013 +.. _`#42017`: https://github.com/saltstack/salt/pull/42017 +.. _`#42021`: https://github.com/saltstack/salt/pull/42021 +.. _`#42024`: https://github.com/saltstack/salt/pull/42024 +.. _`#42028`: https://github.com/saltstack/salt/pull/42028 +.. _`#42029`: https://github.com/saltstack/salt/pull/42029 +.. _`#42030`: https://github.com/saltstack/salt/pull/42030 +.. _`#42041`: https://github.com/saltstack/salt/issues/42041 +.. _`#42045`: https://github.com/saltstack/salt/pull/42045 +.. _`#42048`: https://github.com/saltstack/salt/pull/42048 +.. _`#42055`: https://github.com/saltstack/salt/pull/42055 +.. _`#42076`: https://github.com/saltstack/salt/issues/42076 +.. _`#42077`: https://github.com/saltstack/salt/pull/42077 +.. _`#42078`: https://github.com/saltstack/salt/pull/42078 +.. _`#42086`: https://github.com/saltstack/salt/pull/42086 +.. _`#42094`: https://github.com/saltstack/salt/pull/42094 +.. _`#42095`: https://github.com/saltstack/salt/pull/42095 +.. _`#42097`: https://github.com/saltstack/salt/pull/42097 +.. _`#42098`: https://github.com/saltstack/salt/pull/42098 +.. _`#42099`: https://github.com/saltstack/salt/pull/42099 +.. _`#42105`: https://github.com/saltstack/salt/pull/42105 +.. _`#42109`: https://github.com/saltstack/salt/pull/42109 +.. _`#42114`: https://github.com/saltstack/salt/issues/42114 +.. _`#42115`: https://github.com/saltstack/salt/issues/42115 +.. _`#42116`: https://github.com/saltstack/salt/issues/42116 +.. _`#42119`: https://github.com/saltstack/salt/pull/42119 +.. _`#42121`: https://github.com/saltstack/salt/pull/42121 +.. _`#42123`: https://github.com/saltstack/salt/pull/42123 +.. _`#42137`: https://github.com/saltstack/salt/issues/42137 +.. _`#42140`: https://github.com/saltstack/salt/pull/42140 +.. _`#42141`: https://github.com/saltstack/salt/pull/42141 +.. _`#42142`: https://github.com/saltstack/salt/pull/42142 +.. _`#42152`: https://github.com/saltstack/salt/issues/42152 +.. _`#42155`: https://github.com/saltstack/salt/pull/42155 +.. _`#42163`: https://github.com/saltstack/salt/pull/42163 +.. _`#42164`: https://github.com/saltstack/salt/pull/42164 +.. _`#42172`: https://github.com/saltstack/salt/pull/42172 +.. _`#42173`: https://github.com/saltstack/salt/pull/42173 +.. _`#42175`: https://github.com/saltstack/salt/pull/42175 +.. _`#42176`: https://github.com/saltstack/salt/pull/42176 +.. _`#42181`: https://github.com/saltstack/salt/pull/42181 +.. _`#42194`: https://github.com/saltstack/salt/issues/42194 +.. _`#42198`: https://github.com/saltstack/salt/issues/42198 +.. _`#42200`: https://github.com/saltstack/salt/pull/42200 +.. _`#42211`: https://github.com/saltstack/salt/pull/42211 +.. _`#42215`: https://github.com/saltstack/salt/pull/42215 +.. _`#42224`: https://github.com/saltstack/salt/pull/42224 +.. _`#42232`: https://github.com/saltstack/salt/issues/42232 +.. _`#42235`: https://github.com/saltstack/salt/pull/42235 +.. _`#42240`: https://github.com/saltstack/salt/issues/42240 +.. _`#42252`: https://github.com/saltstack/salt/pull/42252 +.. _`#42253`: https://github.com/saltstack/salt/pull/42253 +.. _`#42261`: https://github.com/saltstack/salt/pull/42261 +.. _`#42262`: https://github.com/saltstack/salt/pull/42262 +.. _`#42264`: https://github.com/saltstack/salt/pull/42264 +.. _`#42267`: https://github.com/saltstack/salt/issues/42267 +.. _`#42269`: https://github.com/saltstack/salt/pull/42269 +.. _`#42275`: https://github.com/saltstack/salt/pull/42275 +.. _`#42279`: https://github.com/saltstack/salt/issues/42279 +.. _`#42282`: https://github.com/saltstack/salt/pull/42282 +.. _`#42289`: https://github.com/saltstack/salt/pull/42289 +.. _`#42291`: https://github.com/saltstack/salt/pull/42291 +.. _`#42295`: https://github.com/saltstack/salt/issues/42295 +.. _`#42308`: https://github.com/saltstack/salt/pull/42308 +.. _`#42314`: https://github.com/saltstack/salt/pull/42314 +.. _`#42319`: https://github.com/saltstack/salt/pull/42319 +.. _`#42329`: https://github.com/saltstack/salt/issues/42329 +.. _`#42333`: https://github.com/saltstack/salt/issues/42333 +.. _`#42339`: https://github.com/saltstack/salt/pull/42339 +.. _`#42350`: https://github.com/saltstack/salt/pull/42350 +.. _`#42352`: https://github.com/saltstack/salt/pull/42352 +.. _`#42353`: https://github.com/saltstack/salt/pull/42353 +.. _`#42356`: https://github.com/saltstack/salt/pull/42356 +.. _`#42357`: https://github.com/saltstack/salt/issues/42357 +.. _`#42359`: https://github.com/saltstack/salt/pull/42359 +.. _`#42360`: https://github.com/saltstack/salt/pull/42360 +.. _`#42368`: https://github.com/saltstack/salt/pull/42368 +.. _`#42370`: https://github.com/saltstack/salt/pull/42370 +.. _`#42371`: https://github.com/saltstack/salt/issues/42371 +.. _`#42375`: https://github.com/saltstack/salt/issues/42375 +.. _`#42387`: https://github.com/saltstack/salt/pull/42387 +.. _`#42403`: https://github.com/saltstack/salt/issues/42403 +.. _`#42405`: https://github.com/saltstack/salt/issues/42405 +.. _`#42413`: https://github.com/saltstack/salt/issues/42413 +.. _`#42414`: https://github.com/saltstack/salt/pull/42414 +.. _`#42417`: https://github.com/saltstack/salt/issues/42417 +.. _`#42424`: https://github.com/saltstack/salt/pull/42424 +.. _`#42433`: https://github.com/saltstack/salt/pull/42433 +.. _`#42443`: https://github.com/saltstack/salt/pull/42443 +.. _`#42456`: https://github.com/saltstack/salt/issues/42456 +.. _`#42464`: https://github.com/saltstack/salt/pull/42464 +.. _`#42477`: https://github.com/saltstack/salt/issues/42477 +.. _`#42479`: https://github.com/saltstack/salt/pull/42479 +.. _`#42509`: https://github.com/saltstack/salt/pull/42509 +.. _`#42515`: https://github.com/saltstack/salt/pull/42515 +.. _`#42516`: https://github.com/saltstack/salt/pull/42516 +.. _`#42523`: https://github.com/saltstack/salt/pull/42523 +.. _`#42527`: https://github.com/saltstack/salt/pull/42527 +.. _`#42547`: https://github.com/saltstack/salt/pull/42547 +.. _`#42551`: https://github.com/saltstack/salt/pull/42551 +.. _`#42552`: https://github.com/saltstack/salt/pull/42552 +.. _`#42571`: https://github.com/saltstack/salt/pull/42571 +.. _`#42573`: https://github.com/saltstack/salt/pull/42573 +.. _`#42574`: https://github.com/saltstack/salt/pull/42574 +.. _`#42586`: https://github.com/saltstack/salt/pull/42586 +.. _`#42600`: https://github.com/saltstack/salt/issues/42600 +.. _`#42623`: https://github.com/saltstack/salt/pull/42623 +.. _`#42627`: https://github.com/saltstack/salt/issues/42627 +.. _`#42629`: https://github.com/saltstack/salt/pull/42629 +.. _`#42642`: https://github.com/saltstack/salt/issues/42642 +.. _`#42644`: https://github.com/saltstack/salt/issues/42644 +.. _`#42651`: https://github.com/saltstack/salt/pull/42651 +.. _`#42655`: https://github.com/saltstack/salt/pull/42655 +.. _`#42663`: https://github.com/saltstack/salt/pull/42663 +.. _`#42669`: https://github.com/saltstack/salt/pull/42669 +.. _`#42683`: https://github.com/saltstack/salt/issues/42683 +.. _`#42686`: https://github.com/saltstack/salt/issues/42686 +.. _`#42690`: https://github.com/saltstack/salt/issues/42690 +.. _`#42693`: https://github.com/saltstack/salt/pull/42693 +.. _`#42694`: https://github.com/saltstack/salt/pull/42694 +.. _`#42731`: https://github.com/saltstack/salt/issues/42731 +.. _`#42744`: https://github.com/saltstack/salt/pull/42744 +.. _`#42747`: https://github.com/saltstack/salt/issues/42747 +.. _`#42748`: https://github.com/saltstack/salt/pull/42748 +.. _`#42753`: https://github.com/saltstack/salt/issues/42753 +.. _`#42760`: https://github.com/saltstack/salt/pull/42760 +.. _`#42764`: https://github.com/saltstack/salt/pull/42764 +.. _`#42784`: https://github.com/saltstack/salt/pull/42784 +.. _`#42786`: https://github.com/saltstack/salt/pull/42786 +.. _`#42788`: https://github.com/saltstack/salt/pull/42788 +.. _`#42795`: https://github.com/saltstack/salt/pull/42795 +.. _`#42798`: https://github.com/saltstack/salt/pull/42798 +.. _`#42803`: https://github.com/saltstack/salt/issues/42803 +.. _`#42804`: https://github.com/saltstack/salt/pull/42804 +.. _`#42805`: https://github.com/saltstack/salt/pull/42805 +.. _`#42806`: https://github.com/saltstack/salt/pull/42806 +.. _`#42826`: https://github.com/saltstack/salt/pull/42826 +.. _`#42829`: https://github.com/saltstack/salt/pull/42829 +.. _`#42835`: https://github.com/saltstack/salt/pull/42835 +.. _`#42836`: https://github.com/saltstack/salt/pull/42836 +.. _`#42838`: https://github.com/saltstack/salt/pull/42838 +.. _`#42848`: https://github.com/saltstack/salt/pull/42848 +.. _`#42851`: https://github.com/saltstack/salt/pull/42851 +.. _`#42856`: https://github.com/saltstack/salt/pull/42856 +.. _`#42859`: https://github.com/saltstack/salt/pull/42859 +.. _`#42861`: https://github.com/saltstack/salt/pull/42861 +.. _`#42864`: https://github.com/saltstack/salt/pull/42864 +.. _`#42869`: https://github.com/saltstack/salt/issues/42869 +.. _`#42871`: https://github.com/saltstack/salt/pull/42871 +.. _`#42877`: https://github.com/saltstack/salt/pull/42877 +.. _`#42882`: https://github.com/saltstack/salt/pull/42882 +.. _`#42883`: https://github.com/saltstack/salt/pull/42883 +.. _`#42886`: https://github.com/saltstack/salt/pull/42886 +.. _`#42890`: https://github.com/saltstack/salt/pull/42890 +.. _`#42918`: https://github.com/saltstack/salt/pull/42918 +.. _`#42919`: https://github.com/saltstack/salt/pull/42919 +.. _`#42940`: https://github.com/saltstack/salt/pull/42940 +.. _`#42942`: https://github.com/saltstack/salt/pull/42942 +.. _`#42944`: https://github.com/saltstack/salt/pull/42944 +.. _`#42949`: https://github.com/saltstack/salt/pull/42949 +.. _`#42950`: https://github.com/saltstack/salt/pull/42950 +.. _`#42952`: https://github.com/saltstack/salt/pull/42952 +.. _`#42954`: https://github.com/saltstack/salt/pull/42954 +.. _`#42959`: https://github.com/saltstack/salt/pull/42959 +.. _`#42968`: https://github.com/saltstack/salt/pull/42968 +.. _`#42969`: https://github.com/saltstack/salt/pull/42969 +.. _`#42985`: https://github.com/saltstack/salt/pull/42985 +.. _`#42986`: https://github.com/saltstack/salt/pull/42986 +.. _`#42992`: https://github.com/saltstack/salt/issues/42992 +.. _`#43009`: https://github.com/saltstack/salt/pull/43009 +.. _`#43014`: https://github.com/saltstack/salt/pull/43014 +.. _`#43019`: https://github.com/saltstack/salt/pull/43019 +.. _`#43020`: https://github.com/saltstack/salt/pull/43020 +.. _`#43021`: https://github.com/saltstack/salt/pull/43021 +.. _`#43023`: https://github.com/saltstack/salt/pull/43023 +.. _`#43026`: https://github.com/saltstack/salt/pull/43026 +.. _`#43027`: https://github.com/saltstack/salt/pull/43027 +.. _`#43031`: https://github.com/saltstack/salt/pull/43031 +.. _`#43032`: https://github.com/saltstack/salt/pull/43032 +.. _`#43033`: https://github.com/saltstack/salt/pull/43033 +.. _`#43036`: https://github.com/saltstack/salt/issues/43036 +.. _`#43037`: https://github.com/saltstack/salt/pull/43037 +.. _`#43048`: https://github.com/saltstack/salt/pull/43048 +.. _`#43054`: https://github.com/saltstack/salt/pull/43054 +.. _`#43060`: https://github.com/saltstack/salt/pull/43060 +.. _`#43064`: https://github.com/saltstack/salt/pull/43064 +.. _`#43092`: https://github.com/saltstack/salt/pull/43092 +.. _`#43100`: https://github.com/saltstack/salt/pull/43100 +.. _`#43101`: https://github.com/saltstack/salt/issues/43101 +.. _`#43103`: https://github.com/saltstack/salt/pull/43103 +.. _`#43116`: https://github.com/saltstack/salt/pull/43116 +.. _`#43143`: https://github.com/saltstack/salt/issues/43143 +.. _`#43151`: https://github.com/saltstack/salt/pull/43151 +.. _`#43154`: https://github.com/saltstack/salt/pull/43154 +.. _`#43171`: https://github.com/saltstack/salt/pull/43171 +.. _`#43173`: https://github.com/saltstack/salt/pull/43173 +.. _`#43178`: https://github.com/saltstack/salt/pull/43178 +.. _`#43179`: https://github.com/saltstack/salt/pull/43179 +.. _`#43191`: https://github.com/saltstack/salt/pull/43191 +.. _`#43196`: https://github.com/saltstack/salt/pull/43196 +.. _`#43198`: https://github.com/saltstack/salt/issues/43198 +.. _`#43199`: https://github.com/saltstack/salt/pull/43199 +.. _`#43202`: https://github.com/saltstack/salt/pull/43202 +.. _`#43228`: https://github.com/saltstack/salt/pull/43228 +.. _`#43271`: https://github.com/saltstack/salt/pull/43271 +.. _`#475`: https://github.com/saltstack/salt/issues/475 +.. _`#495`: https://github.com/saltstack/salt/issues/495 +.. _`bp-37424`: https://github.com/saltstack/salt/pull/37424 +.. _`bp-39366`: https://github.com/saltstack/salt/pull/39366 +.. _`bp-41543`: https://github.com/saltstack/salt/pull/41543 +.. _`bp-41780`: https://github.com/saltstack/salt/pull/41780 +.. _`bp-41820`: https://github.com/saltstack/salt/pull/41820 +.. _`bp-42097`: https://github.com/saltstack/salt/pull/42097 +.. _`bp-42098`: https://github.com/saltstack/salt/pull/42098 +.. _`bp-42109`: https://github.com/saltstack/salt/pull/42109 +.. _`bp-42224`: https://github.com/saltstack/salt/pull/42224 +.. _`bp-42433`: https://github.com/saltstack/salt/pull/42433 +.. _`bp-42547`: https://github.com/saltstack/salt/pull/42547 +.. _`bp-42552`: https://github.com/saltstack/salt/pull/42552 +.. _`bp-42651`: https://github.com/saltstack/salt/pull/42651 +.. _`bp-42744`: https://github.com/saltstack/salt/pull/42744 +.. _`bp-42760`: https://github.com/saltstack/salt/pull/42760 +.. _`bp-42784`: https://github.com/saltstack/salt/pull/42784 +.. _`bp-42848`: https://github.com/saltstack/salt/pull/42848 +.. _`bp-42871`: https://github.com/saltstack/salt/pull/42871 +.. _`bp-42883`: https://github.com/saltstack/salt/pull/42883 +.. _`bp-43020`: https://github.com/saltstack/salt/pull/43020 +.. _`bp-43031`: https://github.com/saltstack/salt/pull/43031 +.. _`bp-43116`: https://github.com/saltstack/salt/pull/43116 +.. _`fix-38839`: https://github.com/saltstack/salt/issues/38839 +.. _`fix-41116`: https://github.com/saltstack/salt/issues/41116 +.. _`fix-41721`: https://github.com/saltstack/salt/issues/41721 +.. _`fix-41885`: https://github.com/saltstack/salt/issues/41885 +.. _`fix-42115`: https://github.com/saltstack/salt/issues/42115 +.. _`fix-42152`: https://github.com/saltstack/salt/issues/42152 +.. _`fix-42267`: https://github.com/saltstack/salt/issues/42267 +.. _`fix-42375`: https://github.com/saltstack/salt/issues/42375 +.. _`fix-42405`: https://github.com/saltstack/salt/issues/42405 +.. _`fix-42417`: https://github.com/saltstack/salt/issues/42417 +.. _`fix-42683`: https://github.com/saltstack/salt/issues/42683 From 6a74fec6a62a4f606eb03e40e2d5ae1b0c91c480 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 11 Sep 2017 15:58:17 +0200 Subject: [PATCH 036/633] boto_elb.register_instances: do not skip instances being unregistered `boto_elb.register_instances` currently skips nodes that are in the process of being unregistered, which causes an instance to getting registered again, if registration happens too fast after deregistration. This patch skips instances, where describe-instance-health returns: { "InstanceId": "i-XXX", "State": "InService", "ReasonCode": "N/A", "Description": "Instance deregistration currently in progress." }, The normal state is: { "InstanceId": "i-XXX", "State": "InService", "ReasonCode": "N/A", "Description": "N/A" }, btw: for an instance being in the process of being registered it looks like this: { "InstanceId": "i-XXX", "State": "OutOfService", "ReasonCode": "ELB", "Description": "Instance registration is still in progress." }, Ref: http://docs.aws.amazon.com/elasticloadbalancing/2012-06-01/APIReference/API_InstanceState.html --- salt/states/boto_elb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/states/boto_elb.py b/salt/states/boto_elb.py index 6163c09dfd..5beb01fd37 100644 --- a/salt/states/boto_elb.py +++ b/salt/states/boto_elb.py @@ -517,7 +517,8 @@ def register_instances(name, instances, region=None, key=None, keyid=None, health = __salt__['boto_elb.get_instance_health']( name, region, key, keyid, profile) - nodes = [value['instance_id'] for value in health] + nodes = [value['instance_id'] for value in health + if value['description'] != 'Instance deregistration currently in progress.'] new = [value for value in instances if value not in nodes] if not len(new): msg = 'Instance/s {0} already exist.'.format(str(instances).strip('[]')) From ea6e66175552a65b9c3a13d78799fd78c784a760 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Fri, 8 Sep 2017 15:07:44 -0600 Subject: [PATCH 037/633] Revert "Reduce fileclient.get_file latency by merging _file_find and _file_hash" This reverts commit 94c62388e792884cebc11095db20e3db81fa1348. --- salt/fileclient.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/salt/fileclient.py b/salt/fileclient.py index 2b4484211c..fc396fcc56 100644 --- a/salt/fileclient.py +++ b/salt/fileclient.py @@ -1270,10 +1270,10 @@ class RemoteClient(Client): hash_type = self.opts.get('hash_type', 'md5') ret['hsum'] = salt.utils.get_hash(path, form=hash_type) ret['hash_type'] = hash_type - return ret, list(os.stat(path)) + return ret load = {'path': path, 'saltenv': saltenv, - 'cmd': '_file_hash_and_stat'} + 'cmd': '_file_hash'} return self.channel.send(load) def hash_file(self, path, saltenv='base'): @@ -1282,14 +1282,33 @@ class RemoteClient(Client): master file server prepend the path with salt:// otherwise, prepend the file with / for a local file. ''' - return self.__hash_and_stat_file(path, saltenv)[0] + return self.__hash_and_stat_file(path, saltenv) def hash_and_stat_file(self, path, saltenv='base'): ''' The same as hash_file, but also return the file's mode, or None if no mode data is present. ''' - return self.__hash_and_stat_file(path, saltenv) + hash_result = self.hash_file(path, saltenv) + try: + path = self._check_proto(path) + except MinionError as err: + if not os.path.isfile(path): + return hash_result, None + else: + try: + return hash_result, list(os.stat(path)) + except Exception: + return hash_result, None + load = {'path': path, + 'saltenv': saltenv, + 'cmd': '_file_find'} + fnd = self.channel.send(load) + try: + stat_result = fnd.get('stat') + except AttributeError: + stat_result = None + return hash_result, stat_result def list_env(self, saltenv='base'): ''' From 6114df8dc3cd44a10380e1868dcc72e083f320fb Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Mon, 11 Sep 2017 11:20:33 -0700 Subject: [PATCH 038/633] Adding a small check to ensure we do not continue to populate kwargs with __pub_ items from the kwargs item. --- salt/utils/schedule.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index 2a818628aa..31e3c1aaf1 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -845,7 +845,8 @@ class Schedule(object): if argspec.keywords: # this function accepts **kwargs, pack in the publish data for key, val in six.iteritems(ret): - kwargs['__pub_{0}'.format(key)] = copy.deepcopy(val) + if key is not 'kwargs': + kwargs['__pub_{0}'.format(key)] = copy.deepcopy(val) ret['return'] = self.functions[func](*args, **kwargs) From e496d28cbf7c0a7c8fe18a2797c83c58d90eca23 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 1 Sep 2017 11:18:20 -0600 Subject: [PATCH 039/633] Fix `unit.utils.test_verify` for Windows Use Windows api to get and set the maxstdio Change messages to work with Windows --- tests/unit/utils/test_verify.py | 42 ++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/tests/unit/utils/test_verify.py b/tests/unit/utils/test_verify.py index 795298877d..4df9f2d8e5 100644 --- a/tests/unit/utils/test_verify.py +++ b/tests/unit/utils/test_verify.py @@ -10,10 +10,15 @@ import os import sys import stat import shutil -import resource import tempfile import socket +# Import third party libs +try: + import win32file +except ImportError: + import resource + # Import Salt Testing libs from tests.support.unit import skipIf, TestCase from tests.support.paths import TMP @@ -82,7 +87,10 @@ class TestVerify(TestCase): writer = FakeWriter() sys.stderr = writer # Now run the test - self.assertFalse(check_user('nouser')) + if salt.utils.is_windows(): + self.assertTrue(check_user('nouser')) + else: + self.assertFalse(check_user('nouser')) # Restore sys.stderr sys.stderr = stderr if writer.output != 'CRITICAL: User not found: "nouser"\n': @@ -118,7 +126,6 @@ class TestVerify(TestCase): # not support IPv6. pass - @skipIf(True, 'Skipping until we can find why Jenkins is bailing out') def test_max_open_files(self): with TestsLoggingHandler() as handler: logmsg_dbg = ( @@ -139,15 +146,31 @@ class TestVerify(TestCase): 'raise the salt\'s max_open_files setting. Please consider ' 'raising this value.' ) + if salt.utils.is_windows(): + logmsg_crash = ( + '{0}:The number of accepted minion keys({1}) should be lower ' + 'than 1/4 of the max open files soft setting({2}). ' + 'salt-master will crash pretty soon! Please consider ' + 'raising this value.' + ) - mof_s, mof_h = resource.getrlimit(resource.RLIMIT_NOFILE) + if sys.platform.startswith('win'): + # Check the Windows API for more detail on this + # http://msdn.microsoft.com/en-us/library/xt874334(v=vs.71).aspx + # and the python binding http://timgolden.me.uk/pywin32-docs/win32file.html + mof_s = mof_h = win32file._getmaxstdio() + else: + mof_s, mof_h = resource.getrlimit(resource.RLIMIT_NOFILE) tempdir = tempfile.mkdtemp(prefix='fake-keys') keys_dir = os.path.join(tempdir, 'minions') os.makedirs(keys_dir) mof_test = 256 - resource.setrlimit(resource.RLIMIT_NOFILE, (mof_test, mof_h)) + if salt.utils.is_windows(): + win32file._setmaxstdio(mof_test) + else: + resource.setrlimit(resource.RLIMIT_NOFILE, (mof_test, mof_h)) try: prev = 0 @@ -181,7 +204,7 @@ class TestVerify(TestCase): level, newmax, mof_test, - mof_h - newmax, + mof_test - newmax if salt.utils.is_windows() else mof_h - newmax, ), handler.messages ) @@ -206,7 +229,7 @@ class TestVerify(TestCase): 'CRITICAL', newmax, mof_test, - mof_h - newmax, + mof_test - newmax if salt.utils.is_windows() else mof_h - newmax, ), handler.messages ) @@ -218,7 +241,10 @@ class TestVerify(TestCase): raise finally: shutil.rmtree(tempdir) - resource.setrlimit(resource.RLIMIT_NOFILE, (mof_s, mof_h)) + if salt.utils.is_windows(): + win32file._setmaxstdio(mof_h) + else: + resource.setrlimit(resource.RLIMIT_NOFILE, (mof_s, mof_h)) @skipIf(NO_MOCK, NO_MOCK_REASON) def test_verify_log(self): From c0dc3f73ef3540c93afc6030e28551289dd18598 Mon Sep 17 00:00:00 2001 From: twangboy Date: Mon, 11 Sep 2017 12:21:21 -0600 Subject: [PATCH 040/633] Use sys.platform instead of salt.utils to detect Windows --- tests/unit/utils/test_verify.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/utils/test_verify.py b/tests/unit/utils/test_verify.py index 4df9f2d8e5..f0335718dd 100644 --- a/tests/unit/utils/test_verify.py +++ b/tests/unit/utils/test_verify.py @@ -14,9 +14,9 @@ import tempfile import socket # Import third party libs -try: +if sys.platform.startswith('win'): import win32file -except ImportError: +else: import resource # Import Salt Testing libs @@ -87,7 +87,7 @@ class TestVerify(TestCase): writer = FakeWriter() sys.stderr = writer # Now run the test - if salt.utils.is_windows(): + if sys.platform.startswith('win'): self.assertTrue(check_user('nouser')) else: self.assertFalse(check_user('nouser')) @@ -146,7 +146,7 @@ class TestVerify(TestCase): 'raise the salt\'s max_open_files setting. Please consider ' 'raising this value.' ) - if salt.utils.is_windows(): + if sys.platform.startswith('win'): logmsg_crash = ( '{0}:The number of accepted minion keys({1}) should be lower ' 'than 1/4 of the max open files soft setting({2}). ' @@ -167,7 +167,7 @@ class TestVerify(TestCase): mof_test = 256 - if salt.utils.is_windows(): + if sys.platform.startswith('win'): win32file._setmaxstdio(mof_test) else: resource.setrlimit(resource.RLIMIT_NOFILE, (mof_test, mof_h)) @@ -204,7 +204,7 @@ class TestVerify(TestCase): level, newmax, mof_test, - mof_test - newmax if salt.utils.is_windows() else mof_h - newmax, + mof_test - newmax if sys.platform.startswith('win') else mof_h - newmax, ), handler.messages ) @@ -229,7 +229,7 @@ class TestVerify(TestCase): 'CRITICAL', newmax, mof_test, - mof_test - newmax if salt.utils.is_windows() else mof_h - newmax, + mof_test - newmax if sys.platform.startswith('win') else mof_h - newmax, ), handler.messages ) @@ -241,7 +241,7 @@ class TestVerify(TestCase): raise finally: shutil.rmtree(tempdir) - if salt.utils.is_windows(): + if sys.platform.startswith('win'): win32file._setmaxstdio(mof_h) else: resource.setrlimit(resource.RLIMIT_NOFILE, (mof_s, mof_h)) From be4f26ab21b92dbf5ecb26963c0099a5e2dd28a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Mon, 11 Sep 2017 19:57:28 +0200 Subject: [PATCH 041/633] Use $HOME to get the user home directory instead using '~' char --- pkg/salt.bash | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/salt.bash b/pkg/salt.bash index 480361fe23..00174c072f 100644 --- a/pkg/salt.bash +++ b/pkg/salt.bash @@ -35,7 +35,8 @@ _salt_get_keys(){ } _salt(){ - local _salt_cache_functions=${SALT_COMP_CACHE_FUNCTIONS:='~/.cache/salt-comp-cache_functions'} + CACHE_DIR="$HOME/.cache/salt-comp-cache_functions" + local _salt_cache_functions=${SALT_COMP_CACHE_FUNCTIONS:=$CACHE_DIR} local _salt_cache_timeout=${SALT_COMP_CACHE_TIMEOUT:='last hour'} if [ ! -d "$(dirname ${_salt_cache_functions})" ]; then From c91cd1c6d92912c6d2d9eefecf19927b3e39725e Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 11 Sep 2017 16:21:09 -0400 Subject: [PATCH 042/633] Bump deprecation warning for boto_vpc.describe_route_table This deprecation warning needs to be bumped out to Neon instead of Oxygen. See Issue #43223 for more details. --- salt/modules/boto_vpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/modules/boto_vpc.py b/salt/modules/boto_vpc.py index a564b863d0..f18ae2d68a 100644 --- a/salt/modules/boto_vpc.py +++ b/salt/modules/boto_vpc.py @@ -2456,7 +2456,8 @@ def describe_route_table(route_table_id=None, route_table_name=None, ''' - salt.utils.warn_until('Oxygen', + salt.utils.warn_until( + 'Neon', 'The \'describe_route_table\' method has been deprecated and ' 'replaced by \'describe_route_tables\'.' ) From d26d961fb09229c5beec181a8be7f1585196cb4e Mon Sep 17 00:00:00 2001 From: Kunal Ajay Bajpai Date: Tue, 12 Sep 2017 13:33:41 +0530 Subject: [PATCH 043/633] Fix check for returner save_load --- salt/utils/job.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/salt/utils/job.py b/salt/utils/job.py index c37e034c32..8fe2167612 100644 --- a/salt/utils/job.py +++ b/salt/utils/job.py @@ -100,8 +100,7 @@ def store_job(opts, load, event=None, mminion=None): raise KeyError(emsg) if 'jid' in load \ - and 'get_load' in mminion.returners \ - and not mminion.returners[getfstr](load.get('jid', '')): + and getfstr in mminion.returners: mminion.returners[savefstr](load['jid'], load) mminion.returners[fstr](load) From 7aab1a90e02e2a09a3f89edb978b53735fe1b1c9 Mon Sep 17 00:00:00 2001 From: assaf shapira Date: Tue, 12 Sep 2017 15:07:25 +0300 Subject: [PATCH 044/633] added better debug info and comments --- salt/cloud/clouds/xen.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/salt/cloud/clouds/xen.py b/salt/cloud/clouds/xen.py index 49a0202c70..dffff9aa4b 100644 --- a/salt/cloud/clouds/xen.py +++ b/salt/cloud/clouds/xen.py @@ -160,7 +160,8 @@ def _get_session(): session.xenapi.login_with_password(user, password, api_version, originator) except XenAPI.Failure as ex: ''' - if the server on the url is not the pool master, the pool master's address will be rturned in the exception message + if the server on the url is not the pool master, the pool master's + address will be rturned in the exception message ''' pool_master_addr = str(ex.__dict__['details'][1]) slash_parts = url.split('/') @@ -189,10 +190,15 @@ def list_nodes(): ret = {} for vm in vms: record = session.xenapi.VM.get_record(vm) - if not record['is_a_template'] and not record['is_control_domain']: - ret[record['name_label']] = { - 'id': record['uuid'], - 'image': record['other_config']['base_template_name'], + if not(record['is_a_template']) and not(record['is_control_domain']): + try: + base_template_name = record['other_config']['base_template_name'] + except Exception as KeyError: + base_template_name = None + log.debug('VM {}, doesnt have base_template_name attribute'.format( + record['name_label'])) + ret[record['name_label']] = {'id': record['uuid'], + 'image': base_template_name, 'name': record['name_label'], 'size': record['memory_dynamic_max'], 'state': record['power_state'], @@ -304,10 +310,17 @@ def list_nodes_full(session=None): for vm in vms: record = session.xenapi.VM.get_record(vm) if not record['is_a_template'] and not record['is_control_domain']: + # deal with cases where the VM doesn't have 'base_template_name' attribute + try: + base_template_name = record['other_config']['base_template_name'] + except Exception as KeyError: + base_template_name = None + log.debug('VM {}, doesnt have base_template_name attribute'.format( + record['name_label'])) vm_cfg = session.xenapi.VM.get_record(vm) vm_cfg['id'] = record['uuid'] vm_cfg['name'] = record['name_label'] - vm_cfg['image'] = record['other_config']['base_template_name'] + vm_cfg['image'] = base_template_name vm_cfg['size'] = None vm_cfg['state'] = record['power_state'] vm_cfg['private_ips'] = get_vm_ip(record['name_label'], session) @@ -463,8 +476,14 @@ def show_instance(name, session=None, call=None): vm = _get_vm(name, session=session) record = session.xenapi.VM.get_record(vm) if not record['is_a_template'] and not record['is_control_domain']: + try: + base_template_name = record['other_config']['base_template_name'] + except Exception as KeyError: + base_template_name = None + log.debug('VM {}, doesnt have base_template_name attribute'.format( + record['name_label'])) ret = {'id': record['uuid'], - 'image': record['other_config']['base_template_name'], + 'image': base_template_name, 'name': record['name_label'], 'size': record['memory_dynamic_max'], 'state': record['power_state'], @@ -724,7 +743,7 @@ def _copy_vm(template=None, name=None, session=None, sr=None): ''' Create VM by copy - This is faster and should be used if source and target are + This is slower and should be used if source and target are NOT in the same storage repository template = object reference From 35c1d8898deb3db520eca389d96553562210b269 Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 12 Sep 2017 09:36:34 -0400 Subject: [PATCH 045/633] Add Neon to version list Follow up to #43445 --- salt/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/version.py b/salt/version.py index 0c7695568a..052ccdf677 100644 --- a/salt/version.py +++ b/salt/version.py @@ -90,8 +90,8 @@ class SaltStackVersion(object): 'Nitrogen' : (MAX_SIZE - 102, 0), 'Oxygen' : (MAX_SIZE - 101, 0), 'Fluorine' : (MAX_SIZE - 100, 0), + 'Neon' : (MAX_SIZE - 99, 0), # pylint: disable=E8265 - #'Neon' : (MAX_SIZE - 99 , 0), #'Sodium' : (MAX_SIZE - 98 , 0), #'Magnesium' : (MAX_SIZE - 97 , 0), #'Aluminium' : (MAX_SIZE - 96 , 0), From 139e065ce9825ddba52997eac9fd35779c3fef52 Mon Sep 17 00:00:00 2001 From: Olivier Mauras Date: Wed, 12 Jul 2017 17:29:22 +0200 Subject: [PATCH 046/633] New pillar/master_tops saltclass module --- doc/topics/releases/oxygen.rst | 188 +++++++++++ salt/pillar/saltclass.py | 62 ++++ salt/tops/saltclass.py | 69 ++++ salt/utils/saltclass.py | 296 ++++++++++++++++++ .../examples/classes/app/borgbackup.yml | 6 + .../examples/classes/app/ssh/server.yml | 4 + .../examples/classes/default/init.yml | 17 + .../examples/classes/default/motd.yml | 3 + .../examples/classes/default/users.yml | 16 + .../saltclass/examples/classes/roles/app.yml | 21 ++ .../examples/classes/roles/nginx/init.yml | 7 + .../examples/classes/roles/nginx/server.yml | 7 + .../examples/classes/subsidiaries/gnv.yml | 20 ++ .../examples/classes/subsidiaries/qls.yml | 17 + .../examples/classes/subsidiaries/zrh.yml | 24 ++ .../saltclass/examples/nodes/fake_id.yml | 6 + tests/unit/pillar/test_saltclass.py | 43 +++ 17 files changed, 806 insertions(+) create mode 100644 salt/pillar/saltclass.py create mode 100644 salt/tops/saltclass.py create mode 100644 salt/utils/saltclass.py create mode 100644 tests/integration/files/saltclass/examples/classes/app/borgbackup.yml create mode 100644 tests/integration/files/saltclass/examples/classes/app/ssh/server.yml create mode 100644 tests/integration/files/saltclass/examples/classes/default/init.yml create mode 100644 tests/integration/files/saltclass/examples/classes/default/motd.yml create mode 100644 tests/integration/files/saltclass/examples/classes/default/users.yml create mode 100644 tests/integration/files/saltclass/examples/classes/roles/app.yml create mode 100644 tests/integration/files/saltclass/examples/classes/roles/nginx/init.yml create mode 100644 tests/integration/files/saltclass/examples/classes/roles/nginx/server.yml create mode 100644 tests/integration/files/saltclass/examples/classes/subsidiaries/gnv.yml create mode 100644 tests/integration/files/saltclass/examples/classes/subsidiaries/qls.yml create mode 100644 tests/integration/files/saltclass/examples/classes/subsidiaries/zrh.yml create mode 100644 tests/integration/files/saltclass/examples/nodes/fake_id.yml create mode 100644 tests/unit/pillar/test_saltclass.py diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index d3cd440d45..ec6a79195e 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -97,6 +97,194 @@ file. For example: These commands will run in sequence **before** the bootstrap script is executed. +New pillar/master_tops module called saltclass +---------------------------------------------- + +This module clones the behaviour of reclass (http://reclass.pantsfullofunix.net/), without the need of an external app, and add several features to improve flexibility. +Saltclass lets you define your nodes from simple ``yaml`` files (``.yml``) through hierarchical class inheritance with the possibility to override pillars down the tree. + +**Features** + +- Define your nodes through hierarchical class inheritance +- Reuse your reclass datas with minimal modifications + - applications => states + - parameters => pillars +- Use Jinja templating in your yaml definitions +- Access to the following Salt objects in Jinja + - ``__opts__`` + - ``__salt__`` + - ``__grains__`` + - ``__pillars__`` + - ``minion_id`` +- Chose how to merge or override your lists using ^ character (see examples) +- Expand variables ${} with possibility to escape them if needed \${} (see examples) +- Ignores missing node/class and will simply return empty without breaking the pillar module completely - will be logged + +An example subset of datas is available here: http://git.mauras.ch/salt/saltclass/src/master/examples + +========================== =========== +Terms usable in yaml files Description +========================== =========== +classes A list of classes that will be processed in order +states A list of states that will be returned by master_tops function +pillars A yaml dictionnary that will be returned by the ext_pillar function +environment Node saltenv that will be used by master_tops +========================== =========== + +A class consists of: + +- zero or more parent classes +- zero or more states +- any number of pillars + +A child class can override pillars from a parent class. +A node definition is a class in itself with an added ``environment`` parameter for ``saltenv`` definition. + +**class names** + +Class names mimic salt way of defining states and pillar files. +This means that ``default.users`` class name will correspond to one of these: + +- ``/classes/default/users.yml`` +- ``/classes/default/users/init.yml`` + +**Saltclass tree** + +A saltclass tree would look like this: + +.. code-block:: text + + + ├── classes + │ ├── app + │ │ ├── borgbackup.yml + │ │ └── ssh + │ │ └── server.yml + │ ├── default + │ │ ├── init.yml + │ │ ├── motd.yml + │ │ └── users.yml + │ ├── roles + │ │ ├── app.yml + │ │ └── nginx + │ │ ├── init.yml + │ │ └── server.yml + │ └── subsidiaries + │ ├── gnv.yml + │ ├── qls.yml + │ └── zrh.yml + └── nodes + ├── geneva + │ └── gnv.node1.yml + ├── lausanne + │ ├── qls.node1.yml + │ └── qls.node2.yml + ├── node127.yml + └── zurich + ├── zrh.node1.yml + ├── zrh.node2.yml + └── zrh.node3.yml + +**Examples** + +``/nodes/lausanne/qls.node1.yml`` + +.. code-block:: yaml + + environment: base + + classes: + {% for class in ['default'] %} + - {{ class }} + {% endfor %} + - subsidiaries.{{ __grains__['id'].split('.')[0] }} + +``/classes/default/init.yml`` + +.. code-block:: yaml + + classes: + - default.users + - default.motd + + states: + - openssh + + pillars: + default: + network: + dns: + srv1: 192.168.0.1 + srv2: 192.168.0.2 + domain: example.com + ntp: + srv1: 192.168.10.10 + srv2: 192.168.10.20 + +``/classes/subsidiaries/gnv.yml`` + +.. code-block:: yaml + + pillars: + default: + network: + sub: Geneva + dns: + srv1: 10.20.0.1 + srv2: 10.20.0.2 + srv3: 192.168.1.1 + domain: gnv.example.com + users: + adm1: + uid: 1210 + gid: 1210 + gecos: 'Super user admin1' + homedir: /srv/app/adm1 + adm3: + uid: 1203 + gid: 1203 + gecos: 'Super user adm + +Variable expansions: + +Escaped variables are rendered as is - ``${test}`` + +Missing variables are rendered as is - ``${net:dns:srv2}`` + +.. code-block:: yaml + + pillars: + app: + config: + dns: + srv1: ${default:network:dns:srv1} + srv2: ${net:dns:srv2} + uri: https://application.domain/call?\${test} + prod_parameters: + - p1 + - p2 + - p3 + pkg: + - app-core + - app-backend + +List override: + +Not using ``^`` as the first entry will simply merge the lists + +.. code-block:: yaml + + pillars: + app: + pkg: + - ^ + - app-frontend + + +**Known limitation** + +Currently you can't have both a variable and an escaped variable in the same string as the escaped one will not be correctly rendered - '\${xx}' will stay as is instead of being rendered as '${xx}' + Newer PyWinRM Versions ---------------------- diff --git a/salt/pillar/saltclass.py b/salt/pillar/saltclass.py new file mode 100644 index 0000000000..41732bffd0 --- /dev/null +++ b/salt/pillar/saltclass.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +''' +SaltClass Pillar Module + +.. code-block:: yaml + + ext_pillar: + - saltclass: + - path: /srv/saltclass + +''' + +# import python libs +from __future__ import absolute_import +import salt.utils.saltclass as sc +import logging + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + This module has no external dependencies + ''' + return True + + +def ext_pillar(minion_id, pillar, *args, **kwargs): + ''' + Node definitions path will be retrieved from args - or set to default - + then added to 'salt_data' dict that is passed to the 'get_pillars' function. + 'salt_data' dict is a convenient way to pass all the required datas to the function + It contains: + - __opts__ + - __salt__ + - __grains__ + - __pillar__ + - minion_id + - path + + If successfull the function will return a pillar dict for minion_id + ''' + # If path has not been set, make a default + for i in args: + if 'path' not in i: + path = '/srv/saltclass' + args[i]['path'] = path + log.warning('path variable unset, using default: {0}'.format(path)) + else: + path = i['path'] + + # Create a dict that will contain our salt dicts to pass it to reclass + salt_data = { + '__opts__': __opts__, + '__salt__': __salt__, + '__grains__': __grains__, + '__pillar__': pillar, + 'minion_id': minion_id, + 'path': path + } + + return sc.get_pillars(minion_id, salt_data) diff --git a/salt/tops/saltclass.py b/salt/tops/saltclass.py new file mode 100644 index 0000000000..585641a024 --- /dev/null +++ b/salt/tops/saltclass.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +''' +SaltClass master_tops Module + +.. code-block:: yaml + master_tops: + saltclass: + path: /srv/saltclass +''' + +# import python libs +from __future__ import absolute_import +import logging + +import salt.utils.saltclass as sc + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only run if properly configured + ''' + if __opts__['master_tops'].get('saltclass'): + return True + return False + + +def top(**kwargs): + ''' + Node definitions path will be retrieved from __opts__ - or set to default - + then added to 'salt_data' dict that is passed to the 'get_tops' function. + 'salt_data' dict is a convenient way to pass all the required datas to the function + It contains: + - __opts__ + - empty __salt__ + - __grains__ + - empty __pillar__ + - minion_id + - path + + If successfull the function will return a top dict for minion_id + ''' + # If path has not been set, make a default + _opts = __opts__['master_tops']['saltclass'] + if 'path' not in _opts: + path = '/srv/saltclass' + log.warning('path variable unset, using default: {0}'.format(path)) + else: + path = _opts['path'] + + # Create a dict that will contain our salt objects + # to send to get_tops function + if 'id' not in kwargs['opts']: + log.warning('Minion id not found - Returning empty dict') + return {} + else: + minion_id = kwargs['opts']['id'] + + salt_data = { + '__opts__': kwargs['opts'], + '__salt__': {}, + '__grains__': kwargs['grains'], + '__pillar__': {}, + 'minion_id': minion_id, + 'path': path + } + + return sc.get_tops(minion_id, salt_data) diff --git a/salt/utils/saltclass.py b/salt/utils/saltclass.py new file mode 100644 index 0000000000..3df204d5dc --- /dev/null +++ b/salt/utils/saltclass.py @@ -0,0 +1,296 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +import os +import re +import logging +from salt.ext.six import iteritems +import yaml +from jinja2 import FileSystemLoader, Environment + +log = logging.getLogger(__name__) + + +# Renders jinja from a template file +def render_jinja(_file, salt_data): + j_env = Environment(loader=FileSystemLoader(os.path.dirname(_file))) + j_env.globals.update({ + '__opts__': salt_data['__opts__'], + '__salt__': salt_data['__salt__'], + '__grains__': salt_data['__grains__'], + '__pillar__': salt_data['__pillar__'], + 'minion_id': salt_data['minion_id'], + }) + j_render = j_env.get_template(os.path.basename(_file)).render() + return j_render + + +# Renders yaml from rendered jinja +def render_yaml(_file, salt_data): + return yaml.safe_load(render_jinja(_file, salt_data)) + + +# Returns a dict from a class yaml definition +def get_class(_class, salt_data): + l_files = [] + saltclass_path = salt_data['path'] + + straight = '{0}/classes/{1}.yml'.format(saltclass_path, _class) + sub_straight = '{0}/classes/{1}.yml'.format(saltclass_path, + _class.replace('.', '/')) + sub_init = '{0}/classes/{1}/init.yml'.format(saltclass_path, + _class.replace('.', '/')) + + for root, dirs, files in os.walk('{0}/classes'.format(saltclass_path)): + for l_file in files: + l_files.append('{0}/{1}'.format(root, l_file)) + + if straight in l_files: + return render_yaml(straight, salt_data) + + if sub_straight in l_files: + return render_yaml(sub_straight, salt_data) + + if sub_init in l_files: + return render_yaml(sub_init, salt_data) + + log.warning('{0}: Class definition not found'.format(_class)) + return {} + + +# Return environment +def get_env_from_dict(exp_dict_list): + environment = '' + for s_class in exp_dict_list: + if 'environment' in s_class: + environment = s_class['environment'] + return environment + + +# Merge dict b into a +def dict_merge(a, b, path=None): + if path is None: + path = [] + + for key in b: + if key in a: + if isinstance(a[key], list) and isinstance(b[key], list): + if b[key][0] == '^': + b[key].pop(0) + a[key] = b[key] + else: + a[key].extend(b[key]) + elif isinstance(a[key], dict) and isinstance(b[key], dict): + dict_merge(a[key], b[key], path + [str(key)]) + elif a[key] == b[key]: + pass + else: + a[key] = b[key] + else: + a[key] = b[key] + return a + + +# Recursive search and replace in a dict +def dict_search_and_replace(d, old, new, expanded): + for (k, v) in iteritems(d): + if isinstance(v, dict): + dict_search_and_replace(d[k], old, new, expanded) + if v == old: + d[k] = new + return d + + +# Retrieve original value from ${xx:yy:zz} to be expanded +def find_value_to_expand(x, v): + a = x + for i in v[2:-1].split(':'): + if i in a: + a = a.get(i) + else: + a = v + return a + return a + + +# Return a dict that contains expanded variables if found +def expand_variables(a, b, expanded, path=None): + if path is None: + b = a.copy() + path = [] + + for (k, v) in iteritems(a): + if isinstance(v, dict): + expand_variables(v, b, expanded, path + [str(k)]) + else: + if isinstance(v, str): + vre = re.search(r'(^|.)\$\{.*?\}', v) + if vre: + re_v = vre.group(0) + if re_v.startswith('\\'): + v_new = v.replace(re_v, re_v.lstrip('\\')) + b = dict_search_and_replace(b, v, v_new, expanded) + expanded.append(k) + elif not re_v.startswith('$'): + v_expanded = find_value_to_expand(b, re_v[1:]) + v_new = v.replace(re_v[1:], v_expanded) + b = dict_search_and_replace(b, v, v_new, expanded) + expanded.append(k) + else: + v_expanded = find_value_to_expand(b, re_v) + b = dict_search_and_replace(b, v, v_expanded, expanded) + expanded.append(k) + return b + + +def expand_classes_in_order(minion_dict, + salt_data, + seen_classes, + expanded_classes, + classes_to_expand): + # Get classes to expand from minion dictionnary + if not classes_to_expand and 'classes' in minion_dict: + classes_to_expand = minion_dict['classes'] + + # Now loop on list to recursively expand them + for klass in classes_to_expand: + if klass not in seen_classes: + seen_classes.append(klass) + expanded_classes[klass] = get_class(klass, salt_data) + # Fix corner case where class is loaded but doesn't contain anything + if expanded_classes[klass] is None: + expanded_classes[klass] = {} + # Now replace class element in classes_to_expand by expansion + if 'classes' in expanded_classes[klass]: + l_id = classes_to_expand.index(klass) + classes_to_expand[l_id:l_id] = expanded_classes[klass]['classes'] + expand_classes_in_order(minion_dict, + salt_data, + seen_classes, + expanded_classes, + classes_to_expand) + else: + expand_classes_in_order(minion_dict, + salt_data, + seen_classes, + expanded_classes, + classes_to_expand) + + # We may have duplicates here and we want to remove them + tmp = [] + for t_element in classes_to_expand: + if t_element not in tmp: + tmp.append(t_element) + + classes_to_expand = tmp + + # Now that we've retrieved every class in order, + # let's return an ordered list of dicts + ord_expanded_classes = [] + ord_expanded_states = [] + for ord_klass in classes_to_expand: + ord_expanded_classes.append(expanded_classes[ord_klass]) + # And be smart and sort out states list + # Address the corner case where states is empty in a class definition + if 'states' in expanded_classes[ord_klass] and expanded_classes[ord_klass]['states'] is None: + expanded_classes[ord_klass]['states'] = {} + + if 'states' in expanded_classes[ord_klass]: + ord_expanded_states.extend(expanded_classes[ord_klass]['states']) + + # Add our minion dict as final element but check if we have states to process + if 'states' in minion_dict and minion_dict['states'] is None: + minion_dict['states'] = [] + + if 'states' in minion_dict: + ord_expanded_states.extend(minion_dict['states']) + + ord_expanded_classes.append(minion_dict) + + return ord_expanded_classes, classes_to_expand, ord_expanded_states + + +def expanded_dict_from_minion(minion_id, salt_data): + _file = '' + saltclass_path = salt_data['path'] + # Start + for root, dirs, files in os.walk('{0}/nodes'.format(saltclass_path)): + for minion_file in files: + if minion_file == '{0}.yml'.format(minion_id): + _file = os.path.join(root, minion_file) + + # Load the minion_id definition if existing, else an exmpty dict + node_dict = {} + if _file: + node_dict[minion_id] = render_yaml(_file, salt_data) + else: + log.warning('{0}: Node definition not found'.format(minion_id)) + node_dict[minion_id] = {} + + # Get 2 ordered lists: + # expanded_classes: A list of all the dicts + # classes_list: List of all the classes + expanded_classes, classes_list, states_list = expand_classes_in_order( + node_dict[minion_id], + salt_data, [], {}, []) + + # Here merge the pillars together + pillars_dict = {} + for exp_dict in expanded_classes: + if 'pillars' in exp_dict: + dict_merge(pillars_dict, exp_dict) + + return expanded_classes, pillars_dict, classes_list, states_list + + +def get_pillars(minion_id, salt_data): + # Get 2 dicts and 2 lists + # expanded_classes: Full list of expanded dicts + # pillars_dict: dict containing merged pillars in order + # classes_list: All classes processed in order + # states_list: All states listed in order + (expanded_classes, + pillars_dict, + classes_list, + states_list) = expanded_dict_from_minion(minion_id, salt_data) + + # Retrieve environment + environment = get_env_from_dict(expanded_classes) + + # Expand ${} variables in merged dict + # pillars key shouldn't exist if we haven't found any minion_id ref + if 'pillars' in pillars_dict: + pillars_dict_expanded = expand_variables(pillars_dict['pillars'], {}, []) + else: + pillars_dict_expanded = expand_variables({}, {}, []) + + # Build the final pillars dict + pillars_dict = {} + pillars_dict['__saltclass__'] = {} + pillars_dict['__saltclass__']['states'] = states_list + pillars_dict['__saltclass__']['classes'] = classes_list + pillars_dict['__saltclass__']['environment'] = environment + pillars_dict['__saltclass__']['nodename'] = minion_id + pillars_dict.update(pillars_dict_expanded) + + return pillars_dict + + +def get_tops(minion_id, salt_data): + # Get 2 dicts and 2 lists + # expanded_classes: Full list of expanded dicts + # pillars_dict: dict containing merged pillars in order + # classes_list: All classes processed in order + # states_list: All states listed in order + (expanded_classes, + pillars_dict, + classes_list, + states_list) = expanded_dict_from_minion(minion_id, salt_data) + + # Retrieve environment + environment = get_env_from_dict(expanded_classes) + + # Build final top dict + tops_dict = {} + tops_dict[environment] = states_list + + return tops_dict diff --git a/tests/integration/files/saltclass/examples/classes/app/borgbackup.yml b/tests/integration/files/saltclass/examples/classes/app/borgbackup.yml new file mode 100644 index 0000000000..10f2865df7 --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/app/borgbackup.yml @@ -0,0 +1,6 @@ +classes: + - app.ssh.server + +pillars: + sshd: + root_access: yes diff --git a/tests/integration/files/saltclass/examples/classes/app/ssh/server.yml b/tests/integration/files/saltclass/examples/classes/app/ssh/server.yml new file mode 100644 index 0000000000..9ebd94322f --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/app/ssh/server.yml @@ -0,0 +1,4 @@ +pillars: + sshd: + root_access: no + ssh_port: 22 diff --git a/tests/integration/files/saltclass/examples/classes/default/init.yml b/tests/integration/files/saltclass/examples/classes/default/init.yml new file mode 100644 index 0000000000..20a5e45088 --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/default/init.yml @@ -0,0 +1,17 @@ +classes: + - default.users + - default.motd + +states: + - openssh + +pillars: + default: + network: + dns: + srv1: 192.168.0.1 + srv2: 192.168.0.2 + domain: example.com + ntp: + srv1: 192.168.10.10 + srv2: 192.168.10.20 diff --git a/tests/integration/files/saltclass/examples/classes/default/motd.yml b/tests/integration/files/saltclass/examples/classes/default/motd.yml new file mode 100644 index 0000000000..18938d7b1a --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/default/motd.yml @@ -0,0 +1,3 @@ +pillars: + motd: + text: "Welcome to {{ __grains__['id'] }} system located in ${default:network:sub}" diff --git a/tests/integration/files/saltclass/examples/classes/default/users.yml b/tests/integration/files/saltclass/examples/classes/default/users.yml new file mode 100644 index 0000000000..8bfba67109 --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/default/users.yml @@ -0,0 +1,16 @@ +states: + - user_mgt + +pillars: + default: + users: + adm1: + uid: 1201 + gid: 1201 + gecos: 'Super user admin1' + homedir: /home/adm1 + adm2: + uid: 1202 + gid: 1202 + gecos: 'Super user admin2' + homedir: /home/adm2 diff --git a/tests/integration/files/saltclass/examples/classes/roles/app.yml b/tests/integration/files/saltclass/examples/classes/roles/app.yml new file mode 100644 index 0000000000..af244e402c --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/roles/app.yml @@ -0,0 +1,21 @@ +states: + - app + +pillars: + app: + config: + dns: + srv1: ${default:network:dns:srv1} + srv2: ${default:network:dns:srv2} + uri: https://application.domain/call?\${test} + prod_parameters: + - p1 + - p2 + - p3 + pkg: + - app-core + - app-backend +# Safe minion_id matching +{% if minion_id == 'zrh.node3' %} + safe_pillar: '_only_ zrh.node3 will see this pillar and this cannot be overriden like grains' +{% endif %} diff --git a/tests/integration/files/saltclass/examples/classes/roles/nginx/init.yml b/tests/integration/files/saltclass/examples/classes/roles/nginx/init.yml new file mode 100644 index 0000000000..996ded51fa --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/roles/nginx/init.yml @@ -0,0 +1,7 @@ +states: + - nginx_deployment + +pillars: + nginx: + pkg: + - nginx diff --git a/tests/integration/files/saltclass/examples/classes/roles/nginx/server.yml b/tests/integration/files/saltclass/examples/classes/roles/nginx/server.yml new file mode 100644 index 0000000000..bc290997a6 --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/roles/nginx/server.yml @@ -0,0 +1,7 @@ +classes: + - roles.nginx + +pillars: + nginx: + pkg: + - nginx-module diff --git a/tests/integration/files/saltclass/examples/classes/subsidiaries/gnv.yml b/tests/integration/files/saltclass/examples/classes/subsidiaries/gnv.yml new file mode 100644 index 0000000000..7e7c39c60c --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/subsidiaries/gnv.yml @@ -0,0 +1,20 @@ +pillars: + default: + network: + sub: Geneva + dns: + srv1: 10.20.0.1 + srv2: 10.20.0.2 + srv3: 192.168.1.1 + domain: gnv.example.com + users: + adm1: + uid: 1210 + gid: 1210 + gecos: 'Super user admin1' + homedir: /srv/app/adm1 + adm3: + uid: 1203 + gid: 1203 + gecos: 'Super user admin3' + homedir: /home/adm3 diff --git a/tests/integration/files/saltclass/examples/classes/subsidiaries/qls.yml b/tests/integration/files/saltclass/examples/classes/subsidiaries/qls.yml new file mode 100644 index 0000000000..2289548276 --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/subsidiaries/qls.yml @@ -0,0 +1,17 @@ +classes: + - app.ssh.server + - roles.nginx.server + +pillars: + default: + network: + sub: Lausanne + dns: + srv1: 10.10.0.1 + domain: qls.example.com + users: + nginx_adm: + uid: 250 + gid: 200 + gecos: 'Nginx admin user' + homedir: /srv/www diff --git a/tests/integration/files/saltclass/examples/classes/subsidiaries/zrh.yml b/tests/integration/files/saltclass/examples/classes/subsidiaries/zrh.yml new file mode 100644 index 0000000000..ac30dc73b9 --- /dev/null +++ b/tests/integration/files/saltclass/examples/classes/subsidiaries/zrh.yml @@ -0,0 +1,24 @@ +classes: + - roles.app + # This should validate that we process a class only once + - app.borgbackup + # As this one should not be processed + # and would override in turn overrides from app.borgbackup + - app.ssh.server + +pillars: + default: + network: + sub: Zurich + dns: + srv1: 10.30.0.1 + srv2: 10.30.0.2 + domain: zrh.example.com + ntp: + srv1: 10.0.0.127 + users: + adm1: + uid: 250 + gid: 250 + gecos: 'Super user admin1' + homedir: /srv/app/1 diff --git a/tests/integration/files/saltclass/examples/nodes/fake_id.yml b/tests/integration/files/saltclass/examples/nodes/fake_id.yml new file mode 100644 index 0000000000..a87137e6fb --- /dev/null +++ b/tests/integration/files/saltclass/examples/nodes/fake_id.yml @@ -0,0 +1,6 @@ +environment: base + +classes: +{% for class in ['default'] %} + - {{ class }} +{% endfor %} diff --git a/tests/unit/pillar/test_saltclass.py b/tests/unit/pillar/test_saltclass.py new file mode 100644 index 0000000000..30b63f8c54 --- /dev/null +++ b/tests/unit/pillar/test_saltclass.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +# Import python libs +from __future__ import absolute_import +import os + +# Import Salt Testing libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import NO_MOCK, NO_MOCK_REASON + +# Import Salt Libs +import salt.pillar.saltclass as saltclass + + +base_path = os.path.dirname(os.path.realpath(__file__)) +fake_minion_id = 'fake_id' +fake_pillar = {} +fake_args = ({'path': '{0}/../../integration/files/saltclass/examples'.format(base_path)}) +fake_opts = {} +fake_salt = {} +fake_grains = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class SaltclassPillarTestCase(TestCase, LoaderModuleMockMixin): + ''' + Tests for salt.pillar.saltclass + ''' + def setup_loader_modules(self): + return {saltclass: {'__opts__': fake_opts, + '__salt__': fake_salt, + '__grains__': fake_grains + }} + + def _runner(self, expected_ret): + full_ret = saltclass.ext_pillar(fake_minion_id, fake_pillar, fake_args) + parsed_ret = full_ret['__saltclass__']['classes'] + self.assertListEqual(parsed_ret, expected_ret) + + def test_succeeds(self): + ret = ['default.users', 'default.motd', 'default'] + self._runner(ret) From fb31e9a530e3efbfc9177f3daddd2ca1ad206d6f Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 12 Sep 2017 10:05:36 -0600 Subject: [PATCH 047/633] Add /norestart switch to vcredist install --- pkg/windows/installer/Salt-Minion-Setup.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/windows/installer/Salt-Minion-Setup.nsi b/pkg/windows/installer/Salt-Minion-Setup.nsi index ab890529d5..83010476a0 100644 --- a/pkg/windows/installer/Salt-Minion-Setup.nsi +++ b/pkg/windows/installer/Salt-Minion-Setup.nsi @@ -204,7 +204,7 @@ Section -Prerequisites ; The Correct version of VCRedist is copied over by "build_pkg.bat" SetOutPath "$INSTDIR\" File "..\prereqs\vcredist.exe" - ExecWait "$INSTDIR\vcredist.exe /qb!" + ExecWait "$INSTDIR\vcredist.exe /qb! /norestart" IfErrors 0 endVcRedist MessageBox MB_OK \ "VC Redist 2008 SP1 MFC failed to install. Try installing the package manually." \ From d80aea16cb8a6bc3075ac051e22623d8feeea7bb Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 12 Sep 2017 11:56:17 -0600 Subject: [PATCH 048/633] Handle ErrorCodes returned by VCRedist installer --- pkg/windows/installer/Salt-Minion-Setup.nsi | 28 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/pkg/windows/installer/Salt-Minion-Setup.nsi b/pkg/windows/installer/Salt-Minion-Setup.nsi index 83010476a0..9108fb3e5f 100644 --- a/pkg/windows/installer/Salt-Minion-Setup.nsi +++ b/pkg/windows/installer/Salt-Minion-Setup.nsi @@ -200,17 +200,38 @@ Section -Prerequisites "VC Redist 2008 SP1 MFC is currently not installed. Would you like to install?" \ /SD IDYES IDNO endVcRedist - ClearErrors ; The Correct version of VCRedist is copied over by "build_pkg.bat" SetOutPath "$INSTDIR\" File "..\prereqs\vcredist.exe" - ExecWait "$INSTDIR\vcredist.exe /qb! /norestart" - IfErrors 0 endVcRedist + # If an output variable is specified ($0 in the case below), + # ExecWait sets the variable with the exit code (and only sets the + # error flag if an error occurs; if an error occurs, the contents + # of the user variable are undefined). + # http://nsis.sourceforge.net/Reference/ExecWait + ClearErrors + ExecWait '"$INSTDIR\vcredist.exe" /qb! /norestart' $0 + IfErrors 0 CheckVcRedistErrorCode: MessageBox MB_OK \ "VC Redist 2008 SP1 MFC failed to install. Try installing the package manually." \ /SD IDOK + Goto endVcRedist + + checkVcRedistErrorCode: + # Check for Reboot Error Code (3010) + ${If} $0 == 3010 + MessageBox MB_OK \ + "VC Redist 2008 SP1 MFC installed but requires a restart to complete." \ + /SD IDOK + + # Check for any other errors + ${ElseIfNot} $0 == 0 + MessageBox MB_OK \ + "VC Redist 2008 SP1 MFC failed with ErrorCode: $0. Try installing the package manually." \ + /SD IDOK + ${EndIf} endVcRedist: + ${EndIf} ${EndIf} @@ -715,6 +736,7 @@ Function getMinionConfig confFound: FileOpen $0 "$INSTDIR\conf\minion" r + ClearErrors confLoop: FileRead $0 $1 IfErrors EndOfFile From 2d269d1a763dc3b6de2bd438c036aa75a642cc0e Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 12 Sep 2017 12:59:57 -0600 Subject: [PATCH 049/633] Change all comment markers to '#' --- pkg/windows/installer/Salt-Minion-Setup.nsi | 380 ++++++++++---------- 1 file changed, 190 insertions(+), 190 deletions(-) diff --git a/pkg/windows/installer/Salt-Minion-Setup.nsi b/pkg/windows/installer/Salt-Minion-Setup.nsi index 9108fb3e5f..094e3e7030 100644 --- a/pkg/windows/installer/Salt-Minion-Setup.nsi +++ b/pkg/windows/installer/Salt-Minion-Setup.nsi @@ -38,7 +38,7 @@ ${StrStrAdv} !define CPUARCH "x86" !endif -; Part of the Trim function for Strings +# Part of the Trim function for Strings !define Trim "!insertmacro Trim" !macro Trim ResultVar String Push "${String}" @@ -55,27 +55,27 @@ ${StrStrAdv} !define MUI_UNICON "salt.ico" !define MUI_WELCOMEFINISHPAGE_BITMAP "panel.bmp" -; Welcome page +# Welcome page !insertmacro MUI_PAGE_WELCOME -; License page +# License page !insertmacro MUI_PAGE_LICENSE "LICENSE.txt" -; Configure Minion page +# Configure Minion page Page custom pageMinionConfig pageMinionConfig_Leave -; Instfiles page +# Instfiles page !insertmacro MUI_PAGE_INSTFILES -; Finish page (Customized) +# Finish page (Customized) !define MUI_PAGE_CUSTOMFUNCTION_SHOW pageFinish_Show !define MUI_PAGE_CUSTOMFUNCTION_LEAVE pageFinish_Leave !insertmacro MUI_PAGE_FINISH -; Uninstaller pages +# Uninstaller pages !insertmacro MUI_UNPAGE_INSTFILES -; Language files +# Language files !insertmacro MUI_LANGUAGE "English" @@ -175,11 +175,11 @@ ShowInstDetails show ShowUnInstDetails show -; Check and install Visual C++ 2008 SP1 MFC Security Update redist packages -; See http://blogs.msdn.com/b/astebner/archive/2009/01/29/9384143.aspx for more info +# Check and install Visual C++ 2008 SP1 MFC Security Update redist packages +# See http://blogs.msdn.com/b/astebner/archive/2009/01/29/9384143.aspx for more info Section -Prerequisites - ; VCRedist only needed on Windows Server 2008R2/Windows 7 and below + # VCRedist only needed on Windows Server 2008R2/Windows 7 and below ${If} ${AtMostWin2008R2} !define VC_REDIST_X64_GUID "{5FCE6D76-F5DC-37AB-B2B8-22AB8CEDB1D4}" @@ -200,7 +200,7 @@ Section -Prerequisites "VC Redist 2008 SP1 MFC is currently not installed. Would you like to install?" \ /SD IDYES IDNO endVcRedist - ; The Correct version of VCRedist is copied over by "build_pkg.bat" + # The Correct version of VCRedist is copied over by "build_pkg.bat" SetOutPath "$INSTDIR\" File "..\prereqs\vcredist.exe" # If an output variable is specified ($0 in the case below), @@ -210,7 +210,7 @@ Section -Prerequisites # http://nsis.sourceforge.net/Reference/ExecWait ClearErrors ExecWait '"$INSTDIR\vcredist.exe" /qb! /norestart' $0 - IfErrors 0 CheckVcRedistErrorCode: + IfErrors 0 CheckVcRedistErrorCode MessageBox MB_OK \ "VC Redist 2008 SP1 MFC failed to install. Try installing the package manually." \ /SD IDOK @@ -257,12 +257,12 @@ Function .onInit Call parseCommandLineSwitches - ; Check for existing installation + # Check for existing installation ReadRegStr $R0 HKLM \ "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ "UninstallString" StrCmp $R0 "" checkOther - ; Found existing installation, prompt to uninstall + # Found existing installation, prompt to uninstall MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \ "${PRODUCT_NAME} is already installed.$\n$\n\ Click `OK` to remove the existing installation." \ @@ -270,12 +270,12 @@ Function .onInit Abort checkOther: - ; Check for existing installation of full salt + # Check for existing installation of full salt ReadRegStr $R0 HKLM \ "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME_OTHER}" \ "UninstallString" StrCmp $R0 "" skipUninstall - ; Found existing installation, prompt to uninstall + # Found existing installation, prompt to uninstall MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \ "${PRODUCT_NAME_OTHER} is already installed.$\n$\n\ Click `OK` to remove the existing installation." \ @@ -283,27 +283,27 @@ Function .onInit Abort uninst: - ; Make sure we're in the right directory + # Make sure we're in the right directory ${If} $INSTDIR == "c:\salt\bin\Scripts" StrCpy $INSTDIR "C:\salt" ${EndIf} - ; Stop and remove the salt-minion service + # Stop and remove the salt-minion service nsExec::Exec 'net stop salt-minion' nsExec::Exec 'sc delete salt-minion' - ; Stop and remove the salt-master service + # Stop and remove the salt-master service nsExec::Exec 'net stop salt-master' nsExec::Exec 'sc delete salt-master' - ; Remove salt binaries and batch files + # Remove salt binaries and batch files Delete "$INSTDIR\uninst.exe" Delete "$INSTDIR\nssm.exe" Delete "$INSTDIR\salt*" Delete "$INSTDIR\vcredist.exe" RMDir /r "$INSTDIR\bin" - ; Remove registry entries + # Remove registry entries DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY_OTHER}" DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_CALL_REGKEY}" @@ -313,7 +313,7 @@ Function .onInit DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_MINION_REGKEY}" DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_RUN_REGKEY}" - ; Remove C:\salt from the Path + # Remove C:\salt from the Path Push "C:\salt" Call RemoveFromPath @@ -326,7 +326,7 @@ Section -Post WriteUninstaller "$INSTDIR\uninst.exe" - ; Uninstall Registry Entries + # Uninstall Registry Entries WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" \ "DisplayName" "$(^Name)" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" \ @@ -342,24 +342,24 @@ Section -Post WriteRegStr HKLM "SYSTEM\CurrentControlSet\services\salt-minion" \ "DependOnService" "nsi" - ; Set the estimated size + # Set the estimated size ${GetSize} "$INSTDIR\bin" "/S=OK" $0 $1 $2 IntFmt $0 "0x%08X" $0 WriteRegDWORD ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" \ "EstimatedSize" "$0" - ; Commandline Registry Entries + # Commandline Registry Entries WriteRegStr HKLM "${PRODUCT_CALL_REGKEY}" "" "$INSTDIR\salt-call.bat" WriteRegStr HKLM "${PRODUCT_CALL_REGKEY}" "Path" "$INSTDIR\bin\" WriteRegStr HKLM "${PRODUCT_MINION_REGKEY}" "" "$INSTDIR\salt-minion.bat" WriteRegStr HKLM "${PRODUCT_MINION_REGKEY}" "Path" "$INSTDIR\bin\" - ; Register the Salt-Minion Service + # Register the Salt-Minion Service nsExec::Exec "nssm.exe install salt-minion $INSTDIR\bin\python.exe -E -s $INSTDIR\bin\Scripts\salt-minion -c $INSTDIR\conf -l quiet" nsExec::Exec "nssm.exe set salt-minion Description Salt Minion from saltstack.com" nsExec::Exec "nssm.exe set salt-minion AppNoConsole 1" - RMDir /R "$INSTDIR\var\cache\salt" ; removing cache from old version + RMDir /R "$INSTDIR\var\cache\salt" # removing cache from old version Call updateMinionConfig @@ -373,7 +373,7 @@ SectionEnd Function .onInstSuccess - ; If start-minion is 1, then start the service + # If start-minion is 1, then start the service ${If} $StartMinion == 1 nsExec::Exec 'net start salt-minion' ${EndIf} @@ -391,35 +391,35 @@ FunctionEnd Section Uninstall - ; Stop and Remove salt-minion service + # Stop and Remove salt-minion service nsExec::Exec 'net stop salt-minion' nsExec::Exec 'sc delete salt-minion' - ; Remove files + # Remove files Delete "$INSTDIR\uninst.exe" Delete "$INSTDIR\nssm.exe" Delete "$INSTDIR\salt*" Delete "$INSTDIR\vcredist.exe" - ; Remove salt directory, you must check to make sure you're not removing - ; the Program Files directory + # Remove salt directory, you must check to make sure you're not removing + # the Program Files directory ${If} $INSTDIR != 'Program Files' ${AndIf} $INSTDIR != 'Program Files (x86)' RMDir /r "$INSTDIR" ${EndIf} - ; Remove Uninstall Entries + # Remove Uninstall Entries DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" - ; Remove Commandline Entries + # Remove Commandline Entries DeleteRegKey HKLM "${PRODUCT_CALL_REGKEY}" DeleteRegKey HKLM "${PRODUCT_MINION_REGKEY}" - ; Remove C:\salt from the Path + # Remove C:\salt from the Path Push "C:\salt" Call un.RemoveFromPath - ; Automatically close when finished + # Automatically close when finished SetAutoClose true SectionEnd @@ -450,7 +450,7 @@ FunctionEnd Function Trim - Exch $R1 ; Original string + Exch $R1 # Original string Push $R2 Loop: @@ -482,36 +482,36 @@ Function Trim FunctionEnd -;------------------------------------------------------------------------------ -; StrStr Function -; - find substring in a string -; -; Usage: -; Push "this is some string" -; Push "some" -; Call StrStr -; Pop $0 ; "some string" -;------------------------------------------------------------------------------ +#------------------------------------------------------------------------------ +# StrStr Function +# - find substring in a string +# +# Usage: +# Push "this is some string" +# Push "some" +# Call StrStr +# Pop $0 ; "some string" +#------------------------------------------------------------------------------ !macro StrStr un Function ${un}StrStr - Exch $R1 ; $R1=substring, stack=[old$R1,string,...] - Exch ; stack=[string,old$R1,...] - Exch $R2 ; $R2=string, stack=[old$R2,old$R1,...] - Push $R3 ; $R3=strlen(substring) - Push $R4 ; $R4=count - Push $R5 ; $R5=tmp - StrLen $R3 $R1 ; Get the length of the Search String - StrCpy $R4 0 ; Set the counter to 0 + Exch $R1 # $R1=substring, stack=[old$R1,string,...] + Exch # stack=[string,old$R1,...] + Exch $R2 # $R2=string, stack=[old$R2,old$R1,...] + Push $R3 # $R3=strlen(substring) + Push $R4 # $R4=count + Push $R5 # $R5=tmp + StrLen $R3 $R1 # Get the length of the Search String + StrCpy $R4 0 # Set the counter to 0 loop: - StrCpy $R5 $R2 $R3 $R4 ; Create a moving window of the string that is - ; the size of the length of the search string - StrCmp $R5 $R1 done ; Is the contents of the window the same as - ; search string, then done - StrCmp $R5 "" done ; Is the window empty, then done - IntOp $R4 $R4 + 1 ; Shift the windows one character - Goto loop ; Repeat + StrCpy $R5 $R2 $R3 $R4 # Create a moving window of the string that is + # the size of the length of the search string + StrCmp $R5 $R1 done # Is the contents of the window the same as + # search string, then done + StrCmp $R5 "" done # Is the window empty, then done + IntOp $R4 $R4 + 1 # Shift the windows one character + Goto loop # Repeat done: StrCpy $R1 $R2 "" $R4 @@ -519,7 +519,7 @@ Function ${un}StrStr Pop $R4 Pop $R3 Pop $R2 - Exch $R1 ; $R1=old$R1, stack=[result,...] + Exch $R1 # $R1=old$R1, stack=[result,...] FunctionEnd !macroend @@ -527,74 +527,74 @@ FunctionEnd !insertmacro StrStr "un." -;------------------------------------------------------------------------------ -; AddToPath Function -; - Adds item to Path for All Users -; - Overcomes NSIS ReadRegStr limitation of 1024 characters by using Native -; Windows Commands -; -; Usage: -; Push "C:\path\to\add" -; Call AddToPath -;------------------------------------------------------------------------------ +#------------------------------------------------------------------------------ +# AddToPath Function +# - Adds item to Path for All Users +# - Overcomes NSIS ReadRegStr limitation of 1024 characters by using Native +# Windows Commands +# +# Usage: +# Push "C:\path\to\add" +# Call AddToPath +#------------------------------------------------------------------------------ !define Environ 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' Function AddToPath - Exch $0 ; Path to add - Push $1 ; Current Path - Push $2 ; Results of StrStr / Length of Path + Path to Add - Push $3 ; Handle to Reg / Length of Path - Push $4 ; Result of Registry Call + Exch $0 # Path to add + Push $1 # Current Path + Push $2 # Results of StrStr / Length of Path + Path to Add + Push $3 # Handle to Reg / Length of Path + Push $4 # Result of Registry Call - ; Open a handle to the key in the registry, handle in $3, Error in $4 + # Open a handle to the key in the registry, handle in $3, Error in $4 System::Call "advapi32::RegOpenKey(i 0x80000002, t'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', *i.r3) i.r4" - ; Make sure registry handle opened successfully (returned 0) + # Make sure registry handle opened successfully (returned 0) IntCmp $4 0 0 done done - ; Load the contents of path into $1, Error Code into $4, Path length into $2 + # Load the contents of path into $1, Error Code into $4, Path length into $2 System::Call "advapi32::RegQueryValueEx(i $3, t'PATH', i 0, i 0, t.r1, *i ${NSIS_MAX_STRLEN} r2) i.r4" - ; Close the handle to the registry ($3) + # Close the handle to the registry ($3) System::Call "advapi32::RegCloseKey(i $3)" - ; Check for Error Code 234, Path too long for the variable - IntCmp $4 234 0 +4 +4 ; $4 == ERROR_MORE_DATA + # Check for Error Code 234, Path too long for the variable + IntCmp $4 234 0 +4 +4 # $4 == ERROR_MORE_DATA DetailPrint "AddToPath Failed: original length $2 > ${NSIS_MAX_STRLEN}" MessageBox MB_OK \ "You may add C:\salt to the %PATH% for convenience when issuing local salt commands from the command line." \ /SD IDOK Goto done - ; If no error, continue - IntCmp $4 0 +5 ; $4 != NO_ERROR - ; Error 2 means the Key was not found - IntCmp $4 2 +3 ; $4 != ERROR_FILE_NOT_FOUND + # If no error, continue + IntCmp $4 0 +5 # $4 != NO_ERROR + # Error 2 means the Key was not found + IntCmp $4 2 +3 # $4 != ERROR_FILE_NOT_FOUND DetailPrint "AddToPath: unexpected error code $4" Goto done StrCpy $1 "" - ; Check if already in PATH - Push "$1;" ; The string to search - Push "$0;" ; The string to find + # Check if already in PATH + Push "$1;" # The string to search + Push "$0;" # The string to find Call StrStr - Pop $2 ; The result of the search - StrCmp $2 "" 0 done ; String not found, try again with ';' at the end - ; Otherwise, it's already in the path - Push "$1;" ; The string to search - Push "$0\;" ; The string to find + Pop $2 # The result of the search + StrCmp $2 "" 0 done # String not found, try again with ';' at the end + # Otherwise, it's already in the path + Push "$1;" # The string to search + Push "$0\;" # The string to find Call StrStr - Pop $2 ; The result - StrCmp $2 "" 0 done ; String not found, continue (add) - ; Otherwise, it's already in the path + Pop $2 # The result + StrCmp $2 "" 0 done # String not found, continue (add) + # Otherwise, it's already in the path - ; Prevent NSIS string overflow - StrLen $2 $0 ; Length of path to add ($2) - StrLen $3 $1 ; Length of current path ($3) - IntOp $2 $2 + $3 ; Length of current path + path to add ($2) - IntOp $2 $2 + 2 ; Account for the additional ';' - ; $2 = strlen(dir) + strlen(PATH) + sizeof(";") + # Prevent NSIS string overflow + StrLen $2 $0 # Length of path to add ($2) + StrLen $3 $1 # Length of current path ($3) + IntOp $2 $2 + $3 # Length of current path + path to add ($2) + IntOp $2 $2 + 2 # Account for the additional ';' + # $2 = strlen(dir) + strlen(PATH) + sizeof(";") - ; Make sure the new length isn't over the NSIS_MAX_STRLEN + # Make sure the new length isn't over the NSIS_MAX_STRLEN IntCmp $2 ${NSIS_MAX_STRLEN} +4 +4 0 DetailPrint "AddToPath: new length $2 > ${NSIS_MAX_STRLEN}" MessageBox MB_OK \ @@ -602,18 +602,18 @@ Function AddToPath /SD IDOK Goto done - ; Append dir to PATH + # Append dir to PATH DetailPrint "Add to PATH: $0" - StrCpy $2 $1 1 -1 ; Copy the last character of the existing path - StrCmp $2 ";" 0 +2 ; Check for trailing ';' - StrCpy $1 $1 -1 ; remove trailing ';' - StrCmp $1 "" +2 ; Make sure Path is not empty - StrCpy $0 "$1;$0" ; Append new path at the end ($0) + StrCpy $2 $1 1 -1 # Copy the last character of the existing path + StrCmp $2 ";" 0 +2 # Check for trailing ';' + StrCpy $1 $1 -1 # remove trailing ';' + StrCmp $1 "" +2 # Make sure Path is not empty + StrCpy $0 "$1;$0" # Append new path at the end ($0) - ; We can use the NSIS command here. Only 'ReadRegStr' is affected + # We can use the NSIS command here. Only 'ReadRegStr' is affected WriteRegExpandStr ${Environ} "PATH" $0 - ; Broadcast registry change to open programs + # Broadcast registry change to open programs SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 done: @@ -626,16 +626,16 @@ Function AddToPath FunctionEnd -;------------------------------------------------------------------------------ -; RemoveFromPath Function -; - Removes item from Path for All Users -; - Overcomes NSIS ReadRegStr limitation of 1024 characters by using Native -; Windows Commands -; -; Usage: -; Push "C:\path\to\add" -; Call RemoveFromPath -;------------------------------------------------------------------------------ +#------------------------------------------------------------------------------ +# RemoveFromPath Function +# - Removes item from Path for All Users +# - Overcomes NSIS ReadRegStr limitation of 1024 characters by using Native +# Windows Commands +# +# Usage: +# Push "C:\path\to\add" +# Call RemoveFromPath +#------------------------------------------------------------------------------ !macro RemoveFromPath un Function ${un}RemoveFromPath @@ -647,59 +647,59 @@ Function ${un}RemoveFromPath Push $5 Push $6 - ; Open a handle to the key in the registry, handle in $3, Error in $4 + # Open a handle to the key in the registry, handle in $3, Error in $4 System::Call "advapi32::RegOpenKey(i 0x80000002, t'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', *i.r3) i.r4" - ; Make sure registry handle opened successfully (returned 0) + # Make sure registry handle opened successfully (returned 0) IntCmp $4 0 0 done done - ; Load the contents of path into $1, Error Code into $4, Path length into $2 + # Load the contents of path into $1, Error Code into $4, Path length into $2 System::Call "advapi32::RegQueryValueEx(i $3, t'PATH', i 0, i 0, t.r1, *i ${NSIS_MAX_STRLEN} r2) i.r4" - ; Close the handle to the registry ($3) + # Close the handle to the registry ($3) System::Call "advapi32::RegCloseKey(i $3)" - ; Check for Error Code 234, Path too long for the variable - IntCmp $4 234 0 +4 +4 ; $4 == ERROR_MORE_DATA + # Check for Error Code 234, Path too long for the variable + IntCmp $4 234 0 +4 +4 # $4 == ERROR_MORE_DATA DetailPrint "AddToPath: original length $2 > ${NSIS_MAX_STRLEN}" Goto done - ; If no error, continue - IntCmp $4 0 +5 ; $4 != NO_ERROR - ; Error 2 means the Key was not found - IntCmp $4 2 +3 ; $4 != ERROR_FILE_NOT_FOUND + # If no error, continue + IntCmp $4 0 +5 # $4 != NO_ERROR + # Error 2 means the Key was not found + IntCmp $4 2 +3 # $4 != ERROR_FILE_NOT_FOUND DetailPrint "AddToPath: unexpected error code $4" Goto done StrCpy $1 "" - ; Ensure there's a trailing ';' - StrCpy $5 $1 1 -1 ; Copy the last character of the path - StrCmp $5 ";" +2 ; Check for trailing ';', if found continue - StrCpy $1 "$1;" ; ensure trailing ';' + # Ensure there's a trailing ';' + StrCpy $5 $1 1 -1 # Copy the last character of the path + StrCmp $5 ";" +2 # Check for trailing ';', if found continue + StrCpy $1 "$1;" # ensure trailing ';' - ; Check for our directory inside the path - Push $1 ; String to Search - Push "$0;" ; Dir to Find + # Check for our directory inside the path + Push $1 # String to Search + Push "$0;" # Dir to Find Call ${un}StrStr - Pop $2 ; The results of the search - StrCmp $2 "" done ; If results are empty, we're done, otherwise continue + Pop $2 # The results of the search + StrCmp $2 "" done # If results are empty, we're done, otherwise continue - ; Remove our Directory from the Path + # Remove our Directory from the Path DetailPrint "Remove from PATH: $0" - StrLen $3 "$0;" ; Get the length of our dir ($3) - StrLen $4 $2 ; Get the length of the return from StrStr ($4) - StrCpy $5 $1 -$4 ; $5 is now the part before the path to remove - StrCpy $6 $2 "" $3 ; $6 is now the part after the path to remove - StrCpy $3 "$5$6" ; Combine $5 and $6 + StrLen $3 "$0;" # Get the length of our dir ($3) + StrLen $4 $2 # Get the length of the return from StrStr ($4) + StrCpy $5 $1 -$4 # $5 is now the part before the path to remove + StrCpy $6 $2 "" $3 # $6 is now the part after the path to remove + StrCpy $3 "$5$6" # Combine $5 and $6 - ; Check for Trailing ';' - StrCpy $5 $3 1 -1 ; Load the last character of the string - StrCmp $5 ";" 0 +2 ; Check for ';' - StrCpy $3 $3 -1 ; remove trailing ';' + # Check for Trailing ';' + StrCpy $5 $3 1 -1 # Load the last character of the string + StrCmp $5 ";" 0 +2 # Check for ';' + StrCpy $3 $3 -1 # remove trailing ';' - ; Write the new path to the registry + # Write the new path to the registry WriteRegExpandStr ${Environ} "PATH" $3 - ; Broadcast the change to all open applications + # Broadcast the change to all open applications SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 done: @@ -767,64 +767,64 @@ FunctionEnd Function updateMinionConfig ClearErrors - FileOpen $0 "$INSTDIR\conf\minion" "r" ; open target file for reading - GetTempFileName $R0 ; get new temp file name - FileOpen $1 $R0 "w" ; open temp file for writing + FileOpen $0 "$INSTDIR\conf\minion" "r" # open target file for reading + GetTempFileName $R0 # get new temp file name + FileOpen $1 $R0 "w" # open temp file for writing - loop: ; loop through each line - FileRead $0 $2 ; read line from target file - IfErrors done ; end if errors are encountered (end of line) + loop: # loop through each line + FileRead $0 $2 # read line from target file + IfErrors done # end if errors are encountered (end of line) - ${If} $MasterHost_State != "" ; if master is empty - ${AndIf} $MasterHost_State != "salt" ; and if master is not 'salt' - ${StrLoc} $3 $2 "master:" ">" ; where is 'master:' in this line - ${If} $3 == 0 ; is it in the first... - ${OrIf} $3 == 1 ; or second position (account for comments) - StrCpy $2 "master: $MasterHost_State$\r$\n" ; write the master - ${EndIf} ; close if statement - ${EndIf} ; close if statement + ${If} $MasterHost_State != "" # if master is empty + ${AndIf} $MasterHost_State != "salt" # and if master is not 'salt' + ${StrLoc} $3 $2 "master:" ">" # where is 'master:' in this line + ${If} $3 == 0 # is it in the first... + ${OrIf} $3 == 1 # or second position (account for comments) + StrCpy $2 "master: $MasterHost_State$\r$\n" # write the master + ${EndIf} # close if statement + ${EndIf} # close if statement - ${If} $MinionName_State != "" ; if minion is empty - ${AndIf} $MinionName_State != "hostname" ; and if minion is not 'hostname' - ${StrLoc} $3 $2 "id:" ">" ; where is 'id:' in this line - ${If} $3 == 0 ; is it in the first... - ${OrIf} $3 == 1 ; or the second position (account for comments) - StrCpy $2 "id: $MinionName_State$\r$\n" ; change line - ${EndIf} ; close if statement - ${EndIf} ; close if statement + ${If} $MinionName_State != "" # if minion is empty + ${AndIf} $MinionName_State != "hostname" # and if minion is not 'hostname' + ${StrLoc} $3 $2 "id:" ">" # where is 'id:' in this line + ${If} $3 == 0 # is it in the first... + ${OrIf} $3 == 1 # or the second position (account for comments) + StrCpy $2 "id: $MinionName_State$\r$\n" # change line + ${EndIf} # close if statement + ${EndIf} # close if statement - FileWrite $1 $2 ; write changed or unchanged line to temp file + FileWrite $1 $2 # write changed or unchanged line to temp file Goto loop done: - FileClose $0 ; close target file - FileClose $1 ; close temp file - Delete "$INSTDIR\conf\minion" ; delete target file - CopyFiles /SILENT $R0 "$INSTDIR\conf\minion" ; copy temp file to target file - Delete $R0 ; delete temp file + FileClose $0 # close target file + FileClose $1 # close temp file + Delete "$INSTDIR\conf\minion" # delete target file + CopyFiles /SILENT $R0 "$INSTDIR\conf\minion" # copy temp file to target file + Delete $R0 # delete temp file FunctionEnd Function parseCommandLineSwitches - ; Load the parameters + # Load the parameters ${GetParameters} $R0 - ; Check for start-minion switches - ; /start-service is to be deprecated, so we must check for both + # Check for start-minion switches + # /start-service is to be deprecated, so we must check for both ${GetOptions} $R0 "/start-service=" $R1 ${GetOptions} $R0 "/start-minion=" $R2 # Service: Start Salt Minion ${IfNot} $R2 == "" - ; If start-minion was passed something, then set it + # If start-minion was passed something, then set it StrCpy $StartMinion $R2 ${ElseIfNot} $R1 == "" - ; If start-service was passed something, then set it + # If start-service was passed something, then set it StrCpy $StartMinion $R1 ${Else} - ; Otherwise default to 1 + # Otherwise default to 1 StrCpy $StartMinion 1 ${EndIf} From 7f08983288cbad5e605368d3ba1cc1f761551864 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 10 Sep 2017 14:55:24 -0400 Subject: [PATCH 050/633] Implemented being able to have proxy data both in the esxdatacenter proxy config and in its pillar --- salt/proxy/esxdatacenter.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/salt/proxy/esxdatacenter.py b/salt/proxy/esxdatacenter.py index 186b880c2c..5460863d84 100644 --- a/salt/proxy/esxdatacenter.py +++ b/salt/proxy/esxdatacenter.py @@ -153,6 +153,7 @@ import os # Import Salt Libs import salt.exceptions from salt.config.schemas.esxdatacenter import EsxdatacenterProxySchema +from salt.utils.dictupdate import merge # This must be present or the Salt loader won't load this module. __proxyenabled__ = ['esxdatacenter'] @@ -195,42 +196,44 @@ def init(opts): log.trace('Validating esxdatacenter proxy input') schema = EsxdatacenterProxySchema.serialize() log.trace('schema = {}'.format(schema)) + proxy_conf = merge(opts.get('proxy', {}), __pillar__.get('proxy', {})) + log.trace('proxy_conf = {0}'.format(proxy_conf)) try: - jsonschema.validate(opts['proxy'], schema) + jsonschema.validate(proxy_conf, schema) except jsonschema.exceptions.ValidationError as exc: raise salt.exceptions.InvalidConfigError(exc) # Save mandatory fields in cache for key in ('vcenter', 'datacenter', 'mechanism'): - DETAILS[key] = opts['proxy'][key] + DETAILS[key] = proxy_conf[key] # Additional validation if DETAILS['mechanism'] == 'userpass': - if 'username' not in opts['proxy']: + if 'username' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'userpass\', but no ' '\'username\' key found in proxy config.') - if 'passwords' not in opts['proxy']: + if 'passwords' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'userpass\', but no ' '\'passwords\' key found in proxy config.') for key in ('username', 'passwords'): - DETAILS[key] = opts['proxy'][key] + DETAILS[key] = proxy_conf[key] else: - if 'domain' not in opts['proxy']: + if 'domain' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'sspi\', but no ' '\'domain\' key found in proxy config.') - if 'principal' not in opts['proxy']: + if 'principal' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'sspi\', but no ' '\'principal\' key found in proxy config.') for key in ('domain', 'principal'): - DETAILS[key] = opts['proxy'][key] + DETAILS[key] = proxy_conf[key] # Save optional - DETAILS['protocol'] = opts['proxy'].get('protocol') - DETAILS['port'] = opts['proxy'].get('port') + DETAILS['protocol'] = proxy_conf.get('protocol') + DETAILS['port'] = proxy_conf.get('port') # Test connection if DETAILS['mechanism'] == 'userpass': From 0e7b8e0c92f34a783d7e2a0c8f77c956a7a13438 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 10 Sep 2017 16:37:09 -0400 Subject: [PATCH 051/633] Adjusted tests for esxdatacenter proxy --- tests/unit/proxy/test_esxdatacenter.py | 66 ++++++++++++++++++-------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/tests/unit/proxy/test_esxdatacenter.py b/tests/unit/proxy/test_esxdatacenter.py index fb44851a9f..ea1658d5ac 100644 --- a/tests/unit/proxy/test_esxdatacenter.py +++ b/tests/unit/proxy/test_esxdatacenter.py @@ -38,7 +38,7 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): return {esxdatacenter: {'__virtual__': MagicMock(return_value='esxdatacenter'), - 'DETAILS': {}}} + 'DETAILS': {}, '__pillar__': {}}} def setUp(self): self.opts_userpass = {'proxy': {'proxytype': 'esxdatacenter', @@ -57,6 +57,22 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): 'principal': 'fake_principal', 'protocol': 'fake_protocol', 'port': 100}} + patches = (('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=self.opts_sspi['proxy'])),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def test_merge(self): + mock_pillar_proxy = MagicMock() + mock_opts_proxy = MagicMock() + mock_merge = MagicMock(return_value=self.opts_sspi['proxy']) + with patch.dict(esxdatacenter.__pillar__, + {'proxy': mock_pillar_proxy}): + with patch('salt.proxy.esxdatacenter.merge', mock_merge): + esxdatacenter.init(opts={'proxy': mock_opts_proxy}) + mock_merge.assert_called_once_with(mock_opts_proxy, mock_pillar_proxy) def test_esxdatacenter_schema(self): mock_json_validate = MagicMock() @@ -80,9 +96,11 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_no_username(self): opts = self.opts_userpass.copy() del opts['proxy']['username'] - with self.assertRaises(salt.exceptions.InvalidConfigError) as \ - excinfo: - esxdatacenter.init(opts) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxdatacenter.init(opts) self.assertEqual(excinfo.exception.strerror, 'Mechanism is set to \'userpass\', but no ' '\'username\' key found in proxy config.') @@ -90,9 +108,11 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_no_passwords(self): opts = self.opts_userpass.copy() del opts['proxy']['passwords'] - with self.assertRaises(salt.exceptions.InvalidConfigError) as \ - excinfo: - esxdatacenter.init(opts) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxdatacenter.init(opts) self.assertEqual(excinfo.exception.strerror, 'Mechanism is set to \'userpass\', but no ' '\'passwords\' key found in proxy config.') @@ -100,9 +120,11 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_no_domain(self): opts = self.opts_sspi.copy() del opts['proxy']['domain'] - with self.assertRaises(salt.exceptions.InvalidConfigError) as \ - excinfo: - esxdatacenter.init(opts) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxdatacenter.init(opts) self.assertEqual(excinfo.exception.strerror, 'Mechanism is set to \'sspi\', but no ' '\'domain\' key found in proxy config.') @@ -110,9 +132,11 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_no_principal(self): opts = self.opts_sspi.copy() del opts['proxy']['principal'] - with self.assertRaises(salt.exceptions.InvalidConfigError) as \ - excinfo: - esxdatacenter.init(opts) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxdatacenter.init(opts) self.assertEqual(excinfo.exception.strerror, 'Mechanism is set to \'sspi\', but no ' '\'principal\' key found in proxy config.') @@ -120,17 +144,21 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_find_credentials(self): mock_find_credentials = MagicMock(return_value=('fake_username', 'fake_password')) - with patch('salt.proxy.esxdatacenter.find_credentials', - mock_find_credentials): - esxdatacenter.init(self.opts_userpass) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=self.opts_userpass['proxy'])): + with patch('salt.proxy.esxdatacenter.find_credentials', + mock_find_credentials): + esxdatacenter.init(self.opts_userpass) mock_find_credentials.assert_called_once_with() def test_details_userpass(self): mock_find_credentials = MagicMock(return_value=('fake_username', 'fake_password')) - with patch('salt.proxy.esxdatacenter.find_credentials', - mock_find_credentials): - esxdatacenter.init(self.opts_userpass) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=self.opts_userpass['proxy'])): + with patch('salt.proxy.esxdatacenter.find_credentials', + mock_find_credentials): + esxdatacenter.init(self.opts_userpass) self.assertDictEqual(esxdatacenter.DETAILS, {'vcenter': 'fake_vcenter', 'datacenter': 'fake_dc', From 847710c4331399dcc54cedd127dbe5f17af7d9e2 Mon Sep 17 00:00:00 2001 From: Kunal Ajay Bajpai Date: Wed, 13 Sep 2017 11:55:09 +0530 Subject: [PATCH 052/633] Remove redundant check and add try/except --- salt/utils/job.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/utils/job.py b/salt/utils/job.py index 8fe2167612..ad40213848 100644 --- a/salt/utils/job.py +++ b/salt/utils/job.py @@ -99,9 +99,10 @@ def store_job(opts, load, event=None, mminion=None): log.error(emsg) raise KeyError(emsg) - if 'jid' in load \ - and getfstr in mminion.returners: + try: mminion.returners[savefstr](load['jid'], load) + except KeyError as e: + log.error("Load does not contain 'jid': %s", e) mminion.returners[fstr](load) if (opts.get('job_cache_store_endtime') From b861d5e85ea925b360500edb25afb0a6a75d2f71 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 13 Sep 2017 05:06:58 -0400 Subject: [PATCH 053/633] pylint --- tests/unit/proxy/test_esxdatacenter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/proxy/test_esxdatacenter.py b/tests/unit/proxy/test_esxdatacenter.py index ea1658d5ac..bda93182af 100644 --- a/tests/unit/proxy/test_esxdatacenter.py +++ b/tests/unit/proxy/test_esxdatacenter.py @@ -71,7 +71,7 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(esxdatacenter.__pillar__, {'proxy': mock_pillar_proxy}): with patch('salt.proxy.esxdatacenter.merge', mock_merge): - esxdatacenter.init(opts={'proxy': mock_opts_proxy}) + esxdatacenter.init(opts={'proxy': mock_opts_proxy}) mock_merge.assert_called_once_with(mock_opts_proxy, mock_pillar_proxy) def test_esxdatacenter_schema(self): From ade3f9ad97fca2997b45506191ffc35a87f10e5a Mon Sep 17 00:00:00 2001 From: Joaquin Veira Date: Wed, 13 Sep 2017 14:01:38 +0200 Subject: [PATCH 054/633] Update zabbix_return.py Applied suggested changes --- salt/returners/zabbix_return.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/returners/zabbix_return.py b/salt/returners/zabbix_return.py index 2415f37514..a78f784c56 100644 --- a/salt/returners/zabbix_return.py +++ b/salt/returners/zabbix_return.py @@ -26,6 +26,7 @@ import os # Import Salt libs from salt.ext import six +import salt.utils.files # Get logging started log = logging.getLogger(__name__) @@ -55,7 +56,7 @@ def zbx(): def zabbix_send(key, host, output): - f = open('/etc/zabbix/zabbix_agentd.conf','r') + f = open(zbx()['zabbix_config'],'r') for line in f: if "ServerActive" in line: flag = "true" From 914c9f4a16690e394ed9b299eac34ea542b178c8 Mon Sep 17 00:00:00 2001 From: Cenk Alti Date: Thu, 10 Aug 2017 11:19:40 +0300 Subject: [PATCH 055/633] Yield timed out minions from LocalClient.cmd_iter Fixes #42711 --- salt/client/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/client/__init__.py b/salt/client/__init__.py index b047e59936..f11ccfd5fa 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -883,6 +883,7 @@ class LocalClient(object): else: if kwargs.get(u'yield_pub_data'): yield pub_data + kwargs.setdefault('expect_minions', True) for fn_ret in self.get_iter_returns(pub_data[u'jid'], pub_data[u'minions'], timeout=self._get_timeout(timeout), From 21c11d07aa581f87bceb6c8fb1676ee2ac12267a Mon Sep 17 00:00:00 2001 From: Cenk Alti Date: Wed, 6 Sep 2017 15:16:36 +0300 Subject: [PATCH 056/633] Add yield_all_minions flag to cmd_iter and cmd_iter_no_block --- salt/client/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/salt/client/__init__.py b/salt/client/__init__.py index f11ccfd5fa..5d83db7609 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -837,6 +837,7 @@ class LocalClient(object): tgt_type=u'glob', ret=u'', kwarg=None, + yield_all_minions=True, **kwargs): ''' Yields the individual minion returns as they come in @@ -883,7 +884,8 @@ class LocalClient(object): else: if kwargs.get(u'yield_pub_data'): yield pub_data - kwargs.setdefault('expect_minions', True) + if yield_all_minions: + kwargs['expect_minions'] = True for fn_ret in self.get_iter_returns(pub_data[u'jid'], pub_data[u'minions'], timeout=self._get_timeout(timeout), @@ -909,6 +911,7 @@ class LocalClient(object): kwarg=None, show_jid=False, verbose=False, + yield_all_minions=True, **kwargs): ''' Yields the individual minion returns as they come in, or None @@ -958,6 +961,8 @@ class LocalClient(object): if not pub_data: yield pub_data else: + if yield_all_minions: + kwargs['expect_minions'] = True for fn_ret in self.get_iter_returns(pub_data[u'jid'], pub_data[u'minions'], timeout=timeout, From 85e13b0004217e6ff2352da84e3b8122f3bb5406 Mon Sep 17 00:00:00 2001 From: Cenk Alti Date: Wed, 13 Sep 2017 16:05:22 +0300 Subject: [PATCH 057/633] Revert "Add yield_all_minions flag to cmd_iter and cmd_iter_no_block" This reverts commit 21c11d07aa581f87bceb6c8fb1676ee2ac12267a. --- salt/client/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/salt/client/__init__.py b/salt/client/__init__.py index 5d83db7609..f11ccfd5fa 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -837,7 +837,6 @@ class LocalClient(object): tgt_type=u'glob', ret=u'', kwarg=None, - yield_all_minions=True, **kwargs): ''' Yields the individual minion returns as they come in @@ -884,8 +883,7 @@ class LocalClient(object): else: if kwargs.get(u'yield_pub_data'): yield pub_data - if yield_all_minions: - kwargs['expect_minions'] = True + kwargs.setdefault('expect_minions', True) for fn_ret in self.get_iter_returns(pub_data[u'jid'], pub_data[u'minions'], timeout=self._get_timeout(timeout), @@ -911,7 +909,6 @@ class LocalClient(object): kwarg=None, show_jid=False, verbose=False, - yield_all_minions=True, **kwargs): ''' Yields the individual minion returns as they come in, or None @@ -961,8 +958,6 @@ class LocalClient(object): if not pub_data: yield pub_data else: - if yield_all_minions: - kwargs['expect_minions'] = True for fn_ret in self.get_iter_returns(pub_data[u'jid'], pub_data[u'minions'], timeout=timeout, From 8fa33a06de9f1275ea34e45452cb0bce25ad46b9 Mon Sep 17 00:00:00 2001 From: Cenk Alti Date: Wed, 13 Sep 2017 16:05:28 +0300 Subject: [PATCH 058/633] Revert "Yield timed out minions from LocalClient.cmd_iter" This reverts commit 914c9f4a16690e394ed9b299eac34ea542b178c8. --- salt/client/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/salt/client/__init__.py b/salt/client/__init__.py index f11ccfd5fa..b047e59936 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -883,7 +883,6 @@ class LocalClient(object): else: if kwargs.get(u'yield_pub_data'): yield pub_data - kwargs.setdefault('expect_minions', True) for fn_ret in self.get_iter_returns(pub_data[u'jid'], pub_data[u'minions'], timeout=self._get_timeout(timeout), From a320f2f154062360c4a93c15096ba79dcb9ef80d Mon Sep 17 00:00:00 2001 From: Cenk Alti Date: Wed, 13 Sep 2017 16:09:52 +0300 Subject: [PATCH 059/633] Clarify cmd_iter behavior in docs. --- salt/client/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/salt/client/__init__.py b/salt/client/__init__.py index b047e59936..da32b4181d 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -844,6 +844,10 @@ class LocalClient(object): The function signature is the same as :py:meth:`cmd` with the following exceptions. + Normally :py:meth:`cmd_iter` does not yield results for minions that + are not connected. If you want it to return results for disconnected + minions set `expect_minions=True` in `kwargs`. + :return: A generator yielding the individual minion returns .. code-block:: python From 34b6c3b65fc448d22270f46a214f3636a24608f5 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 28 Aug 2017 19:33:06 -0500 Subject: [PATCH 060/633] Un-deprecate passing kwargs outside of 'kwarg' param --- salt/client/mixins.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/salt/client/mixins.py b/salt/client/mixins.py index f5a29a9cbf..bd69d269bf 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -359,29 +359,20 @@ class SyncClientMixin(object): # packed into the top level object. The plan is to move away from # that since the caller knows what is an arg vs a kwarg, but while # we make the transition we will load "kwargs" using format_call if - # there are no kwargs in the low object passed in - f_call = None - if 'arg' not in low: - f_call = salt.utils.format_call( + # there are no kwargs in the low object passed in. + f_call = {} if 'arg' in low and 'kwarg' in low \ + else salt.utils.format_call( self.functions[fun], low, expected_extra_kws=CLIENT_INTERNAL_KEYWORDS ) - args = f_call.get('args', ()) - else: - args = low['arg'] - if 'kwarg' not in low: - log.critical( - 'kwargs must be passed inside the low data within the ' - '\'kwarg\' key. See usage of ' - 'salt.utils.args.parse_input() and ' - 'salt.minion.load_args_and_kwargs() elsewhere in the ' - 'codebase.' - ) - kwargs = {} - else: - kwargs = low['kwarg'] + args = f_call.get('args', ()) \ + if 'arg' not in low \ + else low['arg'] + kwargs = f_call.get('kwargs', {}) \ + if 'kwarg' not in low \ + else low['kwarg'] # Update the event data with loaded args and kwargs data['fun_args'] = list(args) + ([kwargs] if kwargs else []) From 9db3f5ae6dbbf3c616875e8bc16ece6557de99de Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 31 Aug 2017 00:24:11 -0500 Subject: [PATCH 061/633] Unify reactor configuration, fix caller reactors There are 4 types of reactor jobs, and 3 different config schemas for passing arguments: 1. local - positional and keyword args passed in arg/kwarg params, respectively. 2. runner/wheel - passed as individual params directly under the function name. 3. caller - only positional args supported, passed under an "args" param. In addition to being wildly inconsistent, there are several problems with each of the above approaches: - For local jobs, having to know which are positional and keyword arguments is not user-friendly. - For runner/wheel jobs, the fact that the arguments are all passed in the level directly below the function name means that they are dumped directly into the low chunk. This means that if any arguments are passed which conflict with the reserved keywords in the low chunk (name, order, etc.), they will override their counterparts in the low chunk, which may make the Reactor behave unpredictably. To solve these issues, this commit makes the following changes: 1. A new, unified configuration schema has been added, so that arguments are passed identically across all types of reactions. In this new schema, all arguments are passed as named arguments underneath an "args" parameter. Those named arguments are then passed as keyword arguments to the desired function. This works even for positional arguments because Python will automagically pass a keyword argument as its positional counterpart when the name of a positional argument is found in the kwargs. 2. The caller jobs now support both positional and keyword arguments. Backward-compatibility with the old configuration schema has been preserved, so old Reactor SLS files do not break. In addition, you've probably already said to yourself "Hey, caller jobs were _already_ passing their arguments under an "args" param. What gives?" Well, using the old config schema, only positional arguments were supported. So if we detect a list of positional arguments, we treat the input as positional arguments (i.e. old schema), while if the input is a dictionary (or "dictlist"), we treat the input as kwargs (i.e. new schema). --- salt/utils/reactor.py | 231 +++++++++++++++++++++++++++++------------- 1 file changed, 159 insertions(+), 72 deletions(-) diff --git a/salt/utils/reactor.py b/salt/utils/reactor.py index 57c4fd0863..36971f5c36 100644 --- a/salt/utils/reactor.py +++ b/salt/utils/reactor.py @@ -7,12 +7,14 @@ import glob import logging # Import salt libs +import salt.client import salt.runner import salt.state import salt.utils import salt.utils.cache import salt.utils.event import salt.utils.process +import salt.wheel import salt.defaults.exitcodes # Import 3rd-party libs @@ -21,6 +23,15 @@ import salt.ext.six as six log = logging.getLogger(__name__) +REACTOR_INTERNAL_KEYWORDS = frozenset([ + '__id__', + '__sls__', + 'name', + 'order', + 'fun', + 'state', +]) + class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.state.Compiler): ''' @@ -29,6 +40,10 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat The reactor has the capability to execute pre-programmed executions as reactions to events ''' + aliases = { + 'cmd': 'local', + } + def __init__(self, opts, log_queue=None): super(Reactor, self).__init__(log_queue=log_queue) local_minion_opts = opts.copy() @@ -171,6 +186,16 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat return {'status': False, 'comment': 'Reactor does not exists.'} + def resolve_aliases(self, chunks): + ''' + Preserve backward compatibility by rewriting the 'state' key in the low + chunks if it is using a legacy type. + ''' + for idx, _ in enumerate(chunks): + new_state = self.aliases.get(chunks[idx]['state']) + if new_state is not None: + chunks[idx]['state'] = new_state + def reactions(self, tag, data, reactors): ''' Render a list of reactor files and returns a reaction struct @@ -191,6 +216,7 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat except Exception as exc: log.error('Exception trying to compile reactions: {0}'.format(exc), exc_info=True) + self.resolve_aliases(chunks) return chunks def call_reactions(self, chunks): @@ -248,12 +274,19 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat class ReactWrap(object): ''' - Create a wrapper that executes low data for the reaction system + Wrapper that executes low data for the Reactor System ''' # class-wide cache of clients client_cache = None event_user = 'Reactor' + reaction_class = { + 'local': salt.client.LocalClient, + 'runner': salt.runner.RunnerClient, + 'wheel': salt.wheel.Wheel, + 'caller': salt.client.Caller, + } + def __init__(self, opts): self.opts = opts if ReactWrap.client_cache is None: @@ -264,21 +297,49 @@ class ReactWrap(object): queue_size=self.opts['reactor_worker_hwm'] # queue size for those workers ) + def populate_client_cache(self, low): + ''' + Populate the client cache with an instance of the specified type + ''' + reaction_type = low['state'] + if reaction_type not in self.client_cache: + log.debug('Reactor is populating %s client cache', reaction_type) + if reaction_type in ('runner', 'wheel'): + # Reaction types that run locally on the master want the full + # opts passed. + self.client_cache[reaction_type] = \ + self.reaction_class[reaction_type](self.opts) + # The len() function will cause the module functions to load if + # they aren't already loaded. We want to load them so that the + # spawned threads don't need to load them. Loading in the + # spawned threads creates race conditions such as sometimes not + # finding the required function because another thread is in + # the middle of loading the functions. + len(self.client_cache[reaction_type].functions) + else: + # Reactions which use remote pubs only need the conf file when + # instantiating a client instance. + self.client_cache[reaction_type] = \ + self.reaction_class[reaction_type](self.opts['conf_file']) + def run(self, low): ''' - Execute the specified function in the specified state by passing the - low data + Execute a reaction by invoking the proper wrapper func ''' - l_fun = getattr(self, low['state']) + self.populate_client_cache(low) try: - f_call = salt.utils.format_call(l_fun, low) - kwargs = f_call.get('kwargs', {}) - if 'arg' not in kwargs: - kwargs['arg'] = [] - if 'kwarg' not in kwargs: - kwargs['kwarg'] = {} + l_fun = getattr(self, low['state']) + except AttributeError: + log.error( + 'ReactWrap is missing a wrapper function for \'%s\'', + low['state'] + ) - # TODO: Setting the user doesn't seem to work for actual remote publishes + try: + wrap_call = salt.utils.format_call(l_fun, low) + args = wrap_call.get('args', ()) + kwargs = wrap_call.get('kwargs', {}) + # TODO: Setting user doesn't seem to work for actual remote pubs if low['state'] in ('runner', 'wheel'): # Update called function's low data with event user to # segregate events fired by reactor and avoid reaction loops @@ -286,80 +347,106 @@ class ReactWrap(object): # Replace ``state`` kwarg which comes from high data compiler. # It breaks some runner functions and seems unnecessary. kwargs['__state__'] = kwargs.pop('state') + # NOTE: if any additional keys are added here, they will also + # need to be added to filter_kwargs() - l_fun(*f_call.get('args', ()), **kwargs) + if 'args' in kwargs: + # New configuration + reactor_args = kwargs.pop('args') + for item in ('arg', 'kwarg'): + if item in low: + log.warning( + 'Reactor \'%s\' is ignoring \'%s\' param %s due to ' + 'presence of \'args\' param. Check the Reactor System ' + 'documentation for the correct argument format.', + low['__id__'], item, low[item] + ) + if low['state'] == 'caller' \ + and isinstance(reactor_args, list) \ + and not salt.utils.is_dictlist(reactor_args): + # Legacy 'caller' reactors were already using the 'args' + # param, but only supported a list of positional arguments. + # If low['args'] is a list but is *not* a dictlist, then + # this is actually using the legacy configuration. So, put + # the reactor args into kwarg['arg'] so that the wrapper + # interprets them as positional args. + kwargs['arg'] = reactor_args + kwargs['kwarg'] = {} + else: + kwargs['arg'] = () + kwargs['kwarg'] = reactor_args + if not isinstance(kwargs['kwarg'], dict): + kwargs['kwarg'] = salt.utils.repack_dictlist(kwargs['kwarg']) + if not kwargs['kwarg']: + log.error( + 'Reactor \'%s\' failed to execute %s \'%s\': ' + 'Incorrect argument format, check the Reactor System ' + 'documentation for the correct format.', + low['__id__'], low['state'], low['fun'] + ) + return + else: + # Legacy configuration + react_call = {} + if low['state'] in ('runner', 'wheel'): + if 'arg' not in kwargs or 'kwarg' not in kwargs: + # Runner/wheel execute on the master, so we can use + # format_call to get the functions args/kwargs + react_fun = self.client_cache[low['state']].functions.get(low['fun']) + if react_fun is None: + log.error( + 'Reactor \'%s\' failed to execute %s \'%s\': ' + 'function not available', + low['__id__'], low['state'], low['fun'] + ) + return + + react_call = salt.utils.format_call( + react_fun, + low, + expected_extra_kws=REACTOR_INTERNAL_KEYWORDS + ) + + if 'arg' not in kwargs: + kwargs['arg'] = react_call.get('args', ()) + if 'kwarg' not in kwargs: + kwargs['kwarg'] = react_call.get('kwargs', {}) + + # Execute the wrapper with the proper args/kwargs. kwargs['arg'] + # and kwargs['kwarg'] contain the positional and keyword arguments + # that will be passed to the client interface to execute the + # desired runner/wheel/remote-exec/etc. function. + l_fun(*args, **kwargs) + except SystemExit: + log.warning( + 'Reactor \'%s\' attempted to exit. Ignored.', low['__id__'] + ) except Exception: log.error( - 'Failed to execute {0}: {1}\n'.format(low['state'], l_fun), - exc_info=True - ) - - def local(self, *args, **kwargs): - ''' - Wrap LocalClient for running :ref:`execution modules ` - ''' - if 'local' not in self.client_cache: - self.client_cache['local'] = salt.client.LocalClient(self.opts['conf_file']) - try: - self.client_cache['local'].cmd_async(*args, **kwargs) - except SystemExit: - log.warning('Attempt to exit reactor. Ignored.') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) - - cmd = local + 'Reactor \'%s\' failed to execute %s \'%s\'', + low['__id__'], low['state'], low['fun'], exc_info=True + ) def runner(self, fun, **kwargs): ''' Wrap RunnerClient for executing :ref:`runner modules ` ''' - if 'runner' not in self.client_cache: - self.client_cache['runner'] = salt.runner.RunnerClient(self.opts) - # The len() function will cause the module functions to load if - # they aren't already loaded. We want to load them so that the - # spawned threads don't need to load them. Loading in the spawned - # threads creates race conditions such as sometimes not finding - # the required function because another thread is in the middle - # of loading the functions. - len(self.client_cache['runner'].functions) - try: - self.pool.fire_async(self.client_cache['runner'].low, args=(fun, kwargs)) - except SystemExit: - log.warning('Attempt to exit in reactor by runner. Ignored') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) + self.pool.fire_async(self.client_cache['runner'].low, args=(fun, kwargs)) def wheel(self, fun, **kwargs): ''' Wrap Wheel to enable executing :ref:`wheel modules ` ''' - if 'wheel' not in self.client_cache: - self.client_cache['wheel'] = salt.wheel.Wheel(self.opts) - # The len() function will cause the module functions to load if - # they aren't already loaded. We want to load them so that the - # spawned threads don't need to load them. Loading in the spawned - # threads creates race conditions such as sometimes not finding - # the required function because another thread is in the middle - # of loading the functions. - len(self.client_cache['wheel'].functions) - try: - self.pool.fire_async(self.client_cache['wheel'].low, args=(fun, kwargs)) - except SystemExit: - log.warning('Attempt to in reactor by whell. Ignored.') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) + self.pool.fire_async(self.client_cache['wheel'].low, args=(fun, kwargs)) - def caller(self, fun, *args, **kwargs): + def local(self, fun, tgt, **kwargs): ''' - Wrap Caller to enable executing :ref:`caller modules ` + Wrap LocalClient for running :ref:`execution modules ` ''' - log.debug("in caller with fun {0} args {1} kwargs {2}".format(fun, args, kwargs)) - args = kwargs.get('args', []) - if 'caller' not in self.client_cache: - self.client_cache['caller'] = salt.client.Caller(self.opts['conf_file']) - try: - self.client_cache['caller'].function(fun, *args) - except SystemExit: - log.warning('Attempt to exit reactor. Ignored.') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) + self.client_cache['local'].cmd_async(tgt, fun, **kwargs) + + def caller(self, fun, **kwargs): + ''' + Wrap LocalCaller to execute remote exec functions locally on the Minion + ''' + self.client_cache['caller'].cmd(fun, *kwargs['arg'], **kwargs['kwarg']) From 4243a2211d1c2350e72382ee5fb823a4b441ef9f Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 31 Aug 2017 23:23:41 -0500 Subject: [PATCH 062/633] Rewrite the reactor unit tests These have been skipped for a while now because they didn't work correctly. The old tests have been scrapped in favor of new ones that test both the old and new config schema. --- tests/unit/utils/test_reactor.py | 602 ++++++++++++++++++++++++++++--- 1 file changed, 542 insertions(+), 60 deletions(-) diff --git a/tests/unit/utils/test_reactor.py b/tests/unit/utils/test_reactor.py index 7a96900977..5c86f766b4 100644 --- a/tests/unit/utils/test_reactor.py +++ b/tests/unit/utils/test_reactor.py @@ -1,74 +1,556 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -import time -import shutil -import tempfile +import codecs +import glob +import logging import os - -from contextlib import contextmanager +import textwrap +import yaml import salt.utils -from salt.utils.process import clean_proc +import salt.loader import salt.utils.reactor as reactor -from tests.integration import AdaptedConfigurationTestCaseMixin -from tests.support.paths import TMP from tests.support.unit import TestCase, skipIf -from tests.support.mock import patch, MagicMock +from tests.support.mixins import AdaptedConfigurationTestCaseMixin +from tests.support.mock import ( + NO_MOCK, + NO_MOCK_REASON, + patch, + MagicMock, + Mock, + mock_open, +) + +REACTOR_CONFIG = '''\ +reactor: + - old_runner: + - /srv/reactor/old_runner.sls + - old_wheel: + - /srv/reactor/old_wheel.sls + - old_local: + - /srv/reactor/old_local.sls + - old_cmd: + - /srv/reactor/old_cmd.sls + - old_caller: + - /srv/reactor/old_caller.sls + - new_runner: + - /srv/reactor/new_runner.sls + - new_wheel: + - /srv/reactor/new_wheel.sls + - new_local: + - /srv/reactor/new_local.sls + - new_cmd: + - /srv/reactor/new_cmd.sls + - new_caller: + - /srv/reactor/new_caller.sls +''' + +REACTOR_DATA = { + 'runner': {'data': {'message': 'This is an error'}}, + 'wheel': {'data': {'id': 'foo'}}, + 'local': {'data': {'pkg': 'zsh', 'repo': 'updates'}}, + 'cmd': {'data': {'pkg': 'zsh', 'repo': 'updates'}}, + 'caller': {'data': {'path': '/tmp/foo'}}, +} + +SLS = { + '/srv/reactor/old_runner.sls': textwrap.dedent('''\ + raise_error: + runner.error.error: + - name: Exception + - message: {{ data['data']['message'] }} + '''), + '/srv/reactor/old_wheel.sls': textwrap.dedent('''\ + remove_key: + wheel.key.delete: + - match: {{ data['data']['id'] }} + '''), + '/srv/reactor/old_local.sls': textwrap.dedent('''\ + install_zsh: + local.state.single: + - tgt: test + - arg: + - pkg.installed + - {{ data['data']['pkg'] }} + - kwarg: + fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/old_cmd.sls': textwrap.dedent('''\ + install_zsh: + cmd.state.single: + - tgt: test + - arg: + - pkg.installed + - {{ data['data']['pkg'] }} + - kwarg: + fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/old_caller.sls': textwrap.dedent('''\ + touch_file: + caller.file.touch: + - args: + - {{ data['data']['path'] }} + '''), + '/srv/reactor/new_runner.sls': textwrap.dedent('''\ + raise_error: + runner.error.error: + - args: + - name: Exception + - message: {{ data['data']['message'] }} + '''), + '/srv/reactor/new_wheel.sls': textwrap.dedent('''\ + remove_key: + wheel.key.delete: + - args: + - match: {{ data['data']['id'] }} + '''), + '/srv/reactor/new_local.sls': textwrap.dedent('''\ + install_zsh: + local.state.single: + - tgt: test + - args: + - fun: pkg.installed + - name: {{ data['data']['pkg'] }} + - fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/new_cmd.sls': textwrap.dedent('''\ + install_zsh: + cmd.state.single: + - tgt: test + - args: + - fun: pkg.installed + - name: {{ data['data']['pkg'] }} + - fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/new_caller.sls': textwrap.dedent('''\ + touch_file: + caller.file.touch: + - args: + - name: {{ data['data']['path'] }} + '''), +} + +LOW_CHUNKS = { + # Note that the "name" value in the chunk has been overwritten by the + # "name" argument in the SLS. This is one reason why the new schema was + # needed. + 'old_runner': [{ + 'state': 'runner', + '__id__': 'raise_error', + '__sls__': '/srv/reactor/old_runner.sls', + 'order': 1, + 'fun': 'error.error', + 'name': 'Exception', + 'message': 'This is an error', + }], + 'old_wheel': [{ + 'state': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/old_wheel.sls', + 'order': 1, + 'fun': 'key.delete', + 'match': 'foo', + }], + 'old_local': [{ + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_local.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }], + 'old_cmd': [{ + 'state': 'local', # 'cmd' should be aliased to 'local' + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_cmd.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }], + 'old_caller': [{ + 'state': 'caller', + '__id__': 'touch_file', + 'name': 'touch_file', + '__sls__': '/srv/reactor/old_caller.sls', + 'order': 1, + 'fun': 'file.touch', + 'args': ['/tmp/foo'], + }], + 'new_runner': [{ + 'state': 'runner', + '__id__': 'raise_error', + 'name': 'raise_error', + '__sls__': '/srv/reactor/new_runner.sls', + 'order': 1, + 'fun': 'error.error', + 'args': [ + {'name': 'Exception'}, + {'message': 'This is an error'}, + ], + }], + 'new_wheel': [{ + 'state': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/new_wheel.sls', + 'order': 1, + 'fun': 'key.delete', + 'args': [ + {'match': 'foo'}, + ], + }], + 'new_local': [{ + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_local.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'args': [ + {'fun': 'pkg.installed'}, + {'name': 'zsh'}, + {'fromrepo': 'updates'}, + ], + }], + 'new_cmd': [{ + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_cmd.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'args': [ + {'fun': 'pkg.installed'}, + {'name': 'zsh'}, + {'fromrepo': 'updates'}, + ], + }], + 'new_caller': [{ + 'state': 'caller', + '__id__': 'touch_file', + 'name': 'touch_file', + '__sls__': '/srv/reactor/new_caller.sls', + 'order': 1, + 'fun': 'file.touch', + 'args': [ + {'name': '/tmp/foo'}, + ], + }], +} + +WRAPPER_CALLS = { + 'old_runner': ( + 'error.error', + { + '__state__': 'runner', + '__id__': 'raise_error', + '__sls__': '/srv/reactor/old_runner.sls', + '__user__': 'Reactor', + 'order': 1, + 'arg': [], + 'kwarg': { + 'name': 'Exception', + 'message': 'This is an error', + }, + 'name': 'Exception', + 'message': 'This is an error', + }, + ), + 'old_wheel': ( + 'key.delete', + { + '__state__': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/old_wheel.sls', + 'order': 1, + '__user__': 'Reactor', + 'arg': ['foo'], + 'kwarg': {}, + 'match': 'foo', + }, + ), + 'old_local': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_local.sls', + 'order': 1, + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }, + }, + 'old_cmd': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_cmd.sls', + 'order': 1, + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }, + }, + 'old_caller': { + 'args': ('file.touch', '/tmp/foo'), + 'kwargs': {}, + }, + 'new_runner': ( + 'error.error', + { + '__state__': 'runner', + '__id__': 'raise_error', + 'name': 'raise_error', + '__sls__': '/srv/reactor/new_runner.sls', + '__user__': 'Reactor', + 'order': 1, + 'arg': (), + 'kwarg': { + 'name': 'Exception', + 'message': 'This is an error', + }, + }, + ), + 'new_wheel': ( + 'key.delete', + { + '__state__': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/new_wheel.sls', + 'order': 1, + '__user__': 'Reactor', + 'arg': (), + 'kwarg': {'match': 'foo'}, + }, + ), + 'new_local': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_local.sls', + 'order': 1, + 'arg': (), + 'kwarg': { + 'fun': 'pkg.installed', + 'name': 'zsh', + 'fromrepo': 'updates', + }, + }, + }, + 'new_cmd': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_cmd.sls', + 'order': 1, + 'arg': (), + 'kwarg': { + 'fun': 'pkg.installed', + 'name': 'zsh', + 'fromrepo': 'updates', + }, + }, + }, + 'new_caller': { + 'args': ('file.touch',), + 'kwargs': {'name': '/tmp/foo'}, + }, +} + +log = logging.getLogger(__name__) -@contextmanager -def reactor_process(opts, reactor): - opts = dict(opts) - opts['reactor'] = reactor - proc = reactor.Reactor(opts) - proc.start() - try: - if os.environ.get('TRAVIS_PYTHON_VERSION', None) is not None: - # Travis is slow - time.sleep(10) - else: - time.sleep(2) - yield - finally: - clean_proc(proc) - - -def _args_sideffect(*args, **kwargs): - return args, kwargs - - -@skipIf(True, 'Skipping until its clear what and how is this supposed to be testing') +@skipIf(NO_MOCK, NO_MOCK_REASON) class TestReactor(TestCase, AdaptedConfigurationTestCaseMixin): - def setUp(self): - self.opts = self.get_temp_config('master') - self.tempdir = tempfile.mkdtemp(dir=TMP) - self.sls_name = os.path.join(self.tempdir, 'test.sls') - with salt.utils.fopen(self.sls_name, 'w') as fh: - fh.write(''' -update_fileserver: - runner.fileserver.update -''') + ''' + Tests for constructing the low chunks to be executed via the Reactor + ''' + @classmethod + def setUpClass(cls): + ''' + Load the reactor config for mocking + ''' + cls.opts = cls.get_temp_config('master') + reactor_config = yaml.safe_load(REACTOR_CONFIG) + cls.opts.update(reactor_config) + cls.reactor = reactor.Reactor(cls.opts) + cls.reaction_map = salt.utils.repack_dictlist(reactor_config['reactor']) + renderers = salt.loader.render(cls.opts, {}) + cls.render_pipe = [(renderers[x], '') for x in ('jinja', 'yaml')] - def tearDown(self): - if os.path.isdir(self.tempdir): - shutil.rmtree(self.tempdir) - del self.opts - del self.tempdir - del self.sls_name + @classmethod + def tearDownClass(cls): + del cls.opts + del cls.reactor + del cls.render_pipe - def test_basic(self): - reactor_config = [ - {'salt/tagA': ['/srv/reactor/A.sls']}, - {'salt/tagB': ['/srv/reactor/B.sls']}, - {'*': ['/srv/reactor/all.sls']}, - ] - wrap = reactor.ReactWrap(self.opts) - with patch.object(reactor.ReactWrap, 'local', MagicMock(side_effect=_args_sideffect)): - ret = wrap.run({'fun': 'test.ping', - 'state': 'local', - 'order': 1, - 'name': 'foo_action', - '__id__': 'foo_action'}) - raise Exception(ret) + def test_list_reactors(self): + ''' + Ensure that list_reactors() returns the correct list of reactor SLS + files for each tag. + ''' + for schema in ('old', 'new'): + for rtype in REACTOR_DATA: + tag = '_'.join((schema, rtype)) + self.assertEqual( + self.reactor.list_reactors(tag), + self.reaction_map[tag] + ) + + def test_reactions(self): + ''' + Ensure that the correct reactions are built from the configured SLS + files and tag data. + ''' + for schema in ('old', 'new'): + for rtype in REACTOR_DATA: + tag = '_'.join((schema, rtype)) + log.debug('test_reactions: processing %s', tag) + reactors = self.reactor.list_reactors(tag) + log.debug('test_reactions: %s reactors: %s', tag, reactors) + # No globbing in our example SLS, and the files don't actually + # exist, so mock glob.glob to just return back the path passed + # to it. + with patch.object( + glob, + 'glob', + MagicMock(side_effect=lambda x: [x])): + # The below four mocks are all so that + # salt.template.compile_template() will read the templates + # we've mocked up in the SLS global variable above. + with patch.object( + os.path, 'isfile', + MagicMock(return_value=True)): + with patch.object( + salt.utils, 'is_empty', + MagicMock(return_value=False)): + with patch.object( + codecs, 'open', + mock_open(read_data=SLS[reactors[0]])): + with patch.object( + salt.template, 'template_shebang', + MagicMock(return_value=self.render_pipe)): + reactions = self.reactor.reactions( + tag, + REACTOR_DATA[rtype], + reactors, + ) + log.debug( + 'test_reactions: %s reactions: %s', + tag, reactions + ) + self.assertEqual(reactions, LOW_CHUNKS[tag]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class TestReactWrap(TestCase, AdaptedConfigurationTestCaseMixin): + ''' + Tests that we are formulating the wrapper calls properly + ''' + @classmethod + def setUpClass(cls): + cls.wrap = reactor.ReactWrap(cls.get_temp_config('master')) + + @classmethod + def tearDownClass(cls): + del cls.wrap + + def test_runner(self): + ''' + Test runner reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'runner')) + chunk = LOW_CHUNKS[tag][0] + thread_pool = Mock() + thread_pool.fire_async = Mock() + with patch.object(self.wrap, 'pool', thread_pool): + self.wrap.run(chunk) + thread_pool.fire_async.assert_called_with( + self.wrap.client_cache['runner'].low, + args=WRAPPER_CALLS[tag] + ) + + def test_wheel(self): + ''' + Test wheel reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'wheel')) + chunk = LOW_CHUNKS[tag][0] + thread_pool = Mock() + thread_pool.fire_async = Mock() + with patch.object(self.wrap, 'pool', thread_pool): + self.wrap.run(chunk) + thread_pool.fire_async.assert_called_with( + self.wrap.client_cache['wheel'].low, + args=WRAPPER_CALLS[tag] + ) + + def test_local(self): + ''' + Test local reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'local')) + chunk = LOW_CHUNKS[tag][0] + client_cache = {'local': Mock()} + client_cache['local'].cmd_async = Mock() + with patch.object(self.wrap, 'client_cache', client_cache): + self.wrap.run(chunk) + client_cache['local'].cmd_async.assert_called_with( + *WRAPPER_CALLS[tag]['args'], + **WRAPPER_CALLS[tag]['kwargs'] + ) + + def test_cmd(self): + ''' + Test cmd reactions (alias for 'local') using both the old and new + config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'cmd')) + chunk = LOW_CHUNKS[tag][0] + client_cache = {'local': Mock()} + client_cache['local'].cmd_async = Mock() + with patch.object(self.wrap, 'client_cache', client_cache): + self.wrap.run(chunk) + client_cache['local'].cmd_async.assert_called_with( + *WRAPPER_CALLS[tag]['args'], + **WRAPPER_CALLS[tag]['kwargs'] + ) + + def test_caller(self): + ''' + Test caller reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'caller')) + chunk = LOW_CHUNKS[tag][0] + client_cache = {'caller': Mock()} + client_cache['caller'].cmd = Mock() + with patch.object(self.wrap, 'client_cache', client_cache): + self.wrap.run(chunk) + client_cache['caller'].cmd.assert_called_with( + *WRAPPER_CALLS[tag]['args'], + **WRAPPER_CALLS[tag]['kwargs'] + ) From 20f6f3cc3991074a5d2762493644e5a8bf452126 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 1 Sep 2017 18:35:01 -0500 Subject: [PATCH 063/633] Include a better example for reactor in master conf file --- doc/ref/configuration/master.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/ref/configuration/master.rst b/doc/ref/configuration/master.rst index 976919a343..ecba2b1537 100644 --- a/doc/ref/configuration/master.rst +++ b/doc/ref/configuration/master.rst @@ -4091,7 +4091,9 @@ information. .. code-block:: yaml - reactor: [] + reactor: + - 'salt/minion/*/start': + - salt://reactor/startup_tasks.sls .. conf_master:: reactor_refresh_interval From b85c8510c7650133ebb448c0eed2455f1d31859d Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 1 Sep 2017 18:33:45 -0500 Subject: [PATCH 064/633] Improve the reactor documentation This reorganizes the reactor docs and includes examples of the new reactor SLS config syntax. --- doc/topics/beacons/index.rst | 5 +- doc/topics/reactor/index.rst | 553 ++++++++++++++++++++--------------- 2 files changed, 320 insertions(+), 238 deletions(-) diff --git a/doc/topics/beacons/index.rst b/doc/topics/beacons/index.rst index 6dae8dca09..62991af2f4 100644 --- a/doc/topics/beacons/index.rst +++ b/doc/topics/beacons/index.rst @@ -253,9 +253,8 @@ in ``/etc/salt/master.d/reactor.conf``: .. note:: You can have only one top level ``reactor`` section, so if one already - exists, add this code to the existing section. See :ref:`Understanding the - Structure of Reactor Formulas ` to learn more about - reactor SLS syntax. + exists, add this code to the existing section. See :ref:`here + ` to learn more about reactor SLS syntax. Start the Salt Master in Debug Mode diff --git a/doc/topics/reactor/index.rst b/doc/topics/reactor/index.rst index 2586245a1a..de5df946ac 100644 --- a/doc/topics/reactor/index.rst +++ b/doc/topics/reactor/index.rst @@ -27,9 +27,9 @@ event bus is an open system used for sending information notifying Salt and other systems about operations. The event system fires events with a very specific criteria. Every event has a -:strong:`tag`. Event tags allow for fast top level filtering of events. In -addition to the tag, each event has a data structure. This data structure is a -dict, which contains information about the event. +**tag**. Event tags allow for fast top-level filtering of events. In addition +to the tag, each event has a data structure. This data structure is a +dictionary, which contains information about the event. .. _reactor-mapping-events: @@ -65,15 +65,12 @@ and each event tag has a list of reactor SLS files to be run. the :ref:`querystring syntax ` (e.g. ``salt://reactor/mycustom.sls?saltenv=reactor``). -Reactor sls files are similar to state and pillar sls files. They are -by default yaml + Jinja templates and are passed familiar context variables. +Reactor SLS files are similar to State and Pillar SLS files. They are by +default YAML + Jinja templates and are passed familiar context variables. +Click :ref:`here ` for more detailed information on the +variables availble in Jinja templating. -They differ because of the addition of the ``tag`` and ``data`` variables. - -- The ``tag`` variable is just the tag in the fired event. -- The ``data`` variable is the event's data dict. - -Here is a simple reactor sls: +Here is the SLS for a simple reaction: .. code-block:: jinja @@ -90,71 +87,278 @@ data structure and compiler used for the state system is used for the reactor system. The only difference is that the data is matched up to the salt command API and the runner system. In this example, a command is published to the ``mysql1`` minion with a function of :py:func:`state.apply -`. Similarly, a runner can be called: +`, which performs a :ref:`highstate +`. Similarly, a runner can be called: .. code-block:: jinja {% if data['data']['custom_var'] == 'runit' %} call_runit_orch: runner.state.orchestrate: - - mods: _orch.runit + - args: + - mods: orchestrate.runit {% endif %} This example will execute the state.orchestrate runner and intiate an execution -of the runit orchestrator located at ``/srv/salt/_orch/runit.sls``. Using -``_orch/`` is any arbitrary path but it is recommended to avoid using "orchestrate" -as this is most likely to cause confusion. +of the ``runit`` orchestrator located at ``/srv/salt/orchestrate/runit.sls``. -Writing SLS Files ------------------ +Types of Reactions +================== -Reactor SLS files are stored in the same location as State SLS files. This means -that both ``file_roots`` and ``gitfs_remotes`` impact what SLS files are -available to the reactor and orchestrator. +============================== ================================================================================== +Name Description +============================== ================================================================================== +:ref:`local ` Runs a :ref:`remote-execution function ` on targeted minions +:ref:`runner ` Executes a :ref:`runner function ` +:ref:`wheel ` Executes a :ref:`wheel function ` on the master +:ref:`caller ` Runs a :ref:`remote-execution function ` on a masterless minion +============================== ================================================================================== -It is recommended to keep reactor and orchestrator SLS files in their own uniquely -named subdirectories such as ``_orch/``, ``orch/``, ``_orchestrate/``, ``react/``, -``_reactor/``, etc. Keeping a unique name helps prevent confusion when trying to -read through this a few years down the road. +.. note:: + The ``local`` and ``caller`` reaction types will be renamed for the Oxygen + release. These reaction types were named after Salt's internal client + interfaces, and are not intuitively named. Both ``local`` and ``caller`` + will continue to work in Reactor SLS files, but for the Oxygen release the + documentation will be updated to reflect the new preferred naming. -The Goal of Writing Reactor SLS Files -===================================== +Where to Put Reactor SLS Files +============================== -Reactor SLS files share the familiar syntax from Salt States but there are -important differences. The goal of a Reactor file is to process a Salt event as -quickly as possible and then to optionally start a **new** process in response. +Reactor SLS files can come both from files local to the master, and from any of +backends enabled via the :conf_master:`fileserver_backend` config option. Files +placed in the Salt fileserver can be referenced using a ``salt://`` URL, just +like they can in State SLS files. -1. The Salt Reactor watches Salt's event bus for new events. -2. The event tag is matched against the list of event tags under the - ``reactor`` section in the Salt Master config. -3. The SLS files for any matches are Rendered into a data structure that - represents one or more function calls. -4. That data structure is given to a pool of worker threads for execution. +It is recommended to place reactor and orchestrator SLS files in their own +uniquely-named subdirectories such as ``orch/``, ``orchestrate/``, ``react/``, +``reactor/``, etc., to keep them organized. + +.. _reactor-sls: + +Writing Reactor SLS +=================== + +The different reaction types were developed separately and have historically +had different methods for passing arguments. For the 2017.7.2 release a new, +unified configuration schema has been introduced, which applies to all reaction +types. + +The old config schema will continue to be supported, and there is no plan to +deprecate it at this time. + +.. _reactor-local: + +Local Reactions +--------------- + +A ``local`` reaction runs a :ref:`remote-execution function ` +on the targeted minions. + +The old config schema required the positional and keyword arguments to be +manually separated by the user under ``arg`` and ``kwarg`` parameters. However, +this is not very user-friendly, as it forces the user to distinguish which type +of argument is which, and make sure that positional arguments are ordered +properly. Therefore, the new config schema is recommended if the master is +running a supported release. + +The below two examples are equivalent: + ++---------------------------------+-----------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++=================================+=============================+ +| :: | :: | +| | | +| install_zsh: | install_zsh: | +| local.state.single: | local.state.single: | +| - tgt: 'kernel:Linux' | - tgt: 'kernel:Linux' | +| - tgt_type: grain | - tgt_type: grain | +| - args: | - arg: | +| - fun: pkg.installed | - pkg.installed | +| - name: zsh | - zsh | +| - fromrepo: updates | - kwarg: | +| | fromrepo: updates | ++---------------------------------+-----------------------------+ + +This reaction would be equvalent to running the following Salt command: + +.. code-block:: bash + + salt -G 'kernel:Linux' state.single pkg.installed name=zsh fromrepo=updates + +.. note:: + Any other parameters in the :py:meth:`LocalClient().cmd_async() + ` method can be passed at the same + indentation level as ``tgt``. + +.. note:: + ``tgt_type`` is only required when the target expression defined in ``tgt`` + uses a :ref:`target type ` other than a minion ID glob. + + The ``tgt_type`` argument was named ``expr_form`` in releases prior to + 2017.7.0. + +.. _reactor-runner: + +Runner Reactions +---------------- + +Runner reactions execute :ref:`runner functions ` locally on +the master. + +The old config schema called for passing arguments to the reaction directly +under the name of the runner function. However, this can cause unpredictable +interactions with the Reactor system's internal arguments. It is also possible +to pass positional and keyword arguments under ``arg`` and ``kwarg`` like above +in :ref:`local reactions `, but as noted above this is not very +user-friendly. Therefore, the new config schema is recommended if the master +is running a supported release. + +The below two examples are equivalent: + ++-------------------------------------------------+-------------------------------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++=================================================+=================================================+ +| :: | :: | +| | | +| deploy_app: | deploy_app: | +| runner.state.orchestrate: | runner.state.orchestrate: | +| - args: | - mods: orchestrate.deploy_app | +| - mods: orchestrate.deploy_app | - kwarg: | +| - pillar: | pillar: | +| event_tag: {{ tag }} | event_tag: {{ tag }} | +| event_data: {{ data['data']|json }} | event_data: {{ data['data']|json }} | ++-------------------------------------------------+-------------------------------------------------+ + +Assuming that the event tag is ``foo``, and the data passed to the event is +``{'bar': 'baz'}``, then this reaction is equvalent to running the following +Salt command: + +.. code-block:: bash + + salt-run state.orchestrate mods=orchestrate.deploy_app pillar='{"event_tag": "foo", "event_data": {"bar": "baz"}}' + +.. _reactor-wheel: + +Wheel Reactions +--------------- + +Wheel reactions run :ref:`wheel functions ` locally on the +master. + +Like :ref:`runner reactions `, the old config schema called for +wheel reactions to have arguments passed directly under the name of the +:ref:`wheel function ` (or in ``arg`` or ``kwarg`` parameters). + +The below two examples are equivalent: + ++-----------------------------------+---------------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++===================================+=================================+ +| :: | :: | +| | | +| remove_key: | remove_key: | +| wheel.key.delete: | wheel.key.delete: | +| - args: | - match: {{ data['id'] }} | +| - match: {{ data['id'] }} | | ++-----------------------------------+---------------------------------+ + +.. _reactor-caller: + +Caller Reactions +---------------- + +Caller reactions run :ref:`remote-execution functions ` on a +minion daemon's Reactor system. To run a Reactor on the minion, it is necessary +to configure the :mod:`Reactor Engine ` in the minion +config file, and then setup your watched events in a ``reactor`` section in the +minion config file as well. + +.. note:: Masterless Minions use this Reactor + + This is the only way to run the Reactor if you use masterless minions. + +Both the old and new config schemas involve passing arguments under an ``args`` +parameter. However, the old config schema only supports positional arguments. +Therefore, the new config schema is recommended if the masterless minion is +running a supported release. + +The below two examples are equivalent: + ++---------------------------------+---------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++=================================+===========================+ +| :: | :: | +| | | +| touch_file: | touch_file: | +| caller.file.touch: | caller.file.touch: | +| - args: | - args: | +| - name: /tmp/foo | - /tmp/foo | ++---------------------------------+---------------------------+ + +This reaction is equvalent to running the following Salt command: + +.. code-block:: bash + + salt-call file.touch name=/tmp/foo + +Best Practices for Writing Reactor SLS Files +============================================ + +The Reactor works as follows: + +1. The Salt Reactor watches Salt's event bus for new events. +2. Each event's tag is matched against the list of event tags configured under + the :conf_master:`reactor` section in the Salt Master config. +3. The SLS files for any matches are rendered into a data structure that + represents one or more function calls. +4. That data structure is given to a pool of worker threads for execution. Matching and rendering Reactor SLS files is done sequentially in a single -process. Complex Jinja that calls out to slow Execution or Runner modules slows -down the rendering and causes other reactions to pile up behind the current -one. The worker pool is designed to handle complex and long-running processes -such as Salt Orchestrate. +process. For that reason, reactor SLS files should contain few individual +reactions (one, if at all possible). Also, keep in mind that reactions are +fired asynchronously (with the exception of :ref:`caller `) and +do *not* support :ref:`requisites `. -tl;dr: Rendering Reactor SLS files MUST be simple and quick. The new process -started by the worker threads can be long-running. Using the reactor to fire -an orchestrate runner would be ideal. +Complex Jinja templating that calls out to slow :ref:`remote-execution +` or :ref:`runner ` functions slows down +the rendering and causes other reactions to pile up behind the current one. The +worker pool is designed to handle complex and long-running processes like +:ref:`orchestration ` jobs. + +Therefore, when complex tasks are in order, :ref:`orchestration +` is a natural fit. Orchestration SLS files can be more +complex, and use requisites. Performing a complex task using orchestration lets +the Reactor system fire off the orchestration job and proceed with processing +other reactions. + +.. _reactor-jinja-context: Jinja Context -------------- +============= -Reactor files only have access to a minimal Jinja context. ``grains`` and -``pillar`` are not available. The ``salt`` object is available for calling -Runner and Execution modules but it should be used sparingly and only for quick -tasks for the reasons mentioned above. +Reactor SLS files only have access to a minimal Jinja context. ``grains`` and +``pillar`` are *not* available. The ``salt`` object is available for calling +:ref:`remote-execution ` or :ref:`runner ` +functions, but it should be used sparingly and only for quick tasks for the +reasons mentioned above. + +In addition to the ``salt`` object, the following variables are available in +the Jinja context: + +- ``tag`` - the tag from the event that triggered execution of the Reactor SLS + file +- ``data`` - the event's data dictionary + +The ``data`` dict will contain an ``id`` key containing the minion ID, if the +event was fired from a minion, and a ``data`` key containing the data passed to +the event. Advanced State System Capabilities ----------------------------------- +================================== -Reactor SLS files, by design, do not support Requisites, ordering, -``onlyif``/``unless`` conditionals and most other powerful constructs from -Salt's State system. +Reactor SLS files, by design, do not support :ref:`requisites `, +ordering, ``onlyif``/``unless`` conditionals and most other powerful constructs +from Salt's State system. Complex Master-side operations are best performed by Salt's Orchestrate system so using the Reactor to kick off an Orchestrate run is a very common pairing. @@ -166,7 +370,7 @@ For example: # /etc/salt/master.d/reactor.conf # A custom event containing: {"foo": "Foo!", "bar: "bar*", "baz": "Baz!"} reactor: - - myco/custom/event: + - my/custom/event: - /srv/reactor/some_event.sls .. code-block:: jinja @@ -174,15 +378,15 @@ For example: # /srv/reactor/some_event.sls invoke_orchestrate_file: runner.state.orchestrate: - - mods: _orch.do_complex_thing # /srv/salt/_orch/do_complex_thing.sls - - kwarg: - pillar: - event_tag: {{ tag }} - event_data: {{ data|json() }} + - args: + - mods: orchestrate.do_complex_thing + - pillar: + event_tag: {{ tag }} + event_data: {{ data|json }} .. code-block:: jinja - # /srv/salt/_orch/do_complex_thing.sls + # /srv/salt/orchestrate/do_complex_thing.sls {% set tag = salt.pillar.get('event_tag') %} {% set data = salt.pillar.get('event_data') %} @@ -209,7 +413,7 @@ For example: .. _beacons-and-reactors: Beacons and Reactors --------------------- +==================== An event initiated by a beacon, when it arrives at the master will be wrapped inside a second event, such that the data object containing the beacon @@ -219,27 +423,52 @@ For example, to access the ``id`` field of the beacon event in a reactor file, you will need to reference ``{{ data['data']['id'] }}`` rather than ``{{ data['id'] }}`` as for events initiated directly on the event bus. +Similarly, the data dictionary attached to the event would be located in +``{{ data['data']['data'] }}`` instead of ``{{ data['data'] }}``. + See the :ref:`beacon documentation ` for examples. -Fire an event -============= +Manually Firing an Event +======================== -To fire an event from a minion call ``event.send`` +From the Master +--------------- + +Use the :py:func:`event.send ` runner: .. code-block:: bash - salt-call event.send 'foo' '{orchestrate: refresh}' + salt-run event.send foo '{orchestrate: refresh}' -After this is called, any reactor sls files matching event tag ``foo`` will -execute with ``{{ data['data']['orchestrate'] }}`` equal to ``'refresh'``. +From the Minion +--------------- -See :py:mod:`salt.modules.event` for more information. +To fire an event to the master from a minion, call :py:func:`event.send +`: -Knowing what event is being fired -================================= +.. code-block:: bash -The best way to see exactly what events are fired and what data is available in -each event is to use the :py:func:`state.event runner + salt-call event.send foo '{orchestrate: refresh}' + +To fire an event to the minion's local event bus, call :py:func:`event.fire +`: + +.. code-block:: bash + + salt-call event.fire '{orchestrate: refresh}' foo + +Referencing Data Passed in Events +--------------------------------- + +Assuming any of the above examples, any reactor SLS files triggered by watching +the event tag ``foo`` will execute with ``{{ data['data']['orchestrate'] }}`` +equal to ``'refresh'``. + +Getting Information About Events +================================ + +The best way to see exactly what events have been fired and what data is +available in each event is to use the :py:func:`state.event runner `. .. seealso:: :ref:`Common Salt Events ` @@ -308,156 +537,10 @@ rendered SLS file (or any errors generated while rendering the SLS file). view the result of referencing Jinja variables. If the result is empty then Jinja produced an empty result and the Reactor will ignore it. -.. _reactor-structure: +Passing Event Data to Minions or Orchestration as Pillar +-------------------------------------------------------- -Understanding the Structure of Reactor Formulas -=============================================== - -**I.e., when to use `arg` and `kwarg` and when to specify the function -arguments directly.** - -While the reactor system uses the same basic data structure as the state -system, the functions that will be called using that data structure are -different functions than are called via Salt's state system. The Reactor can -call Runner modules using the `runner` prefix, Wheel modules using the `wheel` -prefix, and can also cause minions to run Execution modules using the `local` -prefix. - -.. versionchanged:: 2014.7.0 - The ``cmd`` prefix was renamed to ``local`` for consistency with other - parts of Salt. A backward-compatible alias was added for ``cmd``. - -The Reactor runs on the master and calls functions that exist on the master. In -the case of Runner and Wheel functions the Reactor can just call those -functions directly since they exist on the master and are run on the master. - -In the case of functions that exist on minions and are run on minions, the -Reactor still needs to call a function on the master in order to send the -necessary data to the minion so the minion can execute that function. - -The Reactor calls functions exposed in :ref:`Salt's Python API documentation -`. and thus the structure of Reactor files very transparently -reflects the function signatures of those functions. - -Calling Execution modules on Minions ------------------------------------- - -The Reactor sends commands down to minions in the exact same way Salt's CLI -interface does. It calls a function locally on the master that sends the name -of the function as well as a list of any arguments and a dictionary of any -keyword arguments that the minion should use to execute that function. - -Specifically, the Reactor calls the async version of :py:meth:`this function -`. You can see that function has 'arg' and 'kwarg' -parameters which are both values that are sent down to the minion. - -Executing remote commands maps to the :strong:`LocalClient` interface which is -used by the :strong:`salt` command. This interface more specifically maps to -the :strong:`cmd_async` method inside of the :strong:`LocalClient` class. This -means that the arguments passed are being passed to the :strong:`cmd_async` -method, not the remote method. A field starts with :strong:`local` to use the -:strong:`LocalClient` subsystem. The result is, to execute a remote command, -a reactor formula would look like this: - -.. code-block:: yaml - - clean_tmp: - local.cmd.run: - - tgt: '*' - - arg: - - rm -rf /tmp/* - -The ``arg`` option takes a list of arguments as they would be presented on the -command line, so the above declaration is the same as running this salt -command: - -.. code-block:: bash - - salt '*' cmd.run 'rm -rf /tmp/*' - -Use the ``tgt_type`` argument to specify a matcher: - -.. code-block:: yaml - - clean_tmp: - local.cmd.run: - - tgt: 'os:Ubuntu' - - tgt_type: grain - - arg: - - rm -rf /tmp/* - - - clean_tmp: - local.cmd.run: - - tgt: 'G@roles:hbase_master' - - tgt_type: compound - - arg: - - rm -rf /tmp/* - -.. note:: - The ``tgt_type`` argument was named ``expr_form`` in releases prior to - 2017.7.0 (2016.11.x and earlier). - -Any other parameters in the :py:meth:`LocalClient().cmd() -` method can be specified as well. - -Executing Reactors from the Minion ----------------------------------- - -The minion can be setup to use the Reactor via a reactor engine. This just -sets up and listens to the minions event bus, instead of to the masters. - -The biggest difference is that you have to use the caller method on the -Reactor, which is the equivalent of salt-call, to run your commands. - -:mod:`Reactor Engine setup ` - -.. code-block:: yaml - - clean_tmp: - caller.cmd.run: - - arg: - - rm -rf /tmp/* - -.. note:: Masterless Minions use this Reactor - - This is the only way to run the Reactor if you use masterless minions. - -Calling Runner modules and Wheel modules ----------------------------------------- - -Calling Runner modules and Wheel modules from the Reactor uses a more direct -syntax since the function is being executed locally instead of sending a -command to a remote system to be executed there. There are no 'arg' or 'kwarg' -parameters (unless the Runner function or Wheel function accepts a parameter -with either of those names.) - -For example: - -.. code-block:: yaml - - clear_the_grains_cache_for_all_minions: - runner.cache.clear_grains - -If the :py:func:`the runner takes arguments ` then -they must be specified as keyword arguments. - -.. code-block:: yaml - - spin_up_more_web_machines: - runner.cloud.profile: - - prof: centos_6 - - instances: - - web11 # These VM names would be generated via Jinja in a - - web12 # real-world example. - -To determine the proper names for the arguments, check the documentation -or source code for the runner function you wish to call. - -Passing event data to Minions or Orchestrate as Pillar ------------------------------------------------------- - -An interesting trick to pass data from the Reactor script to +An interesting trick to pass data from the Reactor SLS file to :py:func:`state.apply ` is to pass it as inline Pillar data since both functions take a keyword argument named ``pillar``. @@ -484,10 +567,9 @@ from the event to the state file via inline Pillar. add_new_minion_to_pool: local.state.apply: - tgt: 'haproxy*' - - arg: - - haproxy.refresh_pool - - kwarg: - pillar: + - args: + - mods: haproxy.refresh_pool + - pillar: new_minion: {{ data['id'] }} {% endif %} @@ -503,17 +585,16 @@ This works with Orchestrate files as well: call_some_orchestrate_file: runner.state.orchestrate: - - mods: _orch.some_orchestrate_file - - pillar: - stuff: things + - args: + - mods: orchestrate.some_orchestrate_file + - pillar: + stuff: things Which is equivalent to the following command at the CLI: .. code-block:: bash - salt-run state.orchestrate _orch.some_orchestrate_file pillar='{stuff: things}' - -This expects to find a file at /srv/salt/_orch/some_orchestrate_file.sls. + salt-run state.orchestrate orchestrate.some_orchestrate_file pillar='{stuff: things}' Finally, that data is available in the state file using the normal Pillar lookup syntax. The following example is grabbing web server names and IP @@ -564,7 +645,7 @@ includes the minion id, which we can use for matching. - 'salt/minion/ink*/start': - /srv/reactor/auth-complete.sls -In this sls file, we say that if the key was rejected we will delete the key on +In this SLS file, we say that if the key was rejected we will delete the key on the master and then also tell the master to ssh in to the minion and tell it to restart the minion, since a minion process will die if the key is rejected. @@ -580,19 +661,21 @@ authentication every ten seconds by default. {% if not data['result'] and data['id'].startswith('ink') %} minion_remove: wheel.key.delete: - - match: {{ data['id'] }} + - args: + - match: {{ data['id'] }} minion_rejoin: local.cmd.run: - tgt: salt-master.domain.tld - - arg: - - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "{{ data['id'] }}" 'sleep 10 && /etc/init.d/salt-minion restart' + - args: + - cmd: ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "{{ data['id'] }}" 'sleep 10 && /etc/init.d/salt-minion restart' {% endif %} {# Ink server is sending new key -- accept this key #} {% if 'act' in data and data['act'] == 'pend' and data['id'].startswith('ink') %} minion_add: wheel.key.accept: - - match: {{ data['id'] }} + - args: + - match: {{ data['id'] }} {% endif %} No if statements are needed here because we already limited this action to just From a7b4e1f78237dcd45b56e4acd78a70b07fe0f214 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 6 Sep 2017 10:38:45 -0500 Subject: [PATCH 065/633] Simplify client logic --- salt/client/mixins.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/salt/client/mixins.py b/salt/client/mixins.py index bd69d269bf..ea2090e263 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -360,19 +360,18 @@ class SyncClientMixin(object): # that since the caller knows what is an arg vs a kwarg, but while # we make the transition we will load "kwargs" using format_call if # there are no kwargs in the low object passed in. - f_call = {} if 'arg' in low and 'kwarg' in low \ - else salt.utils.format_call( + + if 'arg' in low and 'kwarg' in low: + args = low['arg'] + kwargs = low['kwarg'] + else: + f_call = salt.utils.format_call( self.functions[fun], low, expected_extra_kws=CLIENT_INTERNAL_KEYWORDS ) - - args = f_call.get('args', ()) \ - if 'arg' not in low \ - else low['arg'] - kwargs = f_call.get('kwargs', {}) \ - if 'kwarg' not in low \ - else low['kwarg'] + args = f_call.get('args', ()) + kwargs = f_call.get('kwargs', {}) # Update the event data with loaded args and kwargs data['fun_args'] = list(args) + ([kwargs] if kwargs else []) From 3118faca0a96ef1e7f40debeea04f9d84a7eec04 Mon Sep 17 00:00:00 2001 From: Peter Sagerson Date: Tue, 12 Sep 2017 13:17:20 -0700 Subject: [PATCH 066/633] acme.cert: avoid IOError on failure. --- salt/states/acme.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/salt/states/acme.py b/salt/states/acme.py index 1ab6b57dfb..43649a6426 100644 --- a/salt/states/acme.py +++ b/salt/states/acme.py @@ -116,9 +116,14 @@ def cert(name, if res['result'] is None: ret['changes'] = {} else: + if not __salt__['acme.has'](name): + new = None + else: + new = __salt__['acme.info'](name) + ret['changes'] = { 'old': old, - 'new': __salt__['acme.info'](name) + 'new': new } return ret From 6f6619242fd8d46ac226eeb6d7778594a4e3a076 Mon Sep 17 00:00:00 2001 From: 3add3287 <3add3287@users.noreply.github.com> Date: Wed, 13 Sep 2017 17:10:42 +0200 Subject: [PATCH 067/633] Fix checking for newline on end of file by properly checking the last byte of the file if the file is non empty. --- salt/modules/ssh.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/salt/modules/ssh.py b/salt/modules/ssh.py index 022a5bc916..0f8210392c 100644 --- a/salt/modules/ssh.py +++ b/salt/modules/ssh.py @@ -740,10 +740,13 @@ def set_auth_key( with salt.utils.fopen(fconfig, 'ab+') as _fh: if new_file is False: # Let's make sure we have a new line at the end of the file - _fh.seek(1024, 2) - if not _fh.read(1024).rstrip(six.b(' ')).endswith(six.b('\n')): - _fh.seek(0, 2) - _fh.write(six.b('\n')) + _fh.seek(0,2) + if _fh.tell() > 0: + # File isn't empty, check if last byte is a newline + # If not, add one + _fh.seek(-1,2) + if _fh.read(1) != six.b('\n') + _fh.write(six.b('\n')) if six.PY3: auth_line = auth_line.encode(__salt_system_encoding__) _fh.write(auth_line) From 923ec62771b933a4b29c2c4c6bcbc21b5c43757d Mon Sep 17 00:00:00 2001 From: 3add3287 <3add3287@users.noreply.github.com> Date: Wed, 13 Sep 2017 17:35:39 +0200 Subject: [PATCH 068/633] Copy paste typo --- salt/modules/ssh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/ssh.py b/salt/modules/ssh.py index 0f8210392c..22ec4a9fb1 100644 --- a/salt/modules/ssh.py +++ b/salt/modules/ssh.py @@ -745,7 +745,7 @@ def set_auth_key( # File isn't empty, check if last byte is a newline # If not, add one _fh.seek(-1,2) - if _fh.read(1) != six.b('\n') + if _fh.read(1) != six.b('\n'): _fh.write(six.b('\n')) if six.PY3: auth_line = auth_line.encode(__salt_system_encoding__) From 406f61ac9ad8f7cb26be99fbe2916b02dc040363 Mon Sep 17 00:00:00 2001 From: 3add3287 <3add3287@users.noreply.github.com> Date: Wed, 13 Sep 2017 20:38:39 +0200 Subject: [PATCH 069/633] Fix indentation from tabs to spaces --- salt/modules/ssh.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/salt/modules/ssh.py b/salt/modules/ssh.py index 22ec4a9fb1..a158ed3ece 100644 --- a/salt/modules/ssh.py +++ b/salt/modules/ssh.py @@ -739,14 +739,14 @@ def set_auth_key( try: with salt.utils.fopen(fconfig, 'ab+') as _fh: if new_file is False: - # Let's make sure we have a new line at the end of the file - _fh.seek(0,2) - if _fh.tell() > 0: - # File isn't empty, check if last byte is a newline - # If not, add one - _fh.seek(-1,2) - if _fh.read(1) != six.b('\n'): - _fh.write(six.b('\n')) + # Let's make sure we have a new line at the end of the file + _fh.seek(0,2) + if _fh.tell() > 0: + # File isn't empty, check if last byte is a newline + # If not, add one + _fh.seek(-1,2) + if _fh.read(1) != six.b('\n'): + _fh.write(six.b('\n')) if six.PY3: auth_line = auth_line.encode(__salt_system_encoding__) _fh.write(auth_line) From c68dd5b8a43b4348d67f0e48c0b6e24fba7138e6 Mon Sep 17 00:00:00 2001 From: Nicole Thomas Date: Wed, 13 Sep 2017 18:35:49 -0400 Subject: [PATCH 070/633] Lint: fix spacing --- salt/modules/ssh.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/salt/modules/ssh.py b/salt/modules/ssh.py index a158ed3ece..2f48cf7a57 100644 --- a/salt/modules/ssh.py +++ b/salt/modules/ssh.py @@ -739,14 +739,14 @@ def set_auth_key( try: with salt.utils.fopen(fconfig, 'ab+') as _fh: if new_file is False: - # Let's make sure we have a new line at the end of the file - _fh.seek(0,2) - if _fh.tell() > 0: - # File isn't empty, check if last byte is a newline - # If not, add one - _fh.seek(-1,2) - if _fh.read(1) != six.b('\n'): - _fh.write(six.b('\n')) + # Let's make sure we have a new line at the end of the file + _fh.seek(0, 2) + if _fh.tell() > 0: + # File isn't empty, check if last byte is a newline + # If not, add one + _fh.seek(-1, 2) + if _fh.read(1) != six.b('\n'): + _fh.write(six.b('\n')) if six.PY3: auth_line = auth_line.encode(__salt_system_encoding__) _fh.write(auth_line) From 1d6dc6fb727e2c25972acfbb96c62bc21ca79e74 Mon Sep 17 00:00:00 2001 From: Damon Atkins Date: Sun, 3 Sep 2017 17:23:44 +1000 Subject: [PATCH 071/633] Docs are wrong cache_dir (bool) and cache_file (str) cannot be passed on the cli (#2) --- salt/modules/win_pkg.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index f66bd762ee..1f85f49fcd 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -913,18 +913,6 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): # Version 1.2.3 will apply to packages foo and bar salt '*' pkg.install foo,bar version=1.2.3 - cache_file (str): - A single file to copy down for use with the installer. Copied to the - same location as the installer. Use this over ``cache_dir`` if there - are many files in the directory and you only need a specific file - and don't want to cache additional files that may reside in the - installer directory. Only applies to files on ``salt://`` - - cache_dir (bool): - True will copy the contents of the installer directory. This is - useful for installations that are not a single file. Only applies to - directories on ``salt://`` - extra_install_flags (str): Additional install flags that will be appended to the ``install_flags`` defined in the software definition file. Only From a7c8b9e048d0191beab4f02132db1458a1df8701 Mon Sep 17 00:00:00 2001 From: Damon Atkins Date: Wed, 6 Sep 2017 02:28:16 +1000 Subject: [PATCH 072/633] Update win_pkg.py --- salt/modules/win_pkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index 1f85f49fcd..1f6f20b8a3 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -1204,7 +1204,7 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): if use_msiexec: cmd = msiexec arguments = ['/i', cached_pkg] - if pkginfo['version_num'].get('allusers', True): + if pkginfo[version_num].get('allusers', True): arguments.append('ALLUSERS="1"') arguments.extend(salt.utils.shlex_split(install_flags)) else: From d4981a2717d2cdbe1a726b6a7624e307ac834312 Mon Sep 17 00:00:00 2001 From: Damon Atkins Date: Wed, 6 Sep 2017 12:31:51 +1000 Subject: [PATCH 073/633] Update doco --- doc/topics/windows/windows-package-manager.rst | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/topics/windows/windows-package-manager.rst b/doc/topics/windows/windows-package-manager.rst index 063c8b44eb..cea071e888 100644 --- a/doc/topics/windows/windows-package-manager.rst +++ b/doc/topics/windows/windows-package-manager.rst @@ -480,11 +480,17 @@ Alternatively the ``uninstaller`` can also simply repeat the URL of the msi file :param bool allusers: This parameter is specific to `.msi` installations. It tells `msiexec` to install the software for all users. The default is True. -:param bool cache_dir: If true, the entire directory where the installer resides - will be recursively cached. This is useful for installers that depend on - other files in the same directory for installation. +:param bool cache_dir: If true when installer URL begins with salt://, the + entire directory where the installer resides will be recursively cached. + This is useful for installers that depend on other files in the same + directory for installation. -.. note:: Only applies to salt: installer URLs. +:param str cache_file: + When installer URL begins with salt://, this indicates single file to copy + down for use with the installer. Copied to the same location as the + installer. Use this over ``cache_dir`` if there are many files in the + directory and you only need a specific file and don't want to cache + additional files that may reside in the installer directory. Here's an example for a software package that has dependent files: From c3e16661c35314f4af414e586745c327b9bab44c Mon Sep 17 00:00:00 2001 From: Damon Atkins Date: Sun, 3 Sep 2017 17:23:44 +1000 Subject: [PATCH 074/633] Docs are wrong cache_dir (bool) and cache_file (str) cannot be passed on the cli (#2) --- salt/modules/win_pkg.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index d3434cc2b7..3357a87471 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -983,18 +983,6 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): # Version 1.2.3 will apply to packages foo and bar salt '*' pkg.install foo,bar version=1.2.3 - cache_file (str): - A single file to copy down for use with the installer. Copied to the - same location as the installer. Use this over ``cache_dir`` if there - are many files in the directory and you only need a specific file - and don't want to cache additional files that may reside in the - installer directory. Only applies to files on ``salt://`` - - cache_dir (bool): - True will copy the contents of the installer directory. This is - useful for installations that are not a single file. Only applies to - directories on ``salt://`` - extra_install_flags (str): Additional install flags that will be appended to the ``install_flags`` defined in the software definition file. Only From 5cdcdbf428234277555f5c58b22e09ec70b2ca0a Mon Sep 17 00:00:00 2001 From: Damon Atkins Date: Wed, 6 Sep 2017 02:28:16 +1000 Subject: [PATCH 075/633] Update win_pkg.py --- salt/modules/win_pkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index 3357a87471..9ed7e3d7f5 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -1274,7 +1274,7 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): if use_msiexec: cmd = msiexec arguments = ['/i', cached_pkg] - if pkginfo['version_num'].get('allusers', True): + if pkginfo[version_num].get('allusers', True): arguments.append('ALLUSERS="1"') arguments.extend(salt.utils.shlex_split(install_flags)) else: From b3dbafb0357686c90f712acb284cd8473f853f10 Mon Sep 17 00:00:00 2001 From: Damon Atkins Date: Wed, 6 Sep 2017 12:31:51 +1000 Subject: [PATCH 076/633] Update doco --- doc/topics/windows/windows-package-manager.rst | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/topics/windows/windows-package-manager.rst b/doc/topics/windows/windows-package-manager.rst index 9d1838c807..20ed60baf6 100644 --- a/doc/topics/windows/windows-package-manager.rst +++ b/doc/topics/windows/windows-package-manager.rst @@ -481,11 +481,17 @@ Alternatively the ``uninstaller`` can also simply repeat the URL of the msi file :param bool allusers: This parameter is specific to `.msi` installations. It tells `msiexec` to install the software for all users. The default is True. -:param bool cache_dir: If true, the entire directory where the installer resides - will be recursively cached. This is useful for installers that depend on - other files in the same directory for installation. +:param bool cache_dir: If true when installer URL begins with salt://, the + entire directory where the installer resides will be recursively cached. + This is useful for installers that depend on other files in the same + directory for installation. -.. note:: Only applies to salt: installer URLs. +:param str cache_file: + When installer URL begins with salt://, this indicates single file to copy + down for use with the installer. Copied to the same location as the + installer. Use this over ``cache_dir`` if there are many files in the + directory and you only need a specific file and don't want to cache + additional files that may reside in the installer directory. Here's an example for a software package that has dependent files: From 58f7d051c9fe8d11cd373cfec28caf0f37fa4da9 Mon Sep 17 00:00:00 2001 From: haam3r Date: Thu, 14 Sep 2017 22:40:19 +0300 Subject: [PATCH 077/633] Issue #43479 No runners.config in 2017.7 branch Add extra note about needing to import the runners.config module from the develop branch when running on a 2017.7 release. --- doc/ref/runners/all/salt.runners.mattermost.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/ref/runners/all/salt.runners.mattermost.rst b/doc/ref/runners/all/salt.runners.mattermost.rst index 4a2b8e28c6..7fa1e2f3d4 100644 --- a/doc/ref/runners/all/salt.runners.mattermost.rst +++ b/doc/ref/runners/all/salt.runners.mattermost.rst @@ -1,6 +1,12 @@ salt.runners.mattermost module ============================== +**Note for 2017.7 releases!** + +Due to the `salt.runners.config `_ module not being available in this release series, importing the `salt.runners.config `_ module from the develop branch is required to make this module work. + +Ref: `Mattermost runner failing to retrieve config values due to unavailable config runner #43479 `_ + .. automodule:: salt.runners.mattermost :members: :undoc-members: From bcbf7b4e684df322e44f8039a4dbd0e670d75b96 Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 14 Sep 2017 17:26:59 -0600 Subject: [PATCH 078/633] Add logic for test=True --- salt/states/chocolatey.py | 46 ++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/salt/states/chocolatey.py b/salt/states/chocolatey.py index d83f9bddd3..60627e1888 100644 --- a/salt/states/chocolatey.py +++ b/salt/states/chocolatey.py @@ -97,29 +97,49 @@ def installed(name, version=None, source=None, force=False, pre_versions=False, ret['changes'] = {name: 'Version {0} will be installed' ''.format(version)} else: - ret['changes'] = {name: 'Will be installed'} + ret['changes'] = {name: 'Latest version will be installed'} + # Package installed else: version_info = __salt__['chocolatey.version'](name, check_remote=True) full_name = name - lower_name = name.lower() for pkg in version_info: - if lower_name == pkg.lower(): + if name.lower() == pkg.lower(): full_name = pkg - available_version = version_info[full_name]['available'][0] - version = version if version else available_version + installed_version = version_info[full_name]['installed'][0] - if force: - ret['changes'] = {name: 'Version {0} will be forcibly installed' - ''.format(version)} - elif allow_multiple: - ret['changes'] = {name: 'Version {0} will be installed side by side' - ''.format(version)} + if version: + if salt.utils.compare_versions( + ver1=installed_version, oper="==", ver2=version): + if force: + ret['changes'] = {name: 'Version {0} will be reinstalled' + ''.format(version)} + else: + ret['comment'] = '{0} {1} is already installed' \ + ''.format(name, version) + return ret + else: + if allow_multiple: + ret['changes'] = { + name: 'Version {0} will be installed side by side with ' + 'Version {1} if supported' + ''.format(version, installed_version)} + else: + ret['changes'] = { + name: 'Version {0} will be installed over Existing ' + 'Version {1}'.format(version, installed_version)} + force = True else: - ret['comment'] = 'The Package {0} is already installed'.format(name) - return ret + version = installed_version + if force: + ret['changes'] = {name: 'Version {0} will be reinstalled' + ''.format(version)} + else: + ret['comment'] = '{0} {1} is already installed' \ + ''.format(name, version) + return ret if __opts__['test']: ret['result'] = None From 0e3c4475673badb00059897cc6ee1042a865e126 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 14 Sep 2017 20:36:57 -0500 Subject: [PATCH 079/633] Fix incorrect handling of pkg virtual and os_family grain Several Debian-based distros have the wrong os_family grain, and instead of fixing it in the core grains, the aptpkg __virtual__ has been incorrectly modified to look for the incorrect os_family. This fixes the core grains and changes the aptpkg __virtual__ to look only for the Debian os_family. It also adds a comment in the __virtual__ to clear this up for the future. --- salt/grains/core.py | 4 ++++ salt/modules/aptpkg.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/salt/grains/core.py b/salt/grains/core.py index a7e1a22d2a..0a98bc148f 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -1175,6 +1175,10 @@ _OS_FAMILY_MAP = { 'Raspbian': 'Debian', 'Devuan': 'Debian', 'antiX': 'Debian', + 'Kali': 'Debian', + 'neon': 'Debian', + 'Cumulus': 'Debian', + 'Deepin': 'Debian', 'NILinuxRT': 'NILinuxRT', 'NILinuxRT-XFCE': 'NILinuxRT', 'Void': 'Void', diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py index 04ddbaf9a2..01c0548144 100644 --- a/salt/modules/aptpkg.py +++ b/salt/modules/aptpkg.py @@ -93,11 +93,15 @@ __virtualname__ = 'pkg' def __virtual__(): ''' - Confirm this module is on a Debian based system + Confirm this module is on a Debian-based system ''' - if __grains__.get('os_family') in ('Kali', 'Debian', 'neon'): - return __virtualname__ - elif __grains__.get('os_family', False) == 'Cumulus': + # If your minion is running an OS which is Debian-based but does not have + # an "os_family" grain of Debian, then the proper fix is NOT to check for + # the minion's "os_family" grain here in the __virtual__. The correct fix + # is to add the value from the minion's "os" grain to the _OS_FAMILY_MAP + # dict in salt/grains/core.py, so that we assign the correct "os_family" + # grain to the minion. + if __grains__.get('os_family') == 'Debian': return __virtualname__ return (False, 'The pkg module could not be loaded: unsupported OS family') From 2033f3d6d39188a50bb8006e145397a0e0fdd262 Mon Sep 17 00:00:00 2001 From: Joaquin Veira Date: Fri, 15 Sep 2017 14:14:46 +0200 Subject: [PATCH 080/633] Update zabbix_return.py Using salt.utils.fopen... correctly --- salt/returners/zabbix_return.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/returners/zabbix_return.py b/salt/returners/zabbix_return.py index a78f784c56..be4f71cf48 100644 --- a/salt/returners/zabbix_return.py +++ b/salt/returners/zabbix_return.py @@ -56,7 +56,7 @@ def zbx(): def zabbix_send(key, host, output): - f = open(zbx()['zabbix_config'],'r') + with salt.utils.fopen(zbx()['zabbix_config'],'r') as file_handle: for line in f: if "ServerActive" in line: flag = "true" From 0e4a744d95ff9761bb9a0fb1952fab2fd61dbe0f Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Fri, 15 Sep 2017 18:47:03 +0300 Subject: [PATCH 081/633] Forward events to all masters syndic connected to. --- salt/minion.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/salt/minion.py b/salt/minion.py index 6b7c82a8d7..88ef463d0c 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -2589,6 +2589,8 @@ class SyndicManager(MinionBase): ''' if kwargs is None: kwargs = {} + successful = False + # Call for each master for master, syndic_future in self.iter_master_options(master_id): if not syndic_future.done() or syndic_future.exception(): log.error('Unable to call {0} on {1}, that syndic is not connected'.format(func, master)) @@ -2596,12 +2598,12 @@ class SyndicManager(MinionBase): try: getattr(syndic_future.result(), func)(*args, **kwargs) - return + successful = True except SaltClientError: log.error('Unable to call {0} on {1}, trying another...'.format(func, master)) self._mark_master_dead(master) - continue - log.critical('Unable to call {0} on any masters!'.format(func)) + if not successful: + log.critical('Unable to call {0} on any masters!'.format(func)) def _return_pub_syndic(self, values, master_id=None): ''' From b4966ac56574cb0c5f194c2b651bcfe490f44b1b Mon Sep 17 00:00:00 2001 From: Orlando Richards Date: Fri, 15 Sep 2017 17:40:36 +0100 Subject: [PATCH 082/633] Update yumpkg.py Fix issue #41978 - I've been using this amended regular expression for several months now, with around 800-900 versionlocks (all packages on a typical CentOS 7 instance), without issue. --- salt/modules/yumpkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index e5ddc11e5a..849c5c4f29 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -61,7 +61,7 @@ from salt.ext import six log = logging.getLogger(__name__) -__HOLD_PATTERN = r'\w+(?:[.-][^-]+)*' +__HOLD_PATTERN = r'[\w+]+(?:[.-][^-]+)*' # Define the module's virtual name __virtualname__ = 'pkg' From f146399f7a52bcfe0fa2df3c878bb75543d36fea Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 15 Sep 2017 11:07:55 -0600 Subject: [PATCH 083/633] Use posix=False for shlex.split Fixes issue with doublequotes being removed in Windows Removes forced log level so that the command being run will be displayed Consolidates the creation of the uninstall command to avoid duplication --- salt/modules/win_pkg.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index 9ed7e3d7f5..2c7a2b5e01 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -1276,10 +1276,10 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): arguments = ['/i', cached_pkg] if pkginfo[version_num].get('allusers', True): arguments.append('ALLUSERS="1"') - arguments.extend(salt.utils.shlex_split(install_flags)) + arguments.extend(salt.utils.shlex_split(install_flags, posix=False)) else: cmd = cached_pkg - arguments = salt.utils.shlex_split(install_flags) + arguments = salt.utils.shlex_split(install_flags, posix=False) # Install the software # Check Use Scheduler Option @@ -1341,7 +1341,6 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): # Launch the command result = __salt__['cmd.run_all'](cmd, cache_path, - output_loglevel='quiet', python_shell=False, redirect_stderr=True) if not result['retcode']: @@ -1600,19 +1599,20 @@ def remove(name=None, pkgs=None, version=None, **kwargs): #Compute msiexec string use_msiexec, msiexec = _get_msiexec(pkginfo[target].get('msiexec', False)) + # Build cmd and arguments + # cmd and arguments must be separated for use with the task scheduler + if use_msiexec: + cmd = msiexec + arguments = ['/x'] + arguments.extend(salt.utils.shlex_split(uninstall_flags, posix=False)) + else: + cmd = expanded_cached_pkg + arguments = salt.utils.shlex_split(uninstall_flags, posix=False) + # Uninstall the software # Check Use Scheduler Option if pkginfo[target].get('use_scheduler', False): - # Build Scheduled Task Parameters - if use_msiexec: - cmd = msiexec - arguments = ['/x'] - arguments.extend(salt.utils.shlex_split(uninstall_flags)) - else: - cmd = expanded_cached_pkg - arguments = salt.utils.shlex_split(uninstall_flags) - # Create Scheduled Task __salt__['task.create_task'](name='update-salt-software', user_name='System', @@ -1633,16 +1633,12 @@ def remove(name=None, pkgs=None, version=None, **kwargs): ret[pkgname] = {'uninstall status': 'failed'} else: # Build the install command - cmd = [] - if use_msiexec: - cmd.extend([msiexec, '/x', expanded_cached_pkg]) - else: - cmd.append(expanded_cached_pkg) - cmd.extend(salt.utils.shlex_split(uninstall_flags)) + cmd = [cmd] + cmd.extend(arguments) + # Launch the command result = __salt__['cmd.run_all']( cmd, - output_loglevel='trace', python_shell=False, redirect_stderr=True) if not result['retcode']: From 1546c1ca0468f71b24b720503a1f1a2e9ccf5521 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 15 Sep 2017 11:16:50 -0600 Subject: [PATCH 084/633] Add posix=False to call to salt.utils.shlex_split --- salt/modules/win_pkg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index 1f6f20b8a3..e6c33e5f12 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -1206,10 +1206,10 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): arguments = ['/i', cached_pkg] if pkginfo[version_num].get('allusers', True): arguments.append('ALLUSERS="1"') - arguments.extend(salt.utils.shlex_split(install_flags)) + arguments.extend(salt.utils.shlex_split(install_flags, posix=False)) else: cmd = cached_pkg - arguments = salt.utils.shlex_split(install_flags) + arguments = salt.utils.shlex_split(install_flags, posix=False) # Install the software # Check Use Scheduler Option @@ -1513,10 +1513,10 @@ def remove(name=None, pkgs=None, version=None, **kwargs): if use_msiexec: cmd = msiexec arguments = ['/x'] - arguments.extend(salt.utils.shlex_split(uninstall_flags)) + arguments.extend(salt.utils.shlex_split(uninstall_flags, posix=False)) else: cmd = expanded_cached_pkg - arguments = salt.utils.shlex_split(uninstall_flags) + arguments = salt.utils.shlex_split(uninstall_flags, posix=False) # Create Scheduled Task __salt__['task.create_task'](name='update-salt-software', @@ -1543,7 +1543,7 @@ def remove(name=None, pkgs=None, version=None, **kwargs): cmd.extend([msiexec, '/x', expanded_cached_pkg]) else: cmd.append(expanded_cached_pkg) - cmd.extend(salt.utils.shlex_split(uninstall_flags)) + cmd.extend(salt.utils.shlex_split(uninstall_flags, posix=False)) # Launch the command result = __salt__['cmd.run_all'](cmd, output_loglevel='trace', From 1b0a4d39d23ebbafd09d5a5b66c95aaecde69ce1 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 15 Sep 2017 13:37:53 -0600 Subject: [PATCH 085/633] Fix logic in `/etc/paths.d/salt` detection --- pkg/osx/pkg-scripts/preinstall | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/osx/pkg-scripts/preinstall b/pkg/osx/pkg-scripts/preinstall index c919cafcb1..7e92eeab6a 100755 --- a/pkg/osx/pkg-scripts/preinstall +++ b/pkg/osx/pkg-scripts/preinstall @@ -129,7 +129,7 @@ fi ############################################################################### # Remove the salt from the paths.d ############################################################################### -if [ ! -f "/etc/paths.d/salt" ]; then +if [ -f "/etc/paths.d/salt" ]; then echo "Path: Removing salt from the path..." >> "$TEMP_DIR/preinstall.txt" rm "/etc/paths.d/salt" echo "Path: Removed Successfully" >> "$TEMP_DIR/preinstall.txt" From f33395f1eef85ba08830825ca9ed770485690b33 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 15 Sep 2017 13:41:23 -0600 Subject: [PATCH 086/633] Fix logic in `/etc/paths.d/salt` detection --- pkg/osx/pkg-scripts/preinstall | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/osx/pkg-scripts/preinstall b/pkg/osx/pkg-scripts/preinstall index 3eb4235107..8c671c6df9 100755 --- a/pkg/osx/pkg-scripts/preinstall +++ b/pkg/osx/pkg-scripts/preinstall @@ -132,7 +132,7 @@ fi ############################################################################### # Remove the salt from the paths.d ############################################################################### -if [ ! -f "/etc/paths.d/salt" ]; then +if [ -f "/etc/paths.d/salt" ]; then echo "Path: Removing salt from the path..." >> "$TEMP_DIR/preinstall.txt" rm "/etc/paths.d/salt" echo "Path: Removed Successfully" >> "$TEMP_DIR/preinstall.txt" From 56be5c35eb20b99da7be924f1784edbc18994759 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 15 Sep 2017 16:08:21 -0600 Subject: [PATCH 087/633] Improve logic for handling chocolatey states --- salt/states/chocolatey.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/salt/states/chocolatey.py b/salt/states/chocolatey.py index 60627e1888..141d5e7d59 100644 --- a/salt/states/chocolatey.py +++ b/salt/states/chocolatey.py @@ -114,11 +114,15 @@ def installed(name, version=None, source=None, force=False, pre_versions=False, if salt.utils.compare_versions( ver1=installed_version, oper="==", ver2=version): if force: - ret['changes'] = {name: 'Version {0} will be reinstalled' - ''.format(version)} + ret['changes'] = { + name: 'Version {0} will be reinstalled'.format(version)} + ret['comment'] = 'Reinstall {0} {1}' \ + ''.format(full_name, version) else: ret['comment'] = '{0} {1} is already installed' \ ''.format(name, version) + if __opts__['test']: + ret['result'] = None return ret else: if allow_multiple: @@ -126,19 +130,27 @@ def installed(name, version=None, source=None, force=False, pre_versions=False, name: 'Version {0} will be installed side by side with ' 'Version {1} if supported' ''.format(version, installed_version)} + ret['comment'] = 'Install {0} {1} side-by-side with {0} {2}' \ + ''.format(full_name, version, installed_version) else: ret['changes'] = { - name: 'Version {0} will be installed over Existing ' - 'Version {1}'.format(version, installed_version)} + name: 'Version {0} will be installed over Version {1} ' + ''.format(version, installed_version)} + ret['comment'] = 'Install {0} {1} over {0} {2}' \ + ''.format(full_name, version, installed_version) force = True else: version = installed_version if force: - ret['changes'] = {name: 'Version {0} will be reinstalled' - ''.format(version)} + ret['changes'] = { + name: 'Version {0} will be reinstalled'.format(version)} + ret['comment'] = 'Reinstall {0} {1}' \ + ''.format(full_name, version) else: ret['comment'] = '{0} {1} is already installed' \ ''.format(name, version) + if __opts__['test']: + ret['result'] = None return ret if __opts__['test']: From 54216177c1f76c04d46482f6c33a00a53fc1e47e Mon Sep 17 00:00:00 2001 From: "Z. Liu" Date: Sat, 16 Sep 2017 10:42:37 +0800 Subject: [PATCH 088/633] _search_name is '' if acl type is other --- salt/states/linux_acl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/salt/states/linux_acl.py b/salt/states/linux_acl.py index a6a54a7fcd..285a37ba37 100644 --- a/salt/states/linux_acl.py +++ b/salt/states/linux_acl.py @@ -81,11 +81,12 @@ def present(name, acl_type, acl_name='', perms='', recurse=False): # applied to the user/group that owns the file, e.g., # default:group::rwx would be listed as default:group:root:rwx # In this case, if acl_name is empty, we really want to search for root + # but still uses '' for other # We search through the dictionary getfacl returns for the owner of the # file if acl_name is empty. if acl_name == '': - _search_name = __current_perms[name].get('comment').get(_acl_type) + _search_name = __current_perms[name].get('comment').get(_acl_type, '') else: _search_name = acl_name @@ -150,11 +151,12 @@ def absent(name, acl_type, acl_name='', perms='', recurse=False): # applied to the user/group that owns the file, e.g., # default:group::rwx would be listed as default:group:root:rwx # In this case, if acl_name is empty, we really want to search for root + # but still uses '' for other # We search through the dictionary getfacl returns for the owner of the # file if acl_name is empty. if acl_name == '': - _search_name = __current_perms[name].get('comment').get(_acl_type) + _search_name = __current_perms[name].get('comment').get(_acl_type, '') else: _search_name = acl_name From 2ccabe296e20f8db06afa1c66968481d8893228a Mon Sep 17 00:00:00 2001 From: Yagnik Date: Tue, 27 Jun 2017 14:23:35 +0530 Subject: [PATCH 089/633] Add support for encrypted tag --- salt/serializers/yaml.py | 16 ++++++++++++++++ tests/unit/serializers/test_serializers.py | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/salt/serializers/yaml.py b/salt/serializers/yaml.py index 2fad384d1b..e893c3f389 100644 --- a/salt/serializers/yaml.py +++ b/salt/serializers/yaml.py @@ -77,10 +77,25 @@ def serialize(obj, **options): raise SerializationError(error) +class EncryptedString(str): + + yaml_tag = u'!encrypted' + + @staticmethod + def yaml_constructor(loader, tag, node): + return EncryptedString(loader.construct_scalar(node)) + + @staticmethod + def yaml_dumper(dumper, data): + return dumper.represent_scalar(EncryptedString.yaml_tag, data.__str__()) + + class Loader(BaseLoader): # pylint: disable=W0232 '''Overwrites Loader as not for pollute legacy Loader''' pass + +Loader.add_multi_constructor(EncryptedString.yaml_tag, EncryptedString.yaml_constructor) Loader.add_multi_constructor('tag:yaml.org,2002:null', Loader.construct_yaml_null) Loader.add_multi_constructor('tag:yaml.org,2002:bool', Loader.construct_yaml_bool) Loader.add_multi_constructor('tag:yaml.org,2002:int', Loader.construct_yaml_int) @@ -100,6 +115,7 @@ class Dumper(BaseDumper): # pylint: disable=W0232 '''Overwrites Dumper as not for pollute legacy Dumper''' pass +Dumper.add_multi_representer(EncryptedString, EncryptedString.yaml_dumper) Dumper.add_multi_representer(type(None), Dumper.represent_none) Dumper.add_multi_representer(str, Dumper.represent_str) if six.PY2: diff --git a/tests/unit/serializers/test_serializers.py b/tests/unit/serializers/test_serializers.py index 4f4890e06e..980405f8b8 100644 --- a/tests/unit/serializers/test_serializers.py +++ b/tests/unit/serializers/test_serializers.py @@ -18,6 +18,7 @@ import salt.serializers.yaml as yaml import salt.serializers.yamlex as yamlex import salt.serializers.msgpack as msgpack import salt.serializers.python as python +from salt.serializers.yaml import EncryptedString from salt.serializers import SerializationError from salt.utils.odict import OrderedDict @@ -43,10 +44,11 @@ class TestSerializers(TestCase): @skipIf(not yaml.available, SKIP_MESSAGE % 'yaml') def test_serialize_yaml(self): data = { - "foo": "bar" + "foo": "bar", + "encrypted_data": EncryptedString("foo") } serialized = yaml.serialize(data) - assert serialized == '{foo: bar}', serialized + assert serialized == '{encrypted_data: !encrypted foo, foo: bar}', serialized deserialized = yaml.deserialize(serialized) assert deserialized == data, deserialized From 1bd263cd51ba7de36ca430250d1961c2bfe8ece5 Mon Sep 17 00:00:00 2001 From: Wedge Jarrad Date: Sat, 16 Sep 2017 22:49:08 -0700 Subject: [PATCH 090/633] Clean up doc formatting in selinux state & module Reformat fcontext methods so that the online documentation will render properly. Add versionadded directives to the fcontext methods added in 2017.7.0. --- salt/modules/selinux.py | 108 +++++++++++++++++++++++++++------------- salt/states/selinux.py | 65 ++++++++++++++++-------- 2 files changed, 118 insertions(+), 55 deletions(-) diff --git a/salt/modules/selinux.py b/salt/modules/selinux.py index d227b12eb4..208eee03f5 100644 --- a/salt/modules/selinux.py +++ b/salt/modules/selinux.py @@ -374,8 +374,10 @@ def list_semod(): def _validate_filetype(filetype): ''' - Checks if the given filetype is a valid SELinux filetype specification. - Throws an SaltInvocationError if it isn't. + .. versionadded:: 2017.7.0 + + Checks if the given filetype is a valid SELinux filetype + specification. Throws an SaltInvocationError if it isn't. ''' if filetype not in _SELINUX_FILETYPES.keys(): raise SaltInvocationError('Invalid filetype given: {0}'.format(filetype)) @@ -384,6 +386,8 @@ def _validate_filetype(filetype): def _context_dict_to_string(context): ''' + .. versionadded:: 2017.7.0 + Converts an SELinux file context from a dict to a string. ''' return '{sel_user}:{sel_role}:{sel_type}:{sel_level}'.format(**context) @@ -391,6 +395,8 @@ def _context_dict_to_string(context): def _context_string_to_dict(context): ''' + .. versionadded:: 2017.7.0 + Converts an SELinux file context from string to dict. ''' if not re.match('[^:]+:[^:]+:[^:]+:[^:]+$', context): @@ -405,8 +411,11 @@ def _context_string_to_dict(context): def filetype_id_to_string(filetype='a'): ''' - Translates SELinux filetype single-letter representation - to a more human-readable version (which is also used in `semanage fcontext -l`). + .. versionadded:: 2017.7.0 + + Translates SELinux filetype single-letter representation to a more + human-readable version (which is also used in `semanage fcontext + -l`). ''' _validate_filetype(filetype) return _SELINUX_FILETYPES.get(filetype, 'error') @@ -414,20 +423,27 @@ def filetype_id_to_string(filetype='a'): def fcontext_get_policy(name, filetype=None, sel_type=None, sel_user=None, sel_level=None): ''' - Returns the current entry in the SELinux policy list as a dictionary. - Returns None if no exact match was found + .. versionadded:: 2017.7.0 + + Returns the current entry in the SELinux policy list as a + dictionary. Returns None if no exact match was found. + Returned keys are: - - filespec (the name supplied and matched) - - filetype (the descriptive name of the filetype supplied) - - sel_user, sel_role, sel_type, sel_level (the selinux context) + + * filespec (the name supplied and matched) + * filetype (the descriptive name of the filetype supplied) + * sel_user, sel_role, sel_type, sel_level (the selinux context) + For a more in-depth explanation of the selinux context, go to https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security-Enhanced_Linux/chap-Security-Enhanced_Linux-SELinux_Contexts.html - name: filespec of the file or directory. Regex syntax is allowed. - filetype: The SELinux filetype specification. - Use one of [a, f, d, c, b, s, l, p]. - See also `man semanage-fcontext`. - Defaults to 'a' (all files) + name + filespec of the file or directory. Regex syntax is allowed. + + filetype + The SELinux filetype specification. Use one of [a, f, d, c, b, + s, l, p]. See also `man semanage-fcontext`. Defaults to 'a' + (all files). CLI Example: @@ -460,20 +476,34 @@ def fcontext_get_policy(name, filetype=None, sel_type=None, sel_user=None, sel_l def fcontext_add_or_delete_policy(action, name, filetype=None, sel_type=None, sel_user=None, sel_level=None): ''' - Sets or deletes the SELinux policy for a given filespec and other optional parameters. - Returns the result of the call to semanage. - Note that you don't have to remove an entry before setting a new one for a given - filespec and filetype, as adding one with semanage automatically overwrites a - previously configured SELinux context. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. - file_type: The SELinux filetype specification. - Use one of [a, f, d, c, b, s, l, p]. - See also ``man semanage-fcontext``. - Defaults to 'a' (all files) - sel_type: SELinux context type. There are many. - sel_user: SELinux user. Use ``semanage login -l`` to determine which ones are available to you - sel_level: The MLS range of the SELinux context. + Sets or deletes the SELinux policy for a given filespec and other + optional parameters. + + Returns the result of the call to semanage. + + Note that you don't have to remove an entry before setting a new + one for a given filespec and filetype, as adding one with semanage + automatically overwrites a previously configured SELinux context. + + name + filespec of the file or directory. Regex syntax is allowed. + + file_type + The SELinux filetype specification. Use one of [a, f, d, c, b, + s, l, p]. See also ``man semanage-fcontext``. Defaults to 'a' + (all files). + + sel_type + SELinux context type. There are many. + + sel_user + SELinux user. Use ``semanage login -l`` to determine which ones + are available to you. + + sel_level + The MLS range of the SELinux context. CLI Example: @@ -499,10 +529,14 @@ def fcontext_add_or_delete_policy(action, name, filetype=None, sel_type=None, se def fcontext_policy_is_applied(name, recursive=False): ''' - Returns an empty string if the SELinux policy for a given filespec is applied, - returns string with differences in policy and actual situation otherwise. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. + Returns an empty string if the SELinux policy for a given filespec + is applied, returns string with differences in policy and actual + situation otherwise. + + name + filespec of the file or directory. Regex syntax is allowed. CLI Example: @@ -519,11 +553,17 @@ def fcontext_policy_is_applied(name, recursive=False): def fcontext_apply_policy(name, recursive=False): ''' - Applies SElinux policies to filespec using `restorecon [-R] filespec`. - Returns dict with changes if succesful, the output of the restorecon command otherwise. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. - recursive: Recursively apply SELinux policies. + Applies SElinux policies to filespec using `restorecon [-R] + filespec`. Returns dict with changes if succesful, the output of + the restorecon command otherwise. + + name + filespec of the file or directory. Regex syntax is allowed. + + recursive + Recursively apply SELinux policies. CLI Example: diff --git a/salt/states/selinux.py b/salt/states/selinux.py index 8187ea8338..3c2a3ee817 100644 --- a/salt/states/selinux.py +++ b/salt/states/selinux.py @@ -310,17 +310,27 @@ def module_remove(name): def fcontext_policy_present(name, sel_type, filetype='a', sel_user=None, sel_level=None): ''' - Makes sure a SELinux policy for a given filespec (name), - filetype and SELinux context type is present. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. - sel_type: SELinux context type. There are many. - filetype: The SELinux filetype specification. - Use one of [a, f, d, c, b, s, l, p]. - See also `man semanage-fcontext`. - Defaults to 'a' (all files) - sel_user: The SELinux user. - sel_level: The SELinux MLS range + Makes sure a SELinux policy for a given filespec (name), filetype + and SELinux context type is present. + + name + filespec of the file or directory. Regex syntax is allowed. + + sel_type + SELinux context type. There are many. + + filetype + The SELinux filetype specification. Use one of [a, f, d, c, b, + s, l, p]. See also `man semanage-fcontext`. Defaults to 'a' + (all files). + + sel_user + The SELinux user. + + sel_level + The SELinux MLS range. ''' ret = {'name': name, 'result': False, 'changes': {}, 'comment': ''} new_state = {} @@ -383,17 +393,27 @@ def fcontext_policy_present(name, sel_type, filetype='a', sel_user=None, sel_lev def fcontext_policy_absent(name, filetype='a', sel_type=None, sel_user=None, sel_level=None): ''' - Makes sure an SELinux file context policy for a given filespec (name), - filetype and SELinux context type is absent. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. - filetype: The SELinux filetype specification. - Use one of [a, f, d, c, b, s, l, p]. - See also `man semanage-fcontext`. - Defaults to 'a' (all files). - sel_type: The SELinux context type. There are many. - sel_user: The SELinux user. - sel_level: The SELinux MLS range + Makes sure an SELinux file context policy for a given filespec + (name), filetype and SELinux context type is absent. + + name + filespec of the file or directory. Regex syntax is allowed. + + filetype + The SELinux filetype specification. Use one of [a, f, d, c, b, + s, l, p]. See also `man semanage-fcontext`. Defaults to 'a' + (all files). + + sel_type + The SELinux context type. There are many. + + sel_user + The SELinux user. + + sel_level + The SELinux MLS range. ''' ret = {'name': name, 'result': False, 'changes': {}, 'comment': ''} new_state = {} @@ -433,7 +453,10 @@ def fcontext_policy_absent(name, filetype='a', sel_type=None, sel_user=None, sel def fcontext_policy_applied(name, recursive=False): ''' - Checks and makes sure the SELinux policies for a given filespec are applied. + .. versionadded:: 2017.7.0 + + Checks and makes sure the SELinux policies for a given filespec are + applied. ''' ret = {'name': name, 'result': False, 'changes': {}, 'comment': ''} From 4171d11838611dcf5c7d9950fd457a3171c14437 Mon Sep 17 00:00:00 2001 From: Damon Atkins Date: Mon, 18 Sep 2017 14:50:35 +1000 Subject: [PATCH 091/633] utils.files.safe_filepath add support to override the os default directory separator Note this function is not currently in use, separate PR will trigger the use of this function --- salt/utils/files.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/salt/utils/files.py b/salt/utils/files.py index 8d463756d9..2dce892602 100644 --- a/salt/utils/files.py +++ b/salt/utils/files.py @@ -271,6 +271,8 @@ def safe_filename_leaf(file_basename): windows is \\ / : * ? " < > | posix is / .. versionadded:: 2017.7.2 + + :codeauthor: Damon Atkins ''' def _replace(re_obj): return urllib.quote(re_obj.group(0), safe=u'') @@ -283,16 +285,24 @@ def safe_filename_leaf(file_basename): return re.sub(u'[\\\\:/*?"<>|]', _replace, file_basename, flags=re.UNICODE) -def safe_filepath(file_path_name): +def safe_filepath(file_path_name, dir_sep=None): ''' Input the full path and filename, splits on directory separator and calls safe_filename_leaf for - each part of the path. + each part of the path. dir_sep allows coder to force a directory separate to a particular character .. versionadded:: 2017.7.2 + + :codeauthor: Damon Atkins ''' + if not dir_sep: + dir_sep = os.sep + # Normally if file_path_name or dir_sep is Unicode then the output will be Unicode + # This code ensure the output type is the same as file_path_name + if not isinstance(file_path_name, six.text_type) and isinstance(dir_sep, six.text_type): + dir_sep = dir_sep.encode('ascii') # This should not be executed under PY3 + # splitdrive only set drive on windows platform (drive, path) = os.path.splitdrive(file_path_name) - path = os.sep.join([safe_filename_leaf(file_section) for file_section in file_path_name.rsplit(os.sep)]) + path = dir_sep.join([safe_filename_leaf(file_section) for file_section in path.rsplit(dir_sep)]) if drive: - return os.sep.join([drive, path]) - else: - return path + path = dir_sep.join([drive, path]) + return path From d4236aeeb73224deb9d5758ea9aae21d07f16ff5 Mon Sep 17 00:00:00 2001 From: Joaquin Veira Date: Mon, 18 Sep 2017 08:43:54 +0200 Subject: [PATCH 092/633] Update zabbix_return.py forgot to change variable f for file_handle --- salt/returners/zabbix_return.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/returners/zabbix_return.py b/salt/returners/zabbix_return.py index be4f71cf48..bdf94d9749 100644 --- a/salt/returners/zabbix_return.py +++ b/salt/returners/zabbix_return.py @@ -57,7 +57,7 @@ def zbx(): def zabbix_send(key, host, output): with salt.utils.fopen(zbx()['zabbix_config'],'r') as file_handle: - for line in f: + for line in file_handle: if "ServerActive" in line: flag = "true" server = line.rsplit('=') From 00e9637738ffbcb0e532bf67bc644a0125b3d3ef Mon Sep 17 00:00:00 2001 From: assaf shapira Date: Mon, 18 Sep 2017 12:43:53 +0300 Subject: [PATCH 093/633] corrected lint errors --- salt/cloud/clouds/xen.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/salt/cloud/clouds/xen.py b/salt/cloud/clouds/xen.py index dffff9aa4b..558c7cacb6 100644 --- a/salt/cloud/clouds/xen.py +++ b/salt/cloud/clouds/xen.py @@ -157,7 +157,8 @@ def _get_session(): user, 'XXX-pw-redacted-XXX', originator)) - session.xenapi.login_with_password(user, password, api_version, originator) + session.xenapi.login_with_password( + user, password, api_version, originator) except XenAPI.Failure as ex: ''' if the server on the url is not the pool master, the pool master's @@ -172,7 +173,8 @@ def _get_session(): user, 'XXX-pw-redacted-XXX', originator)) - session.xenapi.login_with_password(user,password,api_version,originator) + session.xenapi.login_with_password( + user, password, api_version, originator) return session @@ -198,12 +200,12 @@ def list_nodes(): log.debug('VM {}, doesnt have base_template_name attribute'.format( record['name_label'])) ret[record['name_label']] = {'id': record['uuid'], - 'image': base_template_name, - 'name': record['name_label'], - 'size': record['memory_dynamic_max'], - 'state': record['power_state'], - 'private_ips': get_vm_ip(record['name_label'], session), - 'public_ips': None} + 'image': base_template_name, + 'name': record['name_label'], + 'size': record['memory_dynamic_max'], + 'state': record['power_state'], + 'private_ips': get_vm_ip(record['name_label'], session), + 'public_ips': None} return ret From 60e6958bd15c2287f656bb3b22be051b29993e39 Mon Sep 17 00:00:00 2001 From: Levi Dahl Michelsen Date: Mon, 18 Sep 2017 12:09:57 +0200 Subject: [PATCH 094/633] Added versionadded comment --- salt/pillar/rethinkdb_pillar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/pillar/rethinkdb_pillar.py b/salt/pillar/rethinkdb_pillar.py index 0a4793205f..309fcaf7ef 100644 --- a/salt/pillar/rethinkdb_pillar.py +++ b/salt/pillar/rethinkdb_pillar.py @@ -2,6 +2,8 @@ ''' Provide external pillar data from RethinkDB +.. versionadded:: Oxygen + :depends: rethinkdb (on the salt-master) From 36429003945f7c2ead45aa223e9f4b21d2af574c Mon Sep 17 00:00:00 2001 From: Levi Dahl Michelsen Date: Mon, 18 Sep 2017 12:11:17 +0200 Subject: [PATCH 095/633] Fixed indentation mismatch in ext_pillar docstring --- salt/pillar/rethinkdb_pillar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/pillar/rethinkdb_pillar.py b/salt/pillar/rethinkdb_pillar.py index 309fcaf7ef..8f600d809e 100644 --- a/salt/pillar/rethinkdb_pillar.py +++ b/salt/pillar/rethinkdb_pillar.py @@ -84,7 +84,7 @@ def ext_pillar(minion_id, ''' Collect minion external pillars from a RethinkDB database -Arguments: + Arguments: * `table`: The RethinkDB table containing external pillar information. Defaults to ``'pillar'`` * `id_field`: Field in document containing the minion id. From df60501a80a3228dc5cda6e24e0ce188ea7de21a Mon Sep 17 00:00:00 2001 From: Levi Dahl Michelsen Date: Mon, 18 Sep 2017 12:16:18 +0200 Subject: [PATCH 096/633] Removed import shorthand name for rethinkdb module --- salt/pillar/rethinkdb_pillar.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/salt/pillar/rethinkdb_pillar.py b/salt/pillar/rethinkdb_pillar.py index 8f600d809e..bf7c816221 100644 --- a/salt/pillar/rethinkdb_pillar.py +++ b/salt/pillar/rethinkdb_pillar.py @@ -49,7 +49,7 @@ import logging # Import 3rd party libraries try: - import rethinkdb as r + import rethinkdb HAS_RETHINKDB = True except ImportError: HAS_RETHINKDB = False @@ -105,11 +105,11 @@ def ext_pillar(minion_id, .format(host, port, username)) # Connect to the database - conn = r.connect(host=host, - port=port, - db=database, - user=username, - password=password) + conn = rethinkdb.connect(host=host, + port=port, + db=database, + user=username, + password=password) data = None @@ -121,10 +121,11 @@ def ext_pillar(minion_id, table, id_field, minion_id)) if field: - data = r.table(table).filter( + data = rethinkdb.table(table).filter( {id_field: minion_id}).pluck(field).run(conn) else: - data = r.table(table).filter({id_field: minion_id}).run(conn) + data = rethinkdb.table(table).filter( + {id_field: minion_id}).run(conn) else: log.debug('ext_pillar.rethinkdb: looking up pillar. ' @@ -132,9 +133,10 @@ def ext_pillar(minion_id, table, minion_id)) if field: - data = r.table(table).get(minion_id).pluck(field).run(conn) + data = rethinkdb.table(table).get(minion_id).pluck(field).run( + conn) else: - data = r.table(table).get(minion_id).run(conn) + data = rethinkdb.table(table).get(minion_id).run(conn) finally: if conn.is_open(): @@ -158,4 +160,4 @@ def ext_pillar(minion_id, else: # No document found in the database log.debug('ext_pillar.rethinkdb: no document found') - return {} + return {} \ No newline at end of file From fc269b06843af997588adc100f114dc41f7d38b8 Mon Sep 17 00:00:00 2001 From: Vladimir Nadvornik Date: Mon, 18 Sep 2017 15:12:11 +0200 Subject: [PATCH 097/633] Add missing devices to RAID array Implements #40100 --- salt/modules/mdadm.py | 36 +++++++++++++ salt/states/mdadm.py | 119 +++++++++++++++++++++++++++++++----------- 2 files changed, 124 insertions(+), 31 deletions(-) diff --git a/salt/modules/mdadm.py b/salt/modules/mdadm.py index 0b453a2689..334cd46e73 100644 --- a/salt/modules/mdadm.py +++ b/salt/modules/mdadm.py @@ -356,3 +356,39 @@ def assemble(name, return cmd elif test_mode is False: return __salt__['cmd.run'](cmd, python_shell=False) + +def examine(device): + ''' + Show detail for a specified RAID component device + + CLI Example: + + .. code-block:: bash + + salt '*' raid.examine '/dev/sda1' + ''' + res = __salt__['cmd.run_stdout']('mdadm -Y -E {0}'.format(device), output_loglevel='trace', python_shell=False) + ret = {} + + for line in res.splitlines(): + name, var = line.partition("=")[::2] + ret[name] = var + return ret + + +def add(name, device): + ''' + Add new device to RAID array. + + CLI Example: + + .. code-block:: bash + + salt '*' raid.add /dev/md0 /dev/sda1 + + ''' + + cmd = 'mdadm --manage {0} --add {1}'.format(name, device) + if __salt__['cmd.retcode'](cmd) == 0: + return True + return False diff --git a/salt/states/mdadm.py b/salt/states/mdadm.py index 2b1c834087..067c5c4c2f 100644 --- a/salt/states/mdadm.py +++ b/salt/states/mdadm.py @@ -88,69 +88,126 @@ def present(name, # Device exists raids = __salt__['raid.list']() - if raids.get(name): - ret['comment'] = 'Raid {0} already present'.format(name) - return ret + present = raids.get(name) # Decide whether to create or assemble - can_assemble = {} - for dev in devices: - # mdadm -E exits with 0 iff all devices given are part of an array - cmd = 'mdadm -E {0}'.format(dev) - can_assemble[dev] = __salt__['cmd.retcode'](cmd) == 0 + missing = [] + uuid_dict = {} + new_devices = [] - if True in six.itervalues(can_assemble) and False in six.itervalues(can_assemble): - in_raid = sorted([x[0] for x in six.iteritems(can_assemble) if x[1]]) - not_in_raid = sorted([x[0] for x in six.iteritems(can_assemble) if not x[1]]) - ret['comment'] = 'Devices are a mix of RAID constituents ({0}) and '\ - 'non-RAID-constituents({1}).'.format(in_raid, not_in_raid) + for dev in devices: + if dev == 'missing' or not __salt__['file.access'](dev, 'f'): + missing.append(dev) + continue + superblock = __salt__['raid.examine'](dev) + + if 'MD_UUID' in superblock: + uuid = superblock['MD_UUID'] + if uuid not in uuid_dict: + uuid_dict[uuid] = [] + uuid_dict[uuid].append(dev) + else: + new_devices.append(dev) + + if len(uuid_dict) > 1: + ret['comment'] = 'Devices are a mix of RAID constituents with multiple MD_UUIDs: {0}.'.format(uuid_dict.keys()) ret['result'] = False return ret - elif next(six.itervalues(can_assemble)): + elif len(uuid_dict) == 1: + uuid = uuid_dict.keys()[0] + if present and present['uuid'] != uuid: + ret['comment'] = 'Devices MD_UUIDs: {0} differs from present RAID uuid {1}.'.format(uuid, present['uuid']) + ret['result'] = False + return ret + + devices_with_superblock = uuid_dict[uuid] + else: + devices_with_superblock = [] + + if present: + do_assemble = False + do_create = False + elif len(devices_with_superblock) > 0: do_assemble = True + do_create = False verb = 'assembled' else: + if len(new_devices) == 0: + ret['comment'] = 'All devices are missing: {0}.'.format(missing) + ret['result'] = False + return ret do_assemble = False + do_create = True verb = 'created' # If running with test use the test_mode with create or assemble if __opts__['test']: if do_assemble: res = __salt__['raid.assemble'](name, - devices, + devices_with_superblock, test_mode=True, **kwargs) - else: + elif do_create: res = __salt__['raid.create'](name, level, - devices, + new_devices + ['missing'] * len(missing), test_mode=True, **kwargs) - ret['comment'] = 'Raid will be {0} with: {1}'.format(verb, res) - ret['result'] = None + + if present: + ret['comment'] = 'Raid {0} already present.'.format(name) + + if do_assemble or do_create: + ret['comment'] = 'Raid will be {0} with: {1}'.format(verb, res) + ret['result'] = None + + if (do_assemble or present) and len(new_devices) > 0: + ret['comment'] += ' New devices will be added: {0}'.format(new_devices) + ret['result'] = None + + if len(missing) > 0: + ret['comment'] += ' Missing devices: {0}'.format(missing) + return ret # Attempt to create or assemble the array if do_assemble: __salt__['raid.assemble'](name, - devices, + devices_with_superblock, **kwargs) - else: + elif do_create: __salt__['raid.create'](name, level, - devices, + new_devices + ['missing'] * len(missing), **kwargs) - raids = __salt__['raid.list']() - changes = raids.get(name) - if changes: - ret['comment'] = 'Raid {0} {1}.'.format(name, verb) - ret['changes'] = changes - # Saving config - __salt__['raid.save_config']() + if not present: + raids = __salt__['raid.list']() + changes = raids.get(name) + if changes: + ret['comment'] = 'Raid {0} {1}.'.format(name, verb) + ret['changes'] = changes + # Saving config + __salt__['raid.save_config']() + else: + ret['comment'] = 'Raid {0} failed to be {1}.'.format(name, verb) + ret['result'] = False else: - ret['comment'] = 'Raid {0} failed to be {1}.'.format(name, verb) - ret['result'] = False + ret['comment'] = 'Raid {0} already present.'.format(name) + + if (do_assemble or present) and len(new_devices) > 0: + for d in new_devices: + res = __salt__['raid.add'](name, d) + if not res: + ret['comment'] += ' Unable to add {0} to {1}.\n'.format(d, name) + ret['result'] = False + else: + ret['comment'] += ' Added new device {0} to {1}.\n'.format(d, name) + if ret['result']: + ret['changes']['added'] = new_devices + + if len(missing) > 0: + ret['comment'] += ' Missing devices: {0}'.format(missing) return ret From 21966e7ce82d9c1f565ea5fb76320202e74d64d0 Mon Sep 17 00:00:00 2001 From: Denys Havrysh Date: Mon, 18 Sep 2017 16:34:59 +0300 Subject: [PATCH 098/633] cloud.action: list_nodes_min returns all instances --- salt/cloud/clouds/ec2.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/salt/cloud/clouds/ec2.py b/salt/cloud/clouds/ec2.py index f47d2d93c3..a6382a0860 100644 --- a/salt/cloud/clouds/ec2.py +++ b/salt/cloud/clouds/ec2.py @@ -3472,16 +3472,15 @@ def list_nodes_min(location=None, call=None): for instance in instances: if isinstance(instance['instancesSet']['item'], list): - for item in instance['instancesSet']['item']: - state = item['instanceState']['name'] - name = _extract_name_tag(item) - id = item['instanceId'] + items = instance['instancesSet']['item'] else: - item = instance['instancesSet']['item'] + items = [instance['instancesSet']['item']] + + for item in items: state = item['instanceState']['name'] name = _extract_name_tag(item) id = item['instanceId'] - ret[name] = {'state': state, 'id': id} + ret[name] = {'state': state, 'id': id} return ret From fb579321a912ee3615284d77f37c61a60e3a1338 Mon Sep 17 00:00:00 2001 From: Sergey Kizunov Date: Fri, 15 Sep 2017 10:00:17 -0500 Subject: [PATCH 099/633] Add back lost logic for multifunc_ordered PR #38168 was merged but some of the merged logic was subseqently lost. Add back the lost logic so that the feature may work again. Signed-off-by: Sergey Kizunov --- salt/minion.py | 50 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/salt/minion.py b/salt/minion.py index 6b7c82a8d7..c56010bad9 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -1600,13 +1600,24 @@ class Minion(MinionBase): minion side execution. ''' salt.utils.appendproctitle('{0}._thread_multi_return {1}'.format(cls.__name__, data['jid'])) - ret = { - 'return': {}, - 'retcode': {}, - 'success': {} - } - for ind in range(0, len(data['fun'])): - ret['success'][data['fun'][ind]] = False + multifunc_ordered = opts.get('multifunc_ordered', False) + num_funcs = len(data['fun']) + if multifunc_ordered: + ret = { + 'return': [None] * num_funcs, + 'retcode': [None] * num_funcs, + 'success': [False] * num_funcs + } + else: + ret = { + 'return': {}, + 'retcode': {}, + 'success': {} + } + + for ind in range(0, num_funcs): + if not multifunc_ordered: + ret['success'][data['fun'][ind]] = False try: if minion_instance.connected and minion_instance.opts['pillar'].get('minion_blackout', False): # this minion is blacked out. Only allow saltutil.refresh_pillar @@ -1621,12 +1632,20 @@ class Minion(MinionBase): data['arg'][ind], data) minion_instance.functions.pack['__context__']['retcode'] = 0 - ret['return'][data['fun'][ind]] = func(*args, **kwargs) - ret['retcode'][data['fun'][ind]] = minion_instance.functions.pack['__context__'].get( - 'retcode', - 0 - ) - ret['success'][data['fun'][ind]] = True + if multifunc_ordered: + ret['return'][ind] = func(*args, **kwargs) + ret['retcode'][ind] = minion_instance.functions.pack['__context__'].get( + 'retcode', + 0 + ) + ret['success'][ind] = True + else: + ret['return'][data['fun'][ind]] = func(*args, **kwargs) + ret['retcode'][data['fun'][ind]] = minion_instance.functions.pack['__context__'].get( + 'retcode', + 0 + ) + ret['success'][data['fun'][ind]] = True except Exception as exc: trb = traceback.format_exc() log.warning( @@ -1634,7 +1653,10 @@ class Minion(MinionBase): exc ) ) - ret['return'][data['fun'][ind]] = trb + if multifunc_ordered: + ret['return'][ind] = trb + else: + ret['return'][data['fun'][ind]] = trb ret['jid'] = data['jid'] ret['fun'] = data['fun'] ret['fun_args'] = data['arg'] From 9fe32f8b6e6e6075346bf4754bea17400ea8ef42 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Mon, 28 Aug 2017 12:46:26 +0300 Subject: [PATCH 100/633] Regex support for user names in external_auth config. --- salt/auth/__init__.py | 41 ++++------------------------------------- salt/config/__init__.py | 5 +++++ salt/utils/minions.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/salt/auth/__init__.py b/salt/auth/__init__.py index 73e4c98f8a..b24bbd4926 100644 --- a/salt/auth/__init__.py +++ b/salt/auth/__init__.py @@ -377,46 +377,13 @@ class LoadAuth(object): eauth_config = self.opts['external_auth'][eauth] if not groups: groups = [] - group_perm_keys = [item for item in eauth_config if item.endswith('%')] # The configured auth groups - - # First we need to know if the user is allowed to proceed via any of their group memberships. - group_auth_match = False - for group_config in group_perm_keys: - if group_config.rstrip('%') in groups: - group_auth_match = True - break - # If a group_auth_match is set it means only that we have a - # user which matches at least one or more of the groups defined - # in the configuration file. - - external_auth_in_db = False - for entry in eauth_config: - if entry.startswith('^'): - external_auth_in_db = True - break - - # If neither a catchall, a named membership or a group - # membership is found, there is no need to continue. Simply - # deny the user access. - if not ((name in eauth_config) | - ('*' in eauth_config) | - group_auth_match | external_auth_in_db): - # Auth successful, but no matching user found in config - log.warning('Authorization failure occurred.') - return None # We now have an authenticated session and it is time to determine # what the user has access to. - auth_list = [] - if name in eauth_config: - auth_list = eauth_config[name] - elif '*' in eauth_config: - auth_list = eauth_config['*'] - if group_auth_match: - auth_list = self.ckminions.fill_auth_list_from_groups( - eauth_config, - groups, - auth_list) + auth_list = self.ckminions.fill_auth_list( + eauth_config, + name, + groups) auth_list = self.__process_acl(load, auth_list) diff --git a/salt/config/__init__.py b/salt/config/__init__.py index e4982744cd..c558768d1d 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -717,6 +717,10 @@ VALID_OPTS = { 'fileserver_limit_traversal': bool, 'fileserver_verify_config': bool, + # Optionally apply '*' permissioins to any user. By default '*' is a fallback case that is + # applied only if the user didn't matched by other matchers. + 'permissive_acl': bool, + # Optionally enables keeping the calculated user's auth list in the token file. 'keep_acl_in_token': bool, @@ -1466,6 +1470,7 @@ DEFAULT_MASTER_OPTS = { 'external_auth': {}, 'token_expire': 43200, 'token_expire_user_override': False, + 'permissive_acl': False, 'keep_acl_in_token': False, 'eauth_acl_module': '', 'extension_modules': os.path.join(salt.syspaths.CACHE_DIR, 'master', 'extmods'), diff --git a/salt/utils/minions.py b/salt/utils/minions.py index 8afa41698c..f84ad50e1d 100644 --- a/salt/utils/minions.py +++ b/salt/utils/minions.py @@ -985,10 +985,37 @@ class CkMinions(object): auth_list.append(matcher) return auth_list + def fill_auth_list(self, auth_provider, name, groups, auth_list=None, permissive=None): + ''' + Returns a list of authorisation matchers that a user is eligible for. + This list is a combination of the provided personal matchers plus the + matchers of any group the user is in. + ''' + if auth_list is None: + auth_list = [] + if permissive is None: + permissive = self.opts.get('permissive_acl') + name_matched = False + for match in auth_provider: + if match == '*' and not permissive: + continue + if match.endswith('%'): + if match.rstrip('%') in groups: + auth_list.extend(auth_provider[match]) + else: + if salt.utils.expr_match(match, name): + name_matched = True + auth_list.extend(auth_provider[match]) + if not permissive and not name_matched and '*' in auth_provider: + auth_list.extend(auth_provider['*']) + return auth_list + def wheel_check(self, auth_list, fun): ''' Check special API permissions ''' + if not auth_list: + return False comps = fun.split('.') if len(comps) != 2: return False @@ -1020,6 +1047,8 @@ class CkMinions(object): ''' Check special API permissions ''' + if not auth_list: + return False comps = fun.split('.') if len(comps) != 2: return False @@ -1051,6 +1080,8 @@ class CkMinions(object): ''' Check special API permissions ''' + if not auth_list: + return False if form != 'cloud': comps = fun.split('.') if len(comps) != 2: From 14bf2dd8fff191fbbb73c7a3e5b9b570de250385 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Tue, 12 Sep 2017 23:10:06 +0300 Subject: [PATCH 101/633] Support regex in publisher_acl. --- doc/ref/publisheracl.rst | 3 +++ salt/daemons/masterapi.py | 31 +++++++++++++++++++------------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/doc/ref/publisheracl.rst b/doc/ref/publisheracl.rst index eda868b5d2..5549c3c92a 100644 --- a/doc/ref/publisheracl.rst +++ b/doc/ref/publisheracl.rst @@ -25,6 +25,9 @@ configuration: - web*: - test.* - pkg.* + # Allow managers to use saltutil module functions + manager_.*: + - saltutil.* Permission Issues ----------------- diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index d47a5c3aa6..f501f41938 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -204,6 +204,14 @@ def clean_old_jobs(opts): def mk_key(opts, user): + if HAS_PWD: + uid = None + try: + uid = pwd.getpwnam(user).pw_uid + except KeyError: + # User doesn't exist in the system + if opts['client_acl_verify']: + return None if salt.utils.is_windows(): # The username may contain '\' if it is in Windows # 'DOMAIN\username' format. Fix this for the keyfile path. @@ -231,9 +239,9 @@ def mk_key(opts, user): # Write access is necessary since on subsequent runs, if the file # exists, it needs to be written to again. Windows enforces this. os.chmod(keyfile, 0o600) - if HAS_PWD: + if HAS_PWD and uid is not None: try: - os.chown(keyfile, pwd.getpwnam(user).pw_uid, -1) + os.chown(keyfile, uid, -1) except OSError: # The master is not being run as root and can therefore not # chown the key file @@ -248,27 +256,26 @@ def access_keys(opts): ''' # TODO: Need a way to get all available users for systems not supported by pwd module. # For now users pattern matching will not work for publisher_acl. - users = [] keys = {} publisher_acl = opts['publisher_acl'] acl_users = set(publisher_acl.keys()) if opts.get('user'): acl_users.add(opts['user']) acl_users.add(salt.utils.get_user()) + for user in acl_users: + log.info('Preparing the %s key for local communication', user) + key = mk_key(opts, user) + if key is not None: + keys[user] = key + + # Check other users matching ACL patterns if opts['client_acl_verify'] and HAS_PWD: log.profile('Beginning pwd.getpwall() call in masterarpi access_keys function') for user in pwd.getpwall(): - users.append(user.pw_name) - log.profile('End pwd.getpwall() call in masterarpi access_keys function') - for user in acl_users: - log.info('Preparing the %s key for local communication', user) - keys[user] = mk_key(opts, user) - - # Check other users matching ACL patterns - if HAS_PWD: - for user in users: + user = user.pw_name if user not in keys and salt.utils.check_whitelist_blacklist(user, whitelist=acl_users): keys[user] = mk_key(opts, user) + log.profile('End pwd.getpwall() call in masterarpi access_keys function') return keys From b1b4dafd396c8a92c83f6e3219b4cf05bab38b2a Mon Sep 17 00:00:00 2001 From: Andrew Colin Kissa Date: Mon, 18 Sep 2017 18:05:55 +0200 Subject: [PATCH 102/633] Fix CSR not recreated if key changes --- salt/modules/x509.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/modules/x509.py b/salt/modules/x509.py index ae5f8c7723..b63188dd7d 100644 --- a/salt/modules/x509.py +++ b/salt/modules/x509.py @@ -625,6 +625,8 @@ def read_csr(csr): # Get size returns in bytes. The world thinks of key sizes in bits. 'Subject': _parse_subject(csr.get_subject()), 'Subject Hash': _dec2hex(csr.get_subject().as_hash()), + 'Public Key Hash': hashlib.sha1(csr.get_pubkey().get_modulus())\ + .hexdigest() } ret['X509v3 Extensions'] = _get_csr_extensions(csr) From 117a0ddbbc11ff35dfce7e48f3519e82943b7865 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Mon, 18 Sep 2017 11:09:36 -0700 Subject: [PATCH 103/633] Updating the documentation to call out the requirement for the getfacl and setfacl binaries --- salt/modules/linux_acl.py | 3 +++ salt/states/linux_acl.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/salt/modules/linux_acl.py b/salt/modules/linux_acl.py index a7fa3cbd1c..5969b24ea9 100644 --- a/salt/modules/linux_acl.py +++ b/salt/modules/linux_acl.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- ''' Support for Linux File Access Control Lists + +The Linux ACL module requires the `getfacl` and `setfacl` binaries. + ''' from __future__ import absolute_import diff --git a/salt/states/linux_acl.py b/salt/states/linux_acl.py index a6a54a7fcd..4e3c7049b9 100644 --- a/salt/states/linux_acl.py +++ b/salt/states/linux_acl.py @@ -2,6 +2,8 @@ ''' Linux File Access Control Lists +The Linux ACL state module requires the `getfacl` and `setfacl` binaries. + Ensure a Linux ACL is present .. code-block:: yaml From 1a619708c1ab5f1c7383dee12bc8274c302fb8ad Mon Sep 17 00:00:00 2001 From: Mike Place Date: Mon, 18 Sep 2017 13:44:44 -0600 Subject: [PATCH 104/633] Enhance engines docs Add a note about formatting to make it more clear. --- doc/topics/engines/index.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/topics/engines/index.rst b/doc/topics/engines/index.rst index 5fbc99a0b9..c9a8ef5235 100644 --- a/doc/topics/engines/index.rst +++ b/doc/topics/engines/index.rst @@ -27,7 +27,12 @@ Salt engines are configured under an ``engines`` top-level section in your Salt port: 5959 proto: tcp -Salt engines must be in the Salt path, or you can add the ``engines_dirs`` option in your Salt master configuration with a list of directories under which Salt attempts to find Salt engines. +Salt engines must be in the Salt path, or you can add the ``engines_dirs`` option in your Salt master configuration with a list of directories under which Salt attempts to find Salt engines. This option should be formatted as a list of directories to search, such as: + +.. code-block:: yaml + + engines_dirs: + - /home/bob/engines Writing an Engine ================= From 4afb179bade9834e8d9980a66a978209e439d3a3 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 28 Aug 2017 19:33:06 -0500 Subject: [PATCH 105/633] Un-deprecate passing kwargs outside of 'kwarg' param --- salt/client/mixins.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/salt/client/mixins.py b/salt/client/mixins.py index f5a29a9cbf..bd69d269bf 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -359,29 +359,20 @@ class SyncClientMixin(object): # packed into the top level object. The plan is to move away from # that since the caller knows what is an arg vs a kwarg, but while # we make the transition we will load "kwargs" using format_call if - # there are no kwargs in the low object passed in - f_call = None - if 'arg' not in low: - f_call = salt.utils.format_call( + # there are no kwargs in the low object passed in. + f_call = {} if 'arg' in low and 'kwarg' in low \ + else salt.utils.format_call( self.functions[fun], low, expected_extra_kws=CLIENT_INTERNAL_KEYWORDS ) - args = f_call.get('args', ()) - else: - args = low['arg'] - if 'kwarg' not in low: - log.critical( - 'kwargs must be passed inside the low data within the ' - '\'kwarg\' key. See usage of ' - 'salt.utils.args.parse_input() and ' - 'salt.minion.load_args_and_kwargs() elsewhere in the ' - 'codebase.' - ) - kwargs = {} - else: - kwargs = low['kwarg'] + args = f_call.get('args', ()) \ + if 'arg' not in low \ + else low['arg'] + kwargs = f_call.get('kwargs', {}) \ + if 'kwarg' not in low \ + else low['kwarg'] # Update the event data with loaded args and kwargs data['fun_args'] = list(args) + ([kwargs] if kwargs else []) From 2a35ab7f39846f695b27a347dc189b7cd2307112 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 31 Aug 2017 00:24:11 -0500 Subject: [PATCH 106/633] Unify reactor configuration, fix caller reactors There are 4 types of reactor jobs, and 3 different config schemas for passing arguments: 1. local - positional and keyword args passed in arg/kwarg params, respectively. 2. runner/wheel - passed as individual params directly under the function name. 3. caller - only positional args supported, passed under an "args" param. In addition to being wildly inconsistent, there are several problems with each of the above approaches: - For local jobs, having to know which are positional and keyword arguments is not user-friendly. - For runner/wheel jobs, the fact that the arguments are all passed in the level directly below the function name means that they are dumped directly into the low chunk. This means that if any arguments are passed which conflict with the reserved keywords in the low chunk (name, order, etc.), they will override their counterparts in the low chunk, which may make the Reactor behave unpredictably. To solve these issues, this commit makes the following changes: 1. A new, unified configuration schema has been added, so that arguments are passed identically across all types of reactions. In this new schema, all arguments are passed as named arguments underneath an "args" parameter. Those named arguments are then passed as keyword arguments to the desired function. This works even for positional arguments because Python will automagically pass a keyword argument as its positional counterpart when the name of a positional argument is found in the kwargs. 2. The caller jobs now support both positional and keyword arguments. Backward-compatibility with the old configuration schema has been preserved, so old Reactor SLS files do not break. In addition, you've probably already said to yourself "Hey, caller jobs were _already_ passing their arguments under an "args" param. What gives?" Well, using the old config schema, only positional arguments were supported. So if we detect a list of positional arguments, we treat the input as positional arguments (i.e. old schema), while if the input is a dictionary (or "dictlist"), we treat the input as kwargs (i.e. new schema). --- salt/utils/reactor.py | 231 +++++++++++++++++++++++++++++------------- 1 file changed, 159 insertions(+), 72 deletions(-) diff --git a/salt/utils/reactor.py b/salt/utils/reactor.py index 57c4fd0863..36971f5c36 100644 --- a/salt/utils/reactor.py +++ b/salt/utils/reactor.py @@ -7,12 +7,14 @@ import glob import logging # Import salt libs +import salt.client import salt.runner import salt.state import salt.utils import salt.utils.cache import salt.utils.event import salt.utils.process +import salt.wheel import salt.defaults.exitcodes # Import 3rd-party libs @@ -21,6 +23,15 @@ import salt.ext.six as six log = logging.getLogger(__name__) +REACTOR_INTERNAL_KEYWORDS = frozenset([ + '__id__', + '__sls__', + 'name', + 'order', + 'fun', + 'state', +]) + class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.state.Compiler): ''' @@ -29,6 +40,10 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat The reactor has the capability to execute pre-programmed executions as reactions to events ''' + aliases = { + 'cmd': 'local', + } + def __init__(self, opts, log_queue=None): super(Reactor, self).__init__(log_queue=log_queue) local_minion_opts = opts.copy() @@ -171,6 +186,16 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat return {'status': False, 'comment': 'Reactor does not exists.'} + def resolve_aliases(self, chunks): + ''' + Preserve backward compatibility by rewriting the 'state' key in the low + chunks if it is using a legacy type. + ''' + for idx, _ in enumerate(chunks): + new_state = self.aliases.get(chunks[idx]['state']) + if new_state is not None: + chunks[idx]['state'] = new_state + def reactions(self, tag, data, reactors): ''' Render a list of reactor files and returns a reaction struct @@ -191,6 +216,7 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat except Exception as exc: log.error('Exception trying to compile reactions: {0}'.format(exc), exc_info=True) + self.resolve_aliases(chunks) return chunks def call_reactions(self, chunks): @@ -248,12 +274,19 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat class ReactWrap(object): ''' - Create a wrapper that executes low data for the reaction system + Wrapper that executes low data for the Reactor System ''' # class-wide cache of clients client_cache = None event_user = 'Reactor' + reaction_class = { + 'local': salt.client.LocalClient, + 'runner': salt.runner.RunnerClient, + 'wheel': salt.wheel.Wheel, + 'caller': salt.client.Caller, + } + def __init__(self, opts): self.opts = opts if ReactWrap.client_cache is None: @@ -264,21 +297,49 @@ class ReactWrap(object): queue_size=self.opts['reactor_worker_hwm'] # queue size for those workers ) + def populate_client_cache(self, low): + ''' + Populate the client cache with an instance of the specified type + ''' + reaction_type = low['state'] + if reaction_type not in self.client_cache: + log.debug('Reactor is populating %s client cache', reaction_type) + if reaction_type in ('runner', 'wheel'): + # Reaction types that run locally on the master want the full + # opts passed. + self.client_cache[reaction_type] = \ + self.reaction_class[reaction_type](self.opts) + # The len() function will cause the module functions to load if + # they aren't already loaded. We want to load them so that the + # spawned threads don't need to load them. Loading in the + # spawned threads creates race conditions such as sometimes not + # finding the required function because another thread is in + # the middle of loading the functions. + len(self.client_cache[reaction_type].functions) + else: + # Reactions which use remote pubs only need the conf file when + # instantiating a client instance. + self.client_cache[reaction_type] = \ + self.reaction_class[reaction_type](self.opts['conf_file']) + def run(self, low): ''' - Execute the specified function in the specified state by passing the - low data + Execute a reaction by invoking the proper wrapper func ''' - l_fun = getattr(self, low['state']) + self.populate_client_cache(low) try: - f_call = salt.utils.format_call(l_fun, low) - kwargs = f_call.get('kwargs', {}) - if 'arg' not in kwargs: - kwargs['arg'] = [] - if 'kwarg' not in kwargs: - kwargs['kwarg'] = {} + l_fun = getattr(self, low['state']) + except AttributeError: + log.error( + 'ReactWrap is missing a wrapper function for \'%s\'', + low['state'] + ) - # TODO: Setting the user doesn't seem to work for actual remote publishes + try: + wrap_call = salt.utils.format_call(l_fun, low) + args = wrap_call.get('args', ()) + kwargs = wrap_call.get('kwargs', {}) + # TODO: Setting user doesn't seem to work for actual remote pubs if low['state'] in ('runner', 'wheel'): # Update called function's low data with event user to # segregate events fired by reactor and avoid reaction loops @@ -286,80 +347,106 @@ class ReactWrap(object): # Replace ``state`` kwarg which comes from high data compiler. # It breaks some runner functions and seems unnecessary. kwargs['__state__'] = kwargs.pop('state') + # NOTE: if any additional keys are added here, they will also + # need to be added to filter_kwargs() - l_fun(*f_call.get('args', ()), **kwargs) + if 'args' in kwargs: + # New configuration + reactor_args = kwargs.pop('args') + for item in ('arg', 'kwarg'): + if item in low: + log.warning( + 'Reactor \'%s\' is ignoring \'%s\' param %s due to ' + 'presence of \'args\' param. Check the Reactor System ' + 'documentation for the correct argument format.', + low['__id__'], item, low[item] + ) + if low['state'] == 'caller' \ + and isinstance(reactor_args, list) \ + and not salt.utils.is_dictlist(reactor_args): + # Legacy 'caller' reactors were already using the 'args' + # param, but only supported a list of positional arguments. + # If low['args'] is a list but is *not* a dictlist, then + # this is actually using the legacy configuration. So, put + # the reactor args into kwarg['arg'] so that the wrapper + # interprets them as positional args. + kwargs['arg'] = reactor_args + kwargs['kwarg'] = {} + else: + kwargs['arg'] = () + kwargs['kwarg'] = reactor_args + if not isinstance(kwargs['kwarg'], dict): + kwargs['kwarg'] = salt.utils.repack_dictlist(kwargs['kwarg']) + if not kwargs['kwarg']: + log.error( + 'Reactor \'%s\' failed to execute %s \'%s\': ' + 'Incorrect argument format, check the Reactor System ' + 'documentation for the correct format.', + low['__id__'], low['state'], low['fun'] + ) + return + else: + # Legacy configuration + react_call = {} + if low['state'] in ('runner', 'wheel'): + if 'arg' not in kwargs or 'kwarg' not in kwargs: + # Runner/wheel execute on the master, so we can use + # format_call to get the functions args/kwargs + react_fun = self.client_cache[low['state']].functions.get(low['fun']) + if react_fun is None: + log.error( + 'Reactor \'%s\' failed to execute %s \'%s\': ' + 'function not available', + low['__id__'], low['state'], low['fun'] + ) + return + + react_call = salt.utils.format_call( + react_fun, + low, + expected_extra_kws=REACTOR_INTERNAL_KEYWORDS + ) + + if 'arg' not in kwargs: + kwargs['arg'] = react_call.get('args', ()) + if 'kwarg' not in kwargs: + kwargs['kwarg'] = react_call.get('kwargs', {}) + + # Execute the wrapper with the proper args/kwargs. kwargs['arg'] + # and kwargs['kwarg'] contain the positional and keyword arguments + # that will be passed to the client interface to execute the + # desired runner/wheel/remote-exec/etc. function. + l_fun(*args, **kwargs) + except SystemExit: + log.warning( + 'Reactor \'%s\' attempted to exit. Ignored.', low['__id__'] + ) except Exception: log.error( - 'Failed to execute {0}: {1}\n'.format(low['state'], l_fun), - exc_info=True - ) - - def local(self, *args, **kwargs): - ''' - Wrap LocalClient for running :ref:`execution modules ` - ''' - if 'local' not in self.client_cache: - self.client_cache['local'] = salt.client.LocalClient(self.opts['conf_file']) - try: - self.client_cache['local'].cmd_async(*args, **kwargs) - except SystemExit: - log.warning('Attempt to exit reactor. Ignored.') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) - - cmd = local + 'Reactor \'%s\' failed to execute %s \'%s\'', + low['__id__'], low['state'], low['fun'], exc_info=True + ) def runner(self, fun, **kwargs): ''' Wrap RunnerClient for executing :ref:`runner modules ` ''' - if 'runner' not in self.client_cache: - self.client_cache['runner'] = salt.runner.RunnerClient(self.opts) - # The len() function will cause the module functions to load if - # they aren't already loaded. We want to load them so that the - # spawned threads don't need to load them. Loading in the spawned - # threads creates race conditions such as sometimes not finding - # the required function because another thread is in the middle - # of loading the functions. - len(self.client_cache['runner'].functions) - try: - self.pool.fire_async(self.client_cache['runner'].low, args=(fun, kwargs)) - except SystemExit: - log.warning('Attempt to exit in reactor by runner. Ignored') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) + self.pool.fire_async(self.client_cache['runner'].low, args=(fun, kwargs)) def wheel(self, fun, **kwargs): ''' Wrap Wheel to enable executing :ref:`wheel modules ` ''' - if 'wheel' not in self.client_cache: - self.client_cache['wheel'] = salt.wheel.Wheel(self.opts) - # The len() function will cause the module functions to load if - # they aren't already loaded. We want to load them so that the - # spawned threads don't need to load them. Loading in the spawned - # threads creates race conditions such as sometimes not finding - # the required function because another thread is in the middle - # of loading the functions. - len(self.client_cache['wheel'].functions) - try: - self.pool.fire_async(self.client_cache['wheel'].low, args=(fun, kwargs)) - except SystemExit: - log.warning('Attempt to in reactor by whell. Ignored.') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) + self.pool.fire_async(self.client_cache['wheel'].low, args=(fun, kwargs)) - def caller(self, fun, *args, **kwargs): + def local(self, fun, tgt, **kwargs): ''' - Wrap Caller to enable executing :ref:`caller modules ` + Wrap LocalClient for running :ref:`execution modules ` ''' - log.debug("in caller with fun {0} args {1} kwargs {2}".format(fun, args, kwargs)) - args = kwargs.get('args', []) - if 'caller' not in self.client_cache: - self.client_cache['caller'] = salt.client.Caller(self.opts['conf_file']) - try: - self.client_cache['caller'].function(fun, *args) - except SystemExit: - log.warning('Attempt to exit reactor. Ignored.') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) + self.client_cache['local'].cmd_async(tgt, fun, **kwargs) + + def caller(self, fun, **kwargs): + ''' + Wrap LocalCaller to execute remote exec functions locally on the Minion + ''' + self.client_cache['caller'].cmd(fun, *kwargs['arg'], **kwargs['kwarg']) From 531cac610e1d38d266f675dd92e930b0fc267ce0 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 31 Aug 2017 23:23:41 -0500 Subject: [PATCH 107/633] Rewrite the reactor unit tests These have been skipped for a while now because they didn't work correctly. The old tests have been scrapped in favor of new ones that test both the old and new config schema. --- tests/unit/utils/test_reactor.py | 602 ++++++++++++++++++++++++++++--- 1 file changed, 542 insertions(+), 60 deletions(-) diff --git a/tests/unit/utils/test_reactor.py b/tests/unit/utils/test_reactor.py index 7a96900977..5c86f766b4 100644 --- a/tests/unit/utils/test_reactor.py +++ b/tests/unit/utils/test_reactor.py @@ -1,74 +1,556 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -import time -import shutil -import tempfile +import codecs +import glob +import logging import os - -from contextlib import contextmanager +import textwrap +import yaml import salt.utils -from salt.utils.process import clean_proc +import salt.loader import salt.utils.reactor as reactor -from tests.integration import AdaptedConfigurationTestCaseMixin -from tests.support.paths import TMP from tests.support.unit import TestCase, skipIf -from tests.support.mock import patch, MagicMock +from tests.support.mixins import AdaptedConfigurationTestCaseMixin +from tests.support.mock import ( + NO_MOCK, + NO_MOCK_REASON, + patch, + MagicMock, + Mock, + mock_open, +) + +REACTOR_CONFIG = '''\ +reactor: + - old_runner: + - /srv/reactor/old_runner.sls + - old_wheel: + - /srv/reactor/old_wheel.sls + - old_local: + - /srv/reactor/old_local.sls + - old_cmd: + - /srv/reactor/old_cmd.sls + - old_caller: + - /srv/reactor/old_caller.sls + - new_runner: + - /srv/reactor/new_runner.sls + - new_wheel: + - /srv/reactor/new_wheel.sls + - new_local: + - /srv/reactor/new_local.sls + - new_cmd: + - /srv/reactor/new_cmd.sls + - new_caller: + - /srv/reactor/new_caller.sls +''' + +REACTOR_DATA = { + 'runner': {'data': {'message': 'This is an error'}}, + 'wheel': {'data': {'id': 'foo'}}, + 'local': {'data': {'pkg': 'zsh', 'repo': 'updates'}}, + 'cmd': {'data': {'pkg': 'zsh', 'repo': 'updates'}}, + 'caller': {'data': {'path': '/tmp/foo'}}, +} + +SLS = { + '/srv/reactor/old_runner.sls': textwrap.dedent('''\ + raise_error: + runner.error.error: + - name: Exception + - message: {{ data['data']['message'] }} + '''), + '/srv/reactor/old_wheel.sls': textwrap.dedent('''\ + remove_key: + wheel.key.delete: + - match: {{ data['data']['id'] }} + '''), + '/srv/reactor/old_local.sls': textwrap.dedent('''\ + install_zsh: + local.state.single: + - tgt: test + - arg: + - pkg.installed + - {{ data['data']['pkg'] }} + - kwarg: + fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/old_cmd.sls': textwrap.dedent('''\ + install_zsh: + cmd.state.single: + - tgt: test + - arg: + - pkg.installed + - {{ data['data']['pkg'] }} + - kwarg: + fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/old_caller.sls': textwrap.dedent('''\ + touch_file: + caller.file.touch: + - args: + - {{ data['data']['path'] }} + '''), + '/srv/reactor/new_runner.sls': textwrap.dedent('''\ + raise_error: + runner.error.error: + - args: + - name: Exception + - message: {{ data['data']['message'] }} + '''), + '/srv/reactor/new_wheel.sls': textwrap.dedent('''\ + remove_key: + wheel.key.delete: + - args: + - match: {{ data['data']['id'] }} + '''), + '/srv/reactor/new_local.sls': textwrap.dedent('''\ + install_zsh: + local.state.single: + - tgt: test + - args: + - fun: pkg.installed + - name: {{ data['data']['pkg'] }} + - fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/new_cmd.sls': textwrap.dedent('''\ + install_zsh: + cmd.state.single: + - tgt: test + - args: + - fun: pkg.installed + - name: {{ data['data']['pkg'] }} + - fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/new_caller.sls': textwrap.dedent('''\ + touch_file: + caller.file.touch: + - args: + - name: {{ data['data']['path'] }} + '''), +} + +LOW_CHUNKS = { + # Note that the "name" value in the chunk has been overwritten by the + # "name" argument in the SLS. This is one reason why the new schema was + # needed. + 'old_runner': [{ + 'state': 'runner', + '__id__': 'raise_error', + '__sls__': '/srv/reactor/old_runner.sls', + 'order': 1, + 'fun': 'error.error', + 'name': 'Exception', + 'message': 'This is an error', + }], + 'old_wheel': [{ + 'state': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/old_wheel.sls', + 'order': 1, + 'fun': 'key.delete', + 'match': 'foo', + }], + 'old_local': [{ + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_local.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }], + 'old_cmd': [{ + 'state': 'local', # 'cmd' should be aliased to 'local' + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_cmd.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }], + 'old_caller': [{ + 'state': 'caller', + '__id__': 'touch_file', + 'name': 'touch_file', + '__sls__': '/srv/reactor/old_caller.sls', + 'order': 1, + 'fun': 'file.touch', + 'args': ['/tmp/foo'], + }], + 'new_runner': [{ + 'state': 'runner', + '__id__': 'raise_error', + 'name': 'raise_error', + '__sls__': '/srv/reactor/new_runner.sls', + 'order': 1, + 'fun': 'error.error', + 'args': [ + {'name': 'Exception'}, + {'message': 'This is an error'}, + ], + }], + 'new_wheel': [{ + 'state': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/new_wheel.sls', + 'order': 1, + 'fun': 'key.delete', + 'args': [ + {'match': 'foo'}, + ], + }], + 'new_local': [{ + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_local.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'args': [ + {'fun': 'pkg.installed'}, + {'name': 'zsh'}, + {'fromrepo': 'updates'}, + ], + }], + 'new_cmd': [{ + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_cmd.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'args': [ + {'fun': 'pkg.installed'}, + {'name': 'zsh'}, + {'fromrepo': 'updates'}, + ], + }], + 'new_caller': [{ + 'state': 'caller', + '__id__': 'touch_file', + 'name': 'touch_file', + '__sls__': '/srv/reactor/new_caller.sls', + 'order': 1, + 'fun': 'file.touch', + 'args': [ + {'name': '/tmp/foo'}, + ], + }], +} + +WRAPPER_CALLS = { + 'old_runner': ( + 'error.error', + { + '__state__': 'runner', + '__id__': 'raise_error', + '__sls__': '/srv/reactor/old_runner.sls', + '__user__': 'Reactor', + 'order': 1, + 'arg': [], + 'kwarg': { + 'name': 'Exception', + 'message': 'This is an error', + }, + 'name': 'Exception', + 'message': 'This is an error', + }, + ), + 'old_wheel': ( + 'key.delete', + { + '__state__': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/old_wheel.sls', + 'order': 1, + '__user__': 'Reactor', + 'arg': ['foo'], + 'kwarg': {}, + 'match': 'foo', + }, + ), + 'old_local': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_local.sls', + 'order': 1, + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }, + }, + 'old_cmd': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_cmd.sls', + 'order': 1, + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }, + }, + 'old_caller': { + 'args': ('file.touch', '/tmp/foo'), + 'kwargs': {}, + }, + 'new_runner': ( + 'error.error', + { + '__state__': 'runner', + '__id__': 'raise_error', + 'name': 'raise_error', + '__sls__': '/srv/reactor/new_runner.sls', + '__user__': 'Reactor', + 'order': 1, + 'arg': (), + 'kwarg': { + 'name': 'Exception', + 'message': 'This is an error', + }, + }, + ), + 'new_wheel': ( + 'key.delete', + { + '__state__': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/new_wheel.sls', + 'order': 1, + '__user__': 'Reactor', + 'arg': (), + 'kwarg': {'match': 'foo'}, + }, + ), + 'new_local': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_local.sls', + 'order': 1, + 'arg': (), + 'kwarg': { + 'fun': 'pkg.installed', + 'name': 'zsh', + 'fromrepo': 'updates', + }, + }, + }, + 'new_cmd': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_cmd.sls', + 'order': 1, + 'arg': (), + 'kwarg': { + 'fun': 'pkg.installed', + 'name': 'zsh', + 'fromrepo': 'updates', + }, + }, + }, + 'new_caller': { + 'args': ('file.touch',), + 'kwargs': {'name': '/tmp/foo'}, + }, +} + +log = logging.getLogger(__name__) -@contextmanager -def reactor_process(opts, reactor): - opts = dict(opts) - opts['reactor'] = reactor - proc = reactor.Reactor(opts) - proc.start() - try: - if os.environ.get('TRAVIS_PYTHON_VERSION', None) is not None: - # Travis is slow - time.sleep(10) - else: - time.sleep(2) - yield - finally: - clean_proc(proc) - - -def _args_sideffect(*args, **kwargs): - return args, kwargs - - -@skipIf(True, 'Skipping until its clear what and how is this supposed to be testing') +@skipIf(NO_MOCK, NO_MOCK_REASON) class TestReactor(TestCase, AdaptedConfigurationTestCaseMixin): - def setUp(self): - self.opts = self.get_temp_config('master') - self.tempdir = tempfile.mkdtemp(dir=TMP) - self.sls_name = os.path.join(self.tempdir, 'test.sls') - with salt.utils.fopen(self.sls_name, 'w') as fh: - fh.write(''' -update_fileserver: - runner.fileserver.update -''') + ''' + Tests for constructing the low chunks to be executed via the Reactor + ''' + @classmethod + def setUpClass(cls): + ''' + Load the reactor config for mocking + ''' + cls.opts = cls.get_temp_config('master') + reactor_config = yaml.safe_load(REACTOR_CONFIG) + cls.opts.update(reactor_config) + cls.reactor = reactor.Reactor(cls.opts) + cls.reaction_map = salt.utils.repack_dictlist(reactor_config['reactor']) + renderers = salt.loader.render(cls.opts, {}) + cls.render_pipe = [(renderers[x], '') for x in ('jinja', 'yaml')] - def tearDown(self): - if os.path.isdir(self.tempdir): - shutil.rmtree(self.tempdir) - del self.opts - del self.tempdir - del self.sls_name + @classmethod + def tearDownClass(cls): + del cls.opts + del cls.reactor + del cls.render_pipe - def test_basic(self): - reactor_config = [ - {'salt/tagA': ['/srv/reactor/A.sls']}, - {'salt/tagB': ['/srv/reactor/B.sls']}, - {'*': ['/srv/reactor/all.sls']}, - ] - wrap = reactor.ReactWrap(self.opts) - with patch.object(reactor.ReactWrap, 'local', MagicMock(side_effect=_args_sideffect)): - ret = wrap.run({'fun': 'test.ping', - 'state': 'local', - 'order': 1, - 'name': 'foo_action', - '__id__': 'foo_action'}) - raise Exception(ret) + def test_list_reactors(self): + ''' + Ensure that list_reactors() returns the correct list of reactor SLS + files for each tag. + ''' + for schema in ('old', 'new'): + for rtype in REACTOR_DATA: + tag = '_'.join((schema, rtype)) + self.assertEqual( + self.reactor.list_reactors(tag), + self.reaction_map[tag] + ) + + def test_reactions(self): + ''' + Ensure that the correct reactions are built from the configured SLS + files and tag data. + ''' + for schema in ('old', 'new'): + for rtype in REACTOR_DATA: + tag = '_'.join((schema, rtype)) + log.debug('test_reactions: processing %s', tag) + reactors = self.reactor.list_reactors(tag) + log.debug('test_reactions: %s reactors: %s', tag, reactors) + # No globbing in our example SLS, and the files don't actually + # exist, so mock glob.glob to just return back the path passed + # to it. + with patch.object( + glob, + 'glob', + MagicMock(side_effect=lambda x: [x])): + # The below four mocks are all so that + # salt.template.compile_template() will read the templates + # we've mocked up in the SLS global variable above. + with patch.object( + os.path, 'isfile', + MagicMock(return_value=True)): + with patch.object( + salt.utils, 'is_empty', + MagicMock(return_value=False)): + with patch.object( + codecs, 'open', + mock_open(read_data=SLS[reactors[0]])): + with patch.object( + salt.template, 'template_shebang', + MagicMock(return_value=self.render_pipe)): + reactions = self.reactor.reactions( + tag, + REACTOR_DATA[rtype], + reactors, + ) + log.debug( + 'test_reactions: %s reactions: %s', + tag, reactions + ) + self.assertEqual(reactions, LOW_CHUNKS[tag]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class TestReactWrap(TestCase, AdaptedConfigurationTestCaseMixin): + ''' + Tests that we are formulating the wrapper calls properly + ''' + @classmethod + def setUpClass(cls): + cls.wrap = reactor.ReactWrap(cls.get_temp_config('master')) + + @classmethod + def tearDownClass(cls): + del cls.wrap + + def test_runner(self): + ''' + Test runner reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'runner')) + chunk = LOW_CHUNKS[tag][0] + thread_pool = Mock() + thread_pool.fire_async = Mock() + with patch.object(self.wrap, 'pool', thread_pool): + self.wrap.run(chunk) + thread_pool.fire_async.assert_called_with( + self.wrap.client_cache['runner'].low, + args=WRAPPER_CALLS[tag] + ) + + def test_wheel(self): + ''' + Test wheel reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'wheel')) + chunk = LOW_CHUNKS[tag][0] + thread_pool = Mock() + thread_pool.fire_async = Mock() + with patch.object(self.wrap, 'pool', thread_pool): + self.wrap.run(chunk) + thread_pool.fire_async.assert_called_with( + self.wrap.client_cache['wheel'].low, + args=WRAPPER_CALLS[tag] + ) + + def test_local(self): + ''' + Test local reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'local')) + chunk = LOW_CHUNKS[tag][0] + client_cache = {'local': Mock()} + client_cache['local'].cmd_async = Mock() + with patch.object(self.wrap, 'client_cache', client_cache): + self.wrap.run(chunk) + client_cache['local'].cmd_async.assert_called_with( + *WRAPPER_CALLS[tag]['args'], + **WRAPPER_CALLS[tag]['kwargs'] + ) + + def test_cmd(self): + ''' + Test cmd reactions (alias for 'local') using both the old and new + config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'cmd')) + chunk = LOW_CHUNKS[tag][0] + client_cache = {'local': Mock()} + client_cache['local'].cmd_async = Mock() + with patch.object(self.wrap, 'client_cache', client_cache): + self.wrap.run(chunk) + client_cache['local'].cmd_async.assert_called_with( + *WRAPPER_CALLS[tag]['args'], + **WRAPPER_CALLS[tag]['kwargs'] + ) + + def test_caller(self): + ''' + Test caller reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'caller')) + chunk = LOW_CHUNKS[tag][0] + client_cache = {'caller': Mock()} + client_cache['caller'].cmd = Mock() + with patch.object(self.wrap, 'client_cache', client_cache): + self.wrap.run(chunk) + client_cache['caller'].cmd.assert_called_with( + *WRAPPER_CALLS[tag]['args'], + **WRAPPER_CALLS[tag]['kwargs'] + ) From 7a2f12b96a2a9f80f1967a187e0c719464c9dea4 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 1 Sep 2017 18:35:01 -0500 Subject: [PATCH 108/633] Include a better example for reactor in master conf file --- doc/ref/configuration/master.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/ref/configuration/master.rst b/doc/ref/configuration/master.rst index 976919a343..ecba2b1537 100644 --- a/doc/ref/configuration/master.rst +++ b/doc/ref/configuration/master.rst @@ -4091,7 +4091,9 @@ information. .. code-block:: yaml - reactor: [] + reactor: + - 'salt/minion/*/start': + - salt://reactor/startup_tasks.sls .. conf_master:: reactor_refresh_interval From b5f10696c2e5ef5823caa7adcf7204bdfba5748d Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 1 Sep 2017 18:33:45 -0500 Subject: [PATCH 109/633] Improve the reactor documentation This reorganizes the reactor docs and includes examples of the new reactor SLS config syntax. --- doc/topics/beacons/index.rst | 5 +- doc/topics/reactor/index.rst | 553 ++++++++++++++++++++--------------- 2 files changed, 320 insertions(+), 238 deletions(-) diff --git a/doc/topics/beacons/index.rst b/doc/topics/beacons/index.rst index 6dae8dca09..62991af2f4 100644 --- a/doc/topics/beacons/index.rst +++ b/doc/topics/beacons/index.rst @@ -253,9 +253,8 @@ in ``/etc/salt/master.d/reactor.conf``: .. note:: You can have only one top level ``reactor`` section, so if one already - exists, add this code to the existing section. See :ref:`Understanding the - Structure of Reactor Formulas ` to learn more about - reactor SLS syntax. + exists, add this code to the existing section. See :ref:`here + ` to learn more about reactor SLS syntax. Start the Salt Master in Debug Mode diff --git a/doc/topics/reactor/index.rst b/doc/topics/reactor/index.rst index 2586245a1a..de5df946ac 100644 --- a/doc/topics/reactor/index.rst +++ b/doc/topics/reactor/index.rst @@ -27,9 +27,9 @@ event bus is an open system used for sending information notifying Salt and other systems about operations. The event system fires events with a very specific criteria. Every event has a -:strong:`tag`. Event tags allow for fast top level filtering of events. In -addition to the tag, each event has a data structure. This data structure is a -dict, which contains information about the event. +**tag**. Event tags allow for fast top-level filtering of events. In addition +to the tag, each event has a data structure. This data structure is a +dictionary, which contains information about the event. .. _reactor-mapping-events: @@ -65,15 +65,12 @@ and each event tag has a list of reactor SLS files to be run. the :ref:`querystring syntax ` (e.g. ``salt://reactor/mycustom.sls?saltenv=reactor``). -Reactor sls files are similar to state and pillar sls files. They are -by default yaml + Jinja templates and are passed familiar context variables. +Reactor SLS files are similar to State and Pillar SLS files. They are by +default YAML + Jinja templates and are passed familiar context variables. +Click :ref:`here ` for more detailed information on the +variables availble in Jinja templating. -They differ because of the addition of the ``tag`` and ``data`` variables. - -- The ``tag`` variable is just the tag in the fired event. -- The ``data`` variable is the event's data dict. - -Here is a simple reactor sls: +Here is the SLS for a simple reaction: .. code-block:: jinja @@ -90,71 +87,278 @@ data structure and compiler used for the state system is used for the reactor system. The only difference is that the data is matched up to the salt command API and the runner system. In this example, a command is published to the ``mysql1`` minion with a function of :py:func:`state.apply -`. Similarly, a runner can be called: +`, which performs a :ref:`highstate +`. Similarly, a runner can be called: .. code-block:: jinja {% if data['data']['custom_var'] == 'runit' %} call_runit_orch: runner.state.orchestrate: - - mods: _orch.runit + - args: + - mods: orchestrate.runit {% endif %} This example will execute the state.orchestrate runner and intiate an execution -of the runit orchestrator located at ``/srv/salt/_orch/runit.sls``. Using -``_orch/`` is any arbitrary path but it is recommended to avoid using "orchestrate" -as this is most likely to cause confusion. +of the ``runit`` orchestrator located at ``/srv/salt/orchestrate/runit.sls``. -Writing SLS Files ------------------ +Types of Reactions +================== -Reactor SLS files are stored in the same location as State SLS files. This means -that both ``file_roots`` and ``gitfs_remotes`` impact what SLS files are -available to the reactor and orchestrator. +============================== ================================================================================== +Name Description +============================== ================================================================================== +:ref:`local ` Runs a :ref:`remote-execution function ` on targeted minions +:ref:`runner ` Executes a :ref:`runner function ` +:ref:`wheel ` Executes a :ref:`wheel function ` on the master +:ref:`caller ` Runs a :ref:`remote-execution function ` on a masterless minion +============================== ================================================================================== -It is recommended to keep reactor and orchestrator SLS files in their own uniquely -named subdirectories such as ``_orch/``, ``orch/``, ``_orchestrate/``, ``react/``, -``_reactor/``, etc. Keeping a unique name helps prevent confusion when trying to -read through this a few years down the road. +.. note:: + The ``local`` and ``caller`` reaction types will be renamed for the Oxygen + release. These reaction types were named after Salt's internal client + interfaces, and are not intuitively named. Both ``local`` and ``caller`` + will continue to work in Reactor SLS files, but for the Oxygen release the + documentation will be updated to reflect the new preferred naming. -The Goal of Writing Reactor SLS Files -===================================== +Where to Put Reactor SLS Files +============================== -Reactor SLS files share the familiar syntax from Salt States but there are -important differences. The goal of a Reactor file is to process a Salt event as -quickly as possible and then to optionally start a **new** process in response. +Reactor SLS files can come both from files local to the master, and from any of +backends enabled via the :conf_master:`fileserver_backend` config option. Files +placed in the Salt fileserver can be referenced using a ``salt://`` URL, just +like they can in State SLS files. -1. The Salt Reactor watches Salt's event bus for new events. -2. The event tag is matched against the list of event tags under the - ``reactor`` section in the Salt Master config. -3. The SLS files for any matches are Rendered into a data structure that - represents one or more function calls. -4. That data structure is given to a pool of worker threads for execution. +It is recommended to place reactor and orchestrator SLS files in their own +uniquely-named subdirectories such as ``orch/``, ``orchestrate/``, ``react/``, +``reactor/``, etc., to keep them organized. + +.. _reactor-sls: + +Writing Reactor SLS +=================== + +The different reaction types were developed separately and have historically +had different methods for passing arguments. For the 2017.7.2 release a new, +unified configuration schema has been introduced, which applies to all reaction +types. + +The old config schema will continue to be supported, and there is no plan to +deprecate it at this time. + +.. _reactor-local: + +Local Reactions +--------------- + +A ``local`` reaction runs a :ref:`remote-execution function ` +on the targeted minions. + +The old config schema required the positional and keyword arguments to be +manually separated by the user under ``arg`` and ``kwarg`` parameters. However, +this is not very user-friendly, as it forces the user to distinguish which type +of argument is which, and make sure that positional arguments are ordered +properly. Therefore, the new config schema is recommended if the master is +running a supported release. + +The below two examples are equivalent: + ++---------------------------------+-----------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++=================================+=============================+ +| :: | :: | +| | | +| install_zsh: | install_zsh: | +| local.state.single: | local.state.single: | +| - tgt: 'kernel:Linux' | - tgt: 'kernel:Linux' | +| - tgt_type: grain | - tgt_type: grain | +| - args: | - arg: | +| - fun: pkg.installed | - pkg.installed | +| - name: zsh | - zsh | +| - fromrepo: updates | - kwarg: | +| | fromrepo: updates | ++---------------------------------+-----------------------------+ + +This reaction would be equvalent to running the following Salt command: + +.. code-block:: bash + + salt -G 'kernel:Linux' state.single pkg.installed name=zsh fromrepo=updates + +.. note:: + Any other parameters in the :py:meth:`LocalClient().cmd_async() + ` method can be passed at the same + indentation level as ``tgt``. + +.. note:: + ``tgt_type`` is only required when the target expression defined in ``tgt`` + uses a :ref:`target type ` other than a minion ID glob. + + The ``tgt_type`` argument was named ``expr_form`` in releases prior to + 2017.7.0. + +.. _reactor-runner: + +Runner Reactions +---------------- + +Runner reactions execute :ref:`runner functions ` locally on +the master. + +The old config schema called for passing arguments to the reaction directly +under the name of the runner function. However, this can cause unpredictable +interactions with the Reactor system's internal arguments. It is also possible +to pass positional and keyword arguments under ``arg`` and ``kwarg`` like above +in :ref:`local reactions `, but as noted above this is not very +user-friendly. Therefore, the new config schema is recommended if the master +is running a supported release. + +The below two examples are equivalent: + ++-------------------------------------------------+-------------------------------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++=================================================+=================================================+ +| :: | :: | +| | | +| deploy_app: | deploy_app: | +| runner.state.orchestrate: | runner.state.orchestrate: | +| - args: | - mods: orchestrate.deploy_app | +| - mods: orchestrate.deploy_app | - kwarg: | +| - pillar: | pillar: | +| event_tag: {{ tag }} | event_tag: {{ tag }} | +| event_data: {{ data['data']|json }} | event_data: {{ data['data']|json }} | ++-------------------------------------------------+-------------------------------------------------+ + +Assuming that the event tag is ``foo``, and the data passed to the event is +``{'bar': 'baz'}``, then this reaction is equvalent to running the following +Salt command: + +.. code-block:: bash + + salt-run state.orchestrate mods=orchestrate.deploy_app pillar='{"event_tag": "foo", "event_data": {"bar": "baz"}}' + +.. _reactor-wheel: + +Wheel Reactions +--------------- + +Wheel reactions run :ref:`wheel functions ` locally on the +master. + +Like :ref:`runner reactions `, the old config schema called for +wheel reactions to have arguments passed directly under the name of the +:ref:`wheel function ` (or in ``arg`` or ``kwarg`` parameters). + +The below two examples are equivalent: + ++-----------------------------------+---------------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++===================================+=================================+ +| :: | :: | +| | | +| remove_key: | remove_key: | +| wheel.key.delete: | wheel.key.delete: | +| - args: | - match: {{ data['id'] }} | +| - match: {{ data['id'] }} | | ++-----------------------------------+---------------------------------+ + +.. _reactor-caller: + +Caller Reactions +---------------- + +Caller reactions run :ref:`remote-execution functions ` on a +minion daemon's Reactor system. To run a Reactor on the minion, it is necessary +to configure the :mod:`Reactor Engine ` in the minion +config file, and then setup your watched events in a ``reactor`` section in the +minion config file as well. + +.. note:: Masterless Minions use this Reactor + + This is the only way to run the Reactor if you use masterless minions. + +Both the old and new config schemas involve passing arguments under an ``args`` +parameter. However, the old config schema only supports positional arguments. +Therefore, the new config schema is recommended if the masterless minion is +running a supported release. + +The below two examples are equivalent: + ++---------------------------------+---------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++=================================+===========================+ +| :: | :: | +| | | +| touch_file: | touch_file: | +| caller.file.touch: | caller.file.touch: | +| - args: | - args: | +| - name: /tmp/foo | - /tmp/foo | ++---------------------------------+---------------------------+ + +This reaction is equvalent to running the following Salt command: + +.. code-block:: bash + + salt-call file.touch name=/tmp/foo + +Best Practices for Writing Reactor SLS Files +============================================ + +The Reactor works as follows: + +1. The Salt Reactor watches Salt's event bus for new events. +2. Each event's tag is matched against the list of event tags configured under + the :conf_master:`reactor` section in the Salt Master config. +3. The SLS files for any matches are rendered into a data structure that + represents one or more function calls. +4. That data structure is given to a pool of worker threads for execution. Matching and rendering Reactor SLS files is done sequentially in a single -process. Complex Jinja that calls out to slow Execution or Runner modules slows -down the rendering and causes other reactions to pile up behind the current -one. The worker pool is designed to handle complex and long-running processes -such as Salt Orchestrate. +process. For that reason, reactor SLS files should contain few individual +reactions (one, if at all possible). Also, keep in mind that reactions are +fired asynchronously (with the exception of :ref:`caller `) and +do *not* support :ref:`requisites `. -tl;dr: Rendering Reactor SLS files MUST be simple and quick. The new process -started by the worker threads can be long-running. Using the reactor to fire -an orchestrate runner would be ideal. +Complex Jinja templating that calls out to slow :ref:`remote-execution +` or :ref:`runner ` functions slows down +the rendering and causes other reactions to pile up behind the current one. The +worker pool is designed to handle complex and long-running processes like +:ref:`orchestration ` jobs. + +Therefore, when complex tasks are in order, :ref:`orchestration +` is a natural fit. Orchestration SLS files can be more +complex, and use requisites. Performing a complex task using orchestration lets +the Reactor system fire off the orchestration job and proceed with processing +other reactions. + +.. _reactor-jinja-context: Jinja Context -------------- +============= -Reactor files only have access to a minimal Jinja context. ``grains`` and -``pillar`` are not available. The ``salt`` object is available for calling -Runner and Execution modules but it should be used sparingly and only for quick -tasks for the reasons mentioned above. +Reactor SLS files only have access to a minimal Jinja context. ``grains`` and +``pillar`` are *not* available. The ``salt`` object is available for calling +:ref:`remote-execution ` or :ref:`runner ` +functions, but it should be used sparingly and only for quick tasks for the +reasons mentioned above. + +In addition to the ``salt`` object, the following variables are available in +the Jinja context: + +- ``tag`` - the tag from the event that triggered execution of the Reactor SLS + file +- ``data`` - the event's data dictionary + +The ``data`` dict will contain an ``id`` key containing the minion ID, if the +event was fired from a minion, and a ``data`` key containing the data passed to +the event. Advanced State System Capabilities ----------------------------------- +================================== -Reactor SLS files, by design, do not support Requisites, ordering, -``onlyif``/``unless`` conditionals and most other powerful constructs from -Salt's State system. +Reactor SLS files, by design, do not support :ref:`requisites `, +ordering, ``onlyif``/``unless`` conditionals and most other powerful constructs +from Salt's State system. Complex Master-side operations are best performed by Salt's Orchestrate system so using the Reactor to kick off an Orchestrate run is a very common pairing. @@ -166,7 +370,7 @@ For example: # /etc/salt/master.d/reactor.conf # A custom event containing: {"foo": "Foo!", "bar: "bar*", "baz": "Baz!"} reactor: - - myco/custom/event: + - my/custom/event: - /srv/reactor/some_event.sls .. code-block:: jinja @@ -174,15 +378,15 @@ For example: # /srv/reactor/some_event.sls invoke_orchestrate_file: runner.state.orchestrate: - - mods: _orch.do_complex_thing # /srv/salt/_orch/do_complex_thing.sls - - kwarg: - pillar: - event_tag: {{ tag }} - event_data: {{ data|json() }} + - args: + - mods: orchestrate.do_complex_thing + - pillar: + event_tag: {{ tag }} + event_data: {{ data|json }} .. code-block:: jinja - # /srv/salt/_orch/do_complex_thing.sls + # /srv/salt/orchestrate/do_complex_thing.sls {% set tag = salt.pillar.get('event_tag') %} {% set data = salt.pillar.get('event_data') %} @@ -209,7 +413,7 @@ For example: .. _beacons-and-reactors: Beacons and Reactors --------------------- +==================== An event initiated by a beacon, when it arrives at the master will be wrapped inside a second event, such that the data object containing the beacon @@ -219,27 +423,52 @@ For example, to access the ``id`` field of the beacon event in a reactor file, you will need to reference ``{{ data['data']['id'] }}`` rather than ``{{ data['id'] }}`` as for events initiated directly on the event bus. +Similarly, the data dictionary attached to the event would be located in +``{{ data['data']['data'] }}`` instead of ``{{ data['data'] }}``. + See the :ref:`beacon documentation ` for examples. -Fire an event -============= +Manually Firing an Event +======================== -To fire an event from a minion call ``event.send`` +From the Master +--------------- + +Use the :py:func:`event.send ` runner: .. code-block:: bash - salt-call event.send 'foo' '{orchestrate: refresh}' + salt-run event.send foo '{orchestrate: refresh}' -After this is called, any reactor sls files matching event tag ``foo`` will -execute with ``{{ data['data']['orchestrate'] }}`` equal to ``'refresh'``. +From the Minion +--------------- -See :py:mod:`salt.modules.event` for more information. +To fire an event to the master from a minion, call :py:func:`event.send +`: -Knowing what event is being fired -================================= +.. code-block:: bash -The best way to see exactly what events are fired and what data is available in -each event is to use the :py:func:`state.event runner + salt-call event.send foo '{orchestrate: refresh}' + +To fire an event to the minion's local event bus, call :py:func:`event.fire +`: + +.. code-block:: bash + + salt-call event.fire '{orchestrate: refresh}' foo + +Referencing Data Passed in Events +--------------------------------- + +Assuming any of the above examples, any reactor SLS files triggered by watching +the event tag ``foo`` will execute with ``{{ data['data']['orchestrate'] }}`` +equal to ``'refresh'``. + +Getting Information About Events +================================ + +The best way to see exactly what events have been fired and what data is +available in each event is to use the :py:func:`state.event runner `. .. seealso:: :ref:`Common Salt Events ` @@ -308,156 +537,10 @@ rendered SLS file (or any errors generated while rendering the SLS file). view the result of referencing Jinja variables. If the result is empty then Jinja produced an empty result and the Reactor will ignore it. -.. _reactor-structure: +Passing Event Data to Minions or Orchestration as Pillar +-------------------------------------------------------- -Understanding the Structure of Reactor Formulas -=============================================== - -**I.e., when to use `arg` and `kwarg` and when to specify the function -arguments directly.** - -While the reactor system uses the same basic data structure as the state -system, the functions that will be called using that data structure are -different functions than are called via Salt's state system. The Reactor can -call Runner modules using the `runner` prefix, Wheel modules using the `wheel` -prefix, and can also cause minions to run Execution modules using the `local` -prefix. - -.. versionchanged:: 2014.7.0 - The ``cmd`` prefix was renamed to ``local`` for consistency with other - parts of Salt. A backward-compatible alias was added for ``cmd``. - -The Reactor runs on the master and calls functions that exist on the master. In -the case of Runner and Wheel functions the Reactor can just call those -functions directly since they exist on the master and are run on the master. - -In the case of functions that exist on minions and are run on minions, the -Reactor still needs to call a function on the master in order to send the -necessary data to the minion so the minion can execute that function. - -The Reactor calls functions exposed in :ref:`Salt's Python API documentation -`. and thus the structure of Reactor files very transparently -reflects the function signatures of those functions. - -Calling Execution modules on Minions ------------------------------------- - -The Reactor sends commands down to minions in the exact same way Salt's CLI -interface does. It calls a function locally on the master that sends the name -of the function as well as a list of any arguments and a dictionary of any -keyword arguments that the minion should use to execute that function. - -Specifically, the Reactor calls the async version of :py:meth:`this function -`. You can see that function has 'arg' and 'kwarg' -parameters which are both values that are sent down to the minion. - -Executing remote commands maps to the :strong:`LocalClient` interface which is -used by the :strong:`salt` command. This interface more specifically maps to -the :strong:`cmd_async` method inside of the :strong:`LocalClient` class. This -means that the arguments passed are being passed to the :strong:`cmd_async` -method, not the remote method. A field starts with :strong:`local` to use the -:strong:`LocalClient` subsystem. The result is, to execute a remote command, -a reactor formula would look like this: - -.. code-block:: yaml - - clean_tmp: - local.cmd.run: - - tgt: '*' - - arg: - - rm -rf /tmp/* - -The ``arg`` option takes a list of arguments as they would be presented on the -command line, so the above declaration is the same as running this salt -command: - -.. code-block:: bash - - salt '*' cmd.run 'rm -rf /tmp/*' - -Use the ``tgt_type`` argument to specify a matcher: - -.. code-block:: yaml - - clean_tmp: - local.cmd.run: - - tgt: 'os:Ubuntu' - - tgt_type: grain - - arg: - - rm -rf /tmp/* - - - clean_tmp: - local.cmd.run: - - tgt: 'G@roles:hbase_master' - - tgt_type: compound - - arg: - - rm -rf /tmp/* - -.. note:: - The ``tgt_type`` argument was named ``expr_form`` in releases prior to - 2017.7.0 (2016.11.x and earlier). - -Any other parameters in the :py:meth:`LocalClient().cmd() -` method can be specified as well. - -Executing Reactors from the Minion ----------------------------------- - -The minion can be setup to use the Reactor via a reactor engine. This just -sets up and listens to the minions event bus, instead of to the masters. - -The biggest difference is that you have to use the caller method on the -Reactor, which is the equivalent of salt-call, to run your commands. - -:mod:`Reactor Engine setup ` - -.. code-block:: yaml - - clean_tmp: - caller.cmd.run: - - arg: - - rm -rf /tmp/* - -.. note:: Masterless Minions use this Reactor - - This is the only way to run the Reactor if you use masterless minions. - -Calling Runner modules and Wheel modules ----------------------------------------- - -Calling Runner modules and Wheel modules from the Reactor uses a more direct -syntax since the function is being executed locally instead of sending a -command to a remote system to be executed there. There are no 'arg' or 'kwarg' -parameters (unless the Runner function or Wheel function accepts a parameter -with either of those names.) - -For example: - -.. code-block:: yaml - - clear_the_grains_cache_for_all_minions: - runner.cache.clear_grains - -If the :py:func:`the runner takes arguments ` then -they must be specified as keyword arguments. - -.. code-block:: yaml - - spin_up_more_web_machines: - runner.cloud.profile: - - prof: centos_6 - - instances: - - web11 # These VM names would be generated via Jinja in a - - web12 # real-world example. - -To determine the proper names for the arguments, check the documentation -or source code for the runner function you wish to call. - -Passing event data to Minions or Orchestrate as Pillar ------------------------------------------------------- - -An interesting trick to pass data from the Reactor script to +An interesting trick to pass data from the Reactor SLS file to :py:func:`state.apply ` is to pass it as inline Pillar data since both functions take a keyword argument named ``pillar``. @@ -484,10 +567,9 @@ from the event to the state file via inline Pillar. add_new_minion_to_pool: local.state.apply: - tgt: 'haproxy*' - - arg: - - haproxy.refresh_pool - - kwarg: - pillar: + - args: + - mods: haproxy.refresh_pool + - pillar: new_minion: {{ data['id'] }} {% endif %} @@ -503,17 +585,16 @@ This works with Orchestrate files as well: call_some_orchestrate_file: runner.state.orchestrate: - - mods: _orch.some_orchestrate_file - - pillar: - stuff: things + - args: + - mods: orchestrate.some_orchestrate_file + - pillar: + stuff: things Which is equivalent to the following command at the CLI: .. code-block:: bash - salt-run state.orchestrate _orch.some_orchestrate_file pillar='{stuff: things}' - -This expects to find a file at /srv/salt/_orch/some_orchestrate_file.sls. + salt-run state.orchestrate orchestrate.some_orchestrate_file pillar='{stuff: things}' Finally, that data is available in the state file using the normal Pillar lookup syntax. The following example is grabbing web server names and IP @@ -564,7 +645,7 @@ includes the minion id, which we can use for matching. - 'salt/minion/ink*/start': - /srv/reactor/auth-complete.sls -In this sls file, we say that if the key was rejected we will delete the key on +In this SLS file, we say that if the key was rejected we will delete the key on the master and then also tell the master to ssh in to the minion and tell it to restart the minion, since a minion process will die if the key is rejected. @@ -580,19 +661,21 @@ authentication every ten seconds by default. {% if not data['result'] and data['id'].startswith('ink') %} minion_remove: wheel.key.delete: - - match: {{ data['id'] }} + - args: + - match: {{ data['id'] }} minion_rejoin: local.cmd.run: - tgt: salt-master.domain.tld - - arg: - - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "{{ data['id'] }}" 'sleep 10 && /etc/init.d/salt-minion restart' + - args: + - cmd: ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "{{ data['id'] }}" 'sleep 10 && /etc/init.d/salt-minion restart' {% endif %} {# Ink server is sending new key -- accept this key #} {% if 'act' in data and data['act'] == 'pend' and data['id'].startswith('ink') %} minion_add: wheel.key.accept: - - match: {{ data['id'] }} + - args: + - match: {{ data['id'] }} {% endif %} No if statements are needed here because we already limited this action to just From 7abd07fa07d19dc23eb621d545977a65e69f5729 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 6 Sep 2017 10:38:45 -0500 Subject: [PATCH 110/633] Simplify client logic --- salt/client/mixins.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/salt/client/mixins.py b/salt/client/mixins.py index bd69d269bf..ea2090e263 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -360,19 +360,18 @@ class SyncClientMixin(object): # that since the caller knows what is an arg vs a kwarg, but while # we make the transition we will load "kwargs" using format_call if # there are no kwargs in the low object passed in. - f_call = {} if 'arg' in low and 'kwarg' in low \ - else salt.utils.format_call( + + if 'arg' in low and 'kwarg' in low: + args = low['arg'] + kwargs = low['kwarg'] + else: + f_call = salt.utils.format_call( self.functions[fun], low, expected_extra_kws=CLIENT_INTERNAL_KEYWORDS ) - - args = f_call.get('args', ()) \ - if 'arg' not in low \ - else low['arg'] - kwargs = f_call.get('kwargs', {}) \ - if 'kwarg' not in low \ - else low['kwarg'] + args = f_call.get('args', ()) + kwargs = f_call.get('kwargs', {}) # Update the event data with loaded args and kwargs data['fun_args'] = list(args) + ([kwargs] if kwargs else []) From e076e9b6340fa7647b7029ed48e73f7764b3ae91 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Fri, 15 Sep 2017 18:47:03 +0300 Subject: [PATCH 111/633] Forward events to all masters syndic connected to. --- salt/minion.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/salt/minion.py b/salt/minion.py index c9cfc6cb1f..394b11a2e8 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -2588,6 +2588,8 @@ class SyndicManager(MinionBase): ''' if kwargs is None: kwargs = {} + successful = False + # Call for each master for master, syndic_future in self.iter_master_options(master_id): if not syndic_future.done() or syndic_future.exception(): log.error('Unable to call {0} on {1}, that syndic is not connected'.format(func, master)) @@ -2595,12 +2597,12 @@ class SyndicManager(MinionBase): try: getattr(syndic_future.result(), func)(*args, **kwargs) - return + successful = True except SaltClientError: log.error('Unable to call {0} on {1}, trying another...'.format(func, master)) self._mark_master_dead(master) - continue - log.critical('Unable to call {0} on any masters!'.format(func)) + if not successful: + log.critical('Unable to call {0} on any masters!'.format(func)) def _return_pub_syndic(self, values, master_id=None): ''' From e5297e386975a2fd68418e0fa8b881950c17ef33 Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 18 Sep 2017 16:19:15 -0400 Subject: [PATCH 112/633] Add reason to linux_acl state loading failure --- salt/states/linux_acl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/states/linux_acl.py b/salt/states/linux_acl.py index 4e3c7049b9..fec2e58eac 100644 --- a/salt/states/linux_acl.py +++ b/salt/states/linux_acl.py @@ -49,7 +49,7 @@ def __virtual__(): if salt.utils.which('getfacl') and salt.utils.which('setfacl'): return __virtualname__ - return False + return False, 'The linux_acl state cannot be loaded: the getfacl or setfacl binary is not in the path.' def present(name, acl_type, acl_name='', perms='', recurse=False): From 35cf69bc50b837d83dd596ee1a8d32a8f3ac4e18 Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Tue, 19 Sep 2017 09:53:27 +0200 Subject: [PATCH 113/633] Moved exception Salt core The timeout exception is now part of exceptions.py and no longer solely defined in the module. --- salt/exceptions.py | 6 ++++++ salt/modules/kubernetes.py | 9 +++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/salt/exceptions.py b/salt/exceptions.py index 256537dd77..00111df104 100644 --- a/salt/exceptions.py +++ b/salt/exceptions.py @@ -265,6 +265,12 @@ class SaltCacheError(SaltException): ''' +class TimeoutError(SaltException): + ''' + Thrown when an opration cannot be completet within a given time limit. + ''' + + class SaltReqTimeoutError(SaltException): ''' Thrown when a salt master request call fails to return within the timeout diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index aa06645660..b5628d41d3 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -48,6 +48,7 @@ from salt.exceptions import CommandExecutionError from salt.ext.six import iteritems import salt.utils import salt.utils.templates +from salt.exceptions import TimeoutError from salt.ext.six.moves import range # pylint: disable=import-error try: @@ -82,15 +83,11 @@ def __virtual__(): return False, 'python kubernetes library not found' -class TimeoutException(Exception): - pass - - if salt.utils.is_windows(): @contextmanager def _time_limit(seconds): def signal_handler(signum, frame): - raise TimeoutException + raise TimeoutError signal.signal(signal.SIGALRM, signal_handler) signal.alarm(seconds) try: @@ -723,7 +720,7 @@ def delete_deployment(name, namespace='default', **kwargs): sleep(1) else: # pylint: disable=useless-else-on-loop mutable_api_response['code'] = 200 - except TimeoutException: + except TimeoutError: pass else: # Windows has not signal.alarm implementation, so we are just falling From 2d810690b6438a58d55341ed7814df830e6e01cd Mon Sep 17 00:00:00 2001 From: Vladimir Nadvornik Date: Tue, 19 Sep 2017 11:01:27 +0200 Subject: [PATCH 114/633] Fix pylint errors --- salt/modules/mdadm.py | 1 + salt/states/mdadm.py | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/salt/modules/mdadm.py b/salt/modules/mdadm.py index 334cd46e73..354ece93ba 100644 --- a/salt/modules/mdadm.py +++ b/salt/modules/mdadm.py @@ -357,6 +357,7 @@ def assemble(name, elif test_mode is False: return __salt__['cmd.run'](cmd, python_shell=False) + def examine(device): ''' Show detail for a specified RAID component device diff --git a/salt/states/mdadm.py b/salt/states/mdadm.py index 067c5c4c2f..4588d859fa 100644 --- a/salt/states/mdadm.py +++ b/salt/states/mdadm.py @@ -25,9 +25,6 @@ import logging # Import salt libs import salt.utils.path -# Import 3rd-party libs -from salt.ext import six - # Set up logger log = logging.getLogger(__name__) @@ -116,7 +113,7 @@ def present(name, elif len(uuid_dict) == 1: uuid = uuid_dict.keys()[0] if present and present['uuid'] != uuid: - ret['comment'] = 'Devices MD_UUIDs: {0} differs from present RAID uuid {1}.'.format(uuid, present['uuid']) + ret['comment'] = 'Devices MD_UUIDs: {0} differs from present RAID uuid {1}.'.format(uuid, present['uuid']) ret['result'] = False return ret @@ -193,7 +190,7 @@ def present(name, ret['comment'] = 'Raid {0} failed to be {1}.'.format(name, verb) ret['result'] = False else: - ret['comment'] = 'Raid {0} already present.'.format(name) + ret['comment'] = 'Raid {0} already present.'.format(name) if (do_assemble or present) and len(new_devices) > 0: for d in new_devices: From a2b61f7cd2d3367d4167b25de5e52cb557eb1026 Mon Sep 17 00:00:00 2001 From: Tom Williams Date: Tue, 19 Sep 2017 13:24:51 -0400 Subject: [PATCH 115/633] INFRA-5292 - small fix for boto_iam AWS rate limiting errors --- salt/modules/boto_iam.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/salt/modules/boto_iam.py b/salt/modules/boto_iam.py index 97eccd6616..e83fdffd0c 100644 --- a/salt/modules/boto_iam.py +++ b/salt/modules/boto_iam.py @@ -2148,6 +2148,7 @@ def list_entities_for_policy(policy_name, path_prefix=None, entity_filter=None, salt myminion boto_iam.list_entities_for_policy mypolicy ''' conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + retries = 30 params = {} for arg in ('path_prefix', 'entity_filter'): @@ -2155,21 +2156,26 @@ def list_entities_for_policy(policy_name, path_prefix=None, entity_filter=None, params[arg] = locals()[arg] policy_arn = _get_policy_arn(policy_name, region, key, keyid, profile) - try: - allret = { - 'policy_groups': [], - 'policy_users': [], - 'policy_roles': [], - } - for ret in __utils__['boto.paged_call'](conn.list_entities_for_policy, policy_arn=policy_arn, **params): - for k, v in six.iteritems(allret): - v.extend(ret.get('list_entities_for_policy_response', {}).get('list_entities_for_policy_result', {}).get(k)) - return allret - except boto.exception.BotoServerError as e: - log.debug(e) - msg = 'Failed to list {0} policy entities.' - log.error(msg.format(policy_name)) - return {} + while retries: + try: + allret = { + 'policy_groups': [], + 'policy_users': [], + 'policy_roles': [], + } + for ret in __utils__['boto.paged_call'](conn.list_entities_for_policy, policy_arn=policy_arn, **params): + for k, v in six.iteritems(allret): + v.extend(ret.get('list_entities_for_policy_response', {}).get('list_entities_for_policy_result', {}).get(k)) + return allret + except boto.exception.BotoServerError as e: + if e.error_code == 'Throttling': + log.debug("Throttled by AWS API, will retry in 5 seconds...") + time.sleep(5) + retries -= 1 + continue + log.error('Failed to list {0} policy entities: {1}'.format(policy_name, e.message)) + return {} + return {} def list_attached_user_policies(user_name, path_prefix=None, entity_filter=None, From e62a359a0ce1d45fbdbaca0faf2447f9a6894fa1 Mon Sep 17 00:00:00 2001 From: Tom Williams Date: Tue, 19 Sep 2017 15:06:23 -0400 Subject: [PATCH 116/633] INFRA-5492 - dang it, I had this and forgot to cut and paste it over :-/ --- salt/modules/boto_iam.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/modules/boto_iam.py b/salt/modules/boto_iam.py index e83fdffd0c..9575156f83 100644 --- a/salt/modules/boto_iam.py +++ b/salt/modules/boto_iam.py @@ -42,6 +42,7 @@ from __future__ import absolute_import import logging import json import yaml +import time # Import salt libs import salt.ext.six as six From f84b50a06b6694678dce21c27af5bd9e0ee6a9bf Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 19 Sep 2017 09:36:51 -0600 Subject: [PATCH 117/633] results and columns are lists for mysql returns --- salt/modules/mysql.py | 20 ++- .../files/file/base/mysql/select_query.sql | 7 + .../files/file/base/mysql/update_query.sql | 3 + tests/integration/modules/test_mysql.py | 131 +++++++++++++++++- 4 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 tests/integration/files/file/base/mysql/select_query.sql create mode 100644 tests/integration/files/file/base/mysql/update_query.sql diff --git a/salt/modules/mysql.py b/salt/modules/mysql.py index e97525ad08..6f42801a40 100644 --- a/salt/modules/mysql.py +++ b/salt/modules/mysql.py @@ -687,11 +687,20 @@ def file_query(database, file_name, **connection_args): .. versionadded:: 2017.7.0 + database + + database to run script inside + + file_name + + File name of the script. This can be on the minion, or a file that is reachable by the fileserver + CLI Example: .. code-block:: bash salt '*' mysql.file_query mydb file_name=/tmp/sqlfile.sql + salt '*' mysql.file_query mydb file_name=salt://sqlfile.sql Return data: @@ -700,6 +709,9 @@ def file_query(database, file_name, **connection_args): {'query time': {'human': '39.0ms', 'raw': '0.03899'}, 'rows affected': 1L} ''' + if any(file_name.startswith(proto) for proto in ('salt://', 'http://', 'https://', 'swift://', 's3://')): + file_name = __salt__['cp.cache_file'](file_name) + if os.path.exists(file_name): with salt.utils.fopen(file_name, 'r') as ifile: contents = ifile.read() @@ -708,7 +720,7 @@ def file_query(database, file_name, **connection_args): return False query_string = "" - ret = {'rows returned': 0, 'columns': 0, 'results': 0, 'rows affected': 0, 'query time': {'raw': 0}} + ret = {'rows returned': 0, 'columns': [], 'results': [], 'rows affected': 0, 'query time': {'raw': 0}} for line in contents.splitlines(): if re.match(r'--', line): # ignore sql comments continue @@ -728,16 +740,16 @@ def file_query(database, file_name, **connection_args): if 'rows returned' in query_result: ret['rows returned'] += query_result['rows returned'] if 'columns' in query_result: - ret['columns'] += query_result['columns'] + ret['columns'].append(query_result['columns']) if 'results' in query_result: - ret['results'] += query_result['results'] + ret['results'].append(query_result['results']) if 'rows affected' in query_result: ret['rows affected'] += query_result['rows affected'] ret['query time']['human'] = str(round(float(ret['query time']['raw']), 2)) + 's' ret['query time']['raw'] = round(float(ret['query time']['raw']), 5) # Remove empty keys in ret - ret = dict((k, v) for k, v in six.iteritems(ret) if v) + ret = {k: v for k, v in six.iteritems(ret) if v} return ret diff --git a/tests/integration/files/file/base/mysql/select_query.sql b/tests/integration/files/file/base/mysql/select_query.sql new file mode 100644 index 0000000000..10cf4850fd --- /dev/null +++ b/tests/integration/files/file/base/mysql/select_query.sql @@ -0,0 +1,7 @@ +CREATE TABLE test_select (a INT); +insert into test_select values (1); +insert into test_select values (3); +insert into test_select values (4); +insert into test_select values (5); +update test_select set a=2 where a=1; +select * from test_select; diff --git a/tests/integration/files/file/base/mysql/update_query.sql b/tests/integration/files/file/base/mysql/update_query.sql new file mode 100644 index 0000000000..34cee2dab1 --- /dev/null +++ b/tests/integration/files/file/base/mysql/update_query.sql @@ -0,0 +1,3 @@ +CREATE TABLE test_update (a INT); +insert into test_update values (1); +update test_update set a=2 where a=1; diff --git a/tests/integration/modules/test_mysql.py b/tests/integration/modules/test_mysql.py index 20b79da908..0cffdb37fa 100644 --- a/tests/integration/modules/test_mysql.py +++ b/tests/integration/modules/test_mysql.py @@ -1280,6 +1280,7 @@ class MysqlModuleUserGrantTest(ModuleCase, SaltReturnAssertsMixin): testdb1 = 'tes.t\'"saltdb' testdb2 = 't_st `(:=salt%b)' testdb3 = 'test `(:=salteeb)' + test_file_query_db = 'test_query' table1 = 'foo' table2 = "foo `\'%_bar" users = { @@ -1391,13 +1392,19 @@ class MysqlModuleUserGrantTest(ModuleCase, SaltReturnAssertsMixin): name=self.testdb1, connection_user=self.user, connection_pass=self.password, - ) + ) self.run_function( 'mysql.db_remove', name=self.testdb2, connection_user=self.user, connection_pass=self.password, - ) + ) + self.run_function( + 'mysql.db_remove', + name=self.test_file_query_db, + connection_user=self.user, + connection_pass=self.password, + ) def _userCreation(self, uname, @@ -1627,3 +1634,123 @@ class MysqlModuleUserGrantTest(ModuleCase, SaltReturnAssertsMixin): "GRANT USAGE ON *.* TO ''@'localhost'", "GRANT DELETE ON `test ``(:=salteeb)`.* TO ''@'localhost'" ]) + + +@skipIf( + NO_MYSQL, + 'Please install MySQL bindings and a MySQL Server before running' + 'MySQL integration tests.' +) +class MysqlModuleFileQueryTest(ModuleCase, SaltReturnAssertsMixin): + ''' + Test file query module + ''' + + user = 'root' + password = 'poney' + testdb = 'test_file_query' + + @destructiveTest + def setUp(self): + ''' + Test presence of MySQL server, enforce a root password, create users + ''' + super(MysqlModuleFileQueryTest, self).setUp() + NO_MYSQL_SERVER = True + # now ensure we know the mysql root password + # one of theses two at least should work + ret1 = self.run_state( + 'cmd.run', + name='mysqladmin --host="localhost" -u ' + + self.user + + ' flush-privileges password "' + + self.password + + '"' + ) + ret2 = self.run_state( + 'cmd.run', + name='mysqladmin --host="localhost" -u ' + + self.user + + ' --password="' + + self.password + + '" flush-privileges password "' + + self.password + + '"' + ) + key, value = ret2.popitem() + if value['result']: + NO_MYSQL_SERVER = False + else: + self.skipTest('No MySQL Server running, or no root access on it.') + # Create some users and a test db + self.run_function( + 'mysql.db_create', + name=self.testdb, + connection_user=self.user, + connection_pass=self.password, + connection_db='mysql', + ) + + @destructiveTest + def tearDown(self): + ''' + Removes created users and db + ''' + self.run_function( + 'mysql.db_remove', + name=self.testdb, + connection_user=self.user, + connection_pass=self.password, + connection_db='mysql', + ) + + @destructiveTest + def test_update_file_query(self): + ''' + Test query without any output + ''' + ret = self.run_function( + 'mysql.file_query', + database=self.testdb, + file_name='salt://mysql/update_query.sql', + character_set='utf8', + collate='utf8_general_ci', + connection_user=self.user, + connection_pass=self.password + ) + self.assertTrue('query time' in ret) + ret.pop('query time') + self.assertEqual(ret, {'rows affected': 2}) + + @destructiveTest + def test_select_file_query(self): + ''' + Test query with table output + ''' + ret = self.run_function( + 'mysql.file_query', + database=self.testdb, + file_name='salt://mysql/select_query.sql', + character_set='utf8', + collate='utf8_general_ci', + connection_user=self.user, + connection_pass=self.password + ) + expected = { + 'rows affected': 5, + 'rows returned': 4, + 'results': [ + [ + ['2'], + ['3'], + ['4'], + ['5'] + ] + ], + 'columns': [ + ['a'] + ], + } + self.assertTrue('query time' in ret) + ret.pop('query time') + self.assertEqual(ret, expected) From 2e67d2c298d129bf40fff8c6c8454606dde5ab18 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Tue, 19 Sep 2017 17:41:14 -0600 Subject: [PATCH 118/633] Added newline at the end of the file This is needed to satisfy the linter. --- salt/pillar/rethinkdb_pillar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/pillar/rethinkdb_pillar.py b/salt/pillar/rethinkdb_pillar.py index bf7c816221..cf4b2c56f8 100644 --- a/salt/pillar/rethinkdb_pillar.py +++ b/salt/pillar/rethinkdb_pillar.py @@ -160,4 +160,4 @@ def ext_pillar(minion_id, else: # No document found in the database log.debug('ext_pillar.rethinkdb: no document found') - return {} \ No newline at end of file + return {} From 56cd88dfa5d69c939cd23fdbccca0d9338a873d9 Mon Sep 17 00:00:00 2001 From: Andrei Belov Date: Wed, 20 Sep 2017 08:07:13 +0300 Subject: [PATCH 119/633] Several fixes for RDS DB parameter group management In particular: - it is now possible to manage all the parameters in a group, without limiting to MaxRecords=100 (thanks to pagination); - update_parameter_group() now composes valid JSON payload, automatically substitutes boolean values to 'on' / 'off' strings; - parameter_present() now shows actual error message produced by ModifyDBParameterGroup API call. --- salt/modules/boto_rds.py | 35 ++++++++++++++++++++--------------- salt/states/boto_rds.py | 13 ++++++++----- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/salt/modules/boto_rds.py b/salt/modules/boto_rds.py index f57b9633de..cf778bd86e 100644 --- a/salt/modules/boto_rds.py +++ b/salt/modules/boto_rds.py @@ -505,10 +505,17 @@ def update_parameter_group(name, parameters, apply_method="pending-reboot", param_list = [] for key, value in six.iteritems(parameters): - item = (key, value, apply_method) + item = odict.OrderedDict() + item.update({'ParameterName': key}) + item.update({'ApplyMethod': apply_method}) + if type(value) is bool: + item.update({'ParameterValue': 'on' if value else 'off'}) + else: + item.update({'ParameterValue': str(value)}) param_list.append(item) - if not len(param_list): - return {'results': False} + + if not len(param_list): + return {'results': False} try: conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) @@ -843,6 +850,7 @@ def describe_parameters(name, Source=None, MaxRecords=None, Marker=None, 'message': 'Could not establish a connection to RDS'} kwargs = {} + kwargs.update({'DBParameterGroupName': name}) for key in ('Marker', 'Source'): if locals()[key] is not None: kwargs[key] = str(locals()[key]) @@ -850,26 +858,23 @@ def describe_parameters(name, Source=None, MaxRecords=None, Marker=None, if locals()['MaxRecords'] is not None: kwargs['MaxRecords'] = int(locals()['MaxRecords']) - r = conn.describe_db_parameters(DBParameterGroupName=name, **kwargs) + pag = conn.get_paginator('describe_db_parameters') + pit = pag.paginate(**kwargs) - if not r: - return {'result': False, - 'message': 'Failed to get RDS parameters for group {0}.' - .format(name)} - - results = r['Parameters'] keys = ['ParameterName', 'ParameterValue', 'Description', 'Source', 'ApplyType', 'DataType', 'AllowedValues', 'IsModifieable', 'MinimumEngineVersion', 'ApplyMethod'] parameters = odict.OrderedDict() ret = {'result': True} - for result in results: - data = odict.OrderedDict() - for k in keys: - data[k] = result.get(k) - parameters[result.get('ParameterName')] = data + for p in pit: + for result in p['Parameters']: + data = odict.OrderedDict() + for k in keys: + data[k] = result.get(k) + + parameters[result.get('ParameterName')] = data ret['parameters'] = parameters return ret diff --git a/salt/states/boto_rds.py b/salt/states/boto_rds.py index c3bc766155..c35eea5848 100644 --- a/salt/states/boto_rds.py +++ b/salt/states/boto_rds.py @@ -697,7 +697,10 @@ def parameter_present(name, db_parameter_group_family, description, parameters=N changed = {} for items in parameters: for k, value in items.items(): - params[k] = value + if type(value) is bool: + params[k] = 'on' if value else 'off' + else: + params[k] = str(value) logging.debug('Parameters from user are : {0}.'.format(params)) options = __salt__['boto_rds.describe_parameters'](name=name, region=region, key=key, keyid=keyid, profile=profile) if not options.get('result'): @@ -705,8 +708,8 @@ def parameter_present(name, db_parameter_group_family, description, parameters=N ret['comment'] = os.linesep.join([ret['comment'], 'Faled to get parameters for group {0}.'.format(name)]) return ret for parameter in options['parameters'].values(): - if parameter['ParameterName'] in params and str(params.get(parameter['ParameterName'])) != str(parameter['ParameterValue']): - logging.debug('Values that are being compared are {0}:{1} .'.format(params.get(parameter['ParameterName']), parameter['ParameterValue'])) + if parameter['ParameterName'] in params and params.get(parameter['ParameterName']) != str(parameter['ParameterValue']): + logging.debug('Values that are being compared for {0} are {1}:{2} .'.format(parameter['ParameterName'], params.get(parameter['ParameterName']), parameter['ParameterValue'])) changed[parameter['ParameterName']] = params.get(parameter['ParameterName']) if len(changed) > 0: if __opts__['test']: @@ -715,9 +718,9 @@ def parameter_present(name, db_parameter_group_family, description, parameters=N return ret update = __salt__['boto_rds.update_parameter_group'](name, parameters=changed, apply_method=apply_method, tags=tags, region=region, key=key, keyid=keyid, profile=profile) - if not update: + if 'error' in update: ret['result'] = False - ret['comment'] = os.linesep.join([ret['comment'], 'Failed to change parameters {0} for group {1}.'.format(changed, name)]) + ret['comment'] = os.linesep.join([ret['comment'], 'Failed to change parameters {0} for group {1}:'.format(changed, name), update['error']['message']]) return ret ret['changes']['Parameters'] = changed ret['comment'] = os.linesep.join([ret['comment'], 'Parameters {0} for group {1} are changed.'.format(changed, name)]) From 4e8da3045f11b11f95b02d45f04b6c411f5cf0e5 Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Wed, 20 Sep 2017 10:12:32 +0200 Subject: [PATCH 120/633] Fixed logic for windows fallback Silly error - should have been the other way around. --- salt/modules/kubernetes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index b5628d41d3..1afa8d8569 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -83,7 +83,7 @@ def __virtual__(): return False, 'python kubernetes library not found' -if salt.utils.is_windows(): +if not salt.utils.is_windows(): @contextmanager def _time_limit(seconds): def signal_handler(signum, frame): @@ -713,7 +713,7 @@ def delete_deployment(name, namespace='default', **kwargs): namespace=namespace, body=body) mutable_api_response = api_response.to_dict() - if salt.utils.is_windows(): + if not salt.utils.is_windows(): try: with _time_limit(POLLING_TIME_LIMIT): while show_deployment(name, namespace) is not None: From 3a089e450f93ad8b218bf45c7c00e09cd291d2b0 Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Wed, 20 Sep 2017 14:26:06 +0200 Subject: [PATCH 121/633] Added tests for pid-file deletion in DaemonMixIn This is a follow up on this PR: https://github.com/saltstack/salt/pull/43366 Since we can get an OSError durin PIDfile deletion with non-root users, it would make sense to also test for this. So here are the two test cases. One with an OSError and the other one without. --- tests/unit/utils/parsers_test.py | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/unit/utils/parsers_test.py b/tests/unit/utils/parsers_test.py index f6cdb2c9c0..6a7b674727 100644 --- a/tests/unit/utils/parsers_test.py +++ b/tests/unit/utils/parsers_test.py @@ -21,6 +21,7 @@ import salt.utils.parsers import salt.log.setup as log import salt.config import salt.syspaths +from salt.utils.parsers import DaemonMixIn ensure_in_syspath('../../') @@ -803,6 +804,62 @@ class SaltRunOptionParserTestCase(LogSettingsParserTests): self.parser = salt.utils.parsers.SaltRunOptionParser +@skipIf(NO_MOCK, NO_MOCK_REASON) +class DaemonMixInTestCase(LogSettingsParserTests): + ''' + Tests parsing Salt Master options + ''' + def setUp(self): + ''' + Setting up + ''' + # Set defaults + self.default_config = salt.config.DEFAULT_MASTER_OPTS + + # Log file + self.log_file = '/tmp/salt_run_parser_test' + # Function to patch + self.config_func = 'salt.config.master_config' + + # Mock log setup + self.setup_log() + + # Assign parser + self.parser = salt.utils.parsers.SaltRunOptionParser + + # Set PID + self.pid = '/some/fake.pid' + + # Setup mixin + self.mixin = DaemonMixIn() + self.mixin.info = None + self.mixin.config = {} + self.mixin.config['pidfile'] = self.pid + + def test_pid_file_deletion(self): + ''' + PIDfile deletion without exception. + ''' + with patch('os.unlink', MagicMock()) as os_unlink: + with patch('os.path.isfile', MagicMock(return_value=True)): + with patch.object(self.mixin, 'info', MagicMock()): + self.mixin._mixin_before_exit() + assert self.mixin.info.call_count == 0 + assert os_unlink.call_count == 1 + + def test_pid_file_deletion_with_oserror(self): + ''' + PIDfile deletion with exception + ''' + with patch('os.unlink', MagicMock(side_effect=OSError())) as os_unlink: + with patch('os.path.isfile', MagicMock(return_value=True)): + with patch.object(self.mixin, 'info', MagicMock()): + self.mixin._mixin_before_exit() + assert os_unlink.call_count == 1 + self.mixin.info.assert_called_with( + 'PIDfile could not be deleted: {}'.format(self.pid)) + + @skipIf(NO_MOCK, NO_MOCK_REASON) class SaltSSHOptionParserTestCase(LogSettingsParserTests): ''' @@ -944,4 +1001,5 @@ if __name__ == '__main__': SaltCloudParserTestCase, SPMParserTestCase, SaltAPIParserTestCase, + DaemonMixInTestCase, needs_daemon=False) From 08fba98735b7e32ebb7259a2e6ade34153969eee Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Wed, 20 Sep 2017 15:37:24 +0200 Subject: [PATCH 122/633] Fixed several issues with the test * Removed redundant import. * No longer inheriting from LogSettingsParserTests. * Replaced test class description with somethin meaninful. * Fixed identation. I've also moved the class to the bottom, since all the classes inheriting from LogSettingsParserTests are in the block above. --- tests/unit/utils/parsers_test.py | 99 ++++++++++++++------------------ 1 file changed, 42 insertions(+), 57 deletions(-) diff --git a/tests/unit/utils/parsers_test.py b/tests/unit/utils/parsers_test.py index 6a7b674727..ab3abf86ba 100644 --- a/tests/unit/utils/parsers_test.py +++ b/tests/unit/utils/parsers_test.py @@ -21,7 +21,6 @@ import salt.utils.parsers import salt.log.setup as log import salt.config import salt.syspaths -from salt.utils.parsers import DaemonMixIn ensure_in_syspath('../../') @@ -804,62 +803,6 @@ class SaltRunOptionParserTestCase(LogSettingsParserTests): self.parser = salt.utils.parsers.SaltRunOptionParser -@skipIf(NO_MOCK, NO_MOCK_REASON) -class DaemonMixInTestCase(LogSettingsParserTests): - ''' - Tests parsing Salt Master options - ''' - def setUp(self): - ''' - Setting up - ''' - # Set defaults - self.default_config = salt.config.DEFAULT_MASTER_OPTS - - # Log file - self.log_file = '/tmp/salt_run_parser_test' - # Function to patch - self.config_func = 'salt.config.master_config' - - # Mock log setup - self.setup_log() - - # Assign parser - self.parser = salt.utils.parsers.SaltRunOptionParser - - # Set PID - self.pid = '/some/fake.pid' - - # Setup mixin - self.mixin = DaemonMixIn() - self.mixin.info = None - self.mixin.config = {} - self.mixin.config['pidfile'] = self.pid - - def test_pid_file_deletion(self): - ''' - PIDfile deletion without exception. - ''' - with patch('os.unlink', MagicMock()) as os_unlink: - with patch('os.path.isfile', MagicMock(return_value=True)): - with patch.object(self.mixin, 'info', MagicMock()): - self.mixin._mixin_before_exit() - assert self.mixin.info.call_count == 0 - assert os_unlink.call_count == 1 - - def test_pid_file_deletion_with_oserror(self): - ''' - PIDfile deletion with exception - ''' - with patch('os.unlink', MagicMock(side_effect=OSError())) as os_unlink: - with patch('os.path.isfile', MagicMock(return_value=True)): - with patch.object(self.mixin, 'info', MagicMock()): - self.mixin._mixin_before_exit() - assert os_unlink.call_count == 1 - self.mixin.info.assert_called_with( - 'PIDfile could not be deleted: {}'.format(self.pid)) - - @skipIf(NO_MOCK, NO_MOCK_REASON) class SaltSSHOptionParserTestCase(LogSettingsParserTests): ''' @@ -983,6 +926,48 @@ class SaltAPIParserTestCase(LogSettingsParserTests): self.parser = salt.utils.parsers.SaltAPIParser +@skipIf(NO_MOCK, NO_MOCK_REASON) +class DaemonMixInTestCase(TestCase): + ''' + Tests the PIDfile deletion in the DaemonMixIn. + ''' + + def setUp(self): + ''' + Setting up + ''' + # Set PID + self.pid = '/some/fake.pid' + + # Setup mixin + self.mixin = salt.utils.parsers.DaemonMixIn() + self.mixin.info = None + self.mixin.config = {} + self.mixin.config['pidfile'] = self.pid + + def test_pid_file_deletion(self): + ''' + PIDfile deletion without exception. + ''' + with patch('os.unlink', MagicMock()) as os_unlink: + with patch('os.path.isfile', MagicMock(return_value=True)): + with patch.object(self.mixin, 'info', MagicMock()): + self.mixin._mixin_before_exit() + assert self.mixin.info.call_count == 0 + assert os_unlink.call_count == 1 + + def test_pid_file_deletion_with_oserror(self): + ''' + PIDfile deletion with exception + ''' + with patch('os.unlink', MagicMock(side_effect=OSError())) as os_unlink: + with patch('os.path.isfile', MagicMock(return_value=True)): + with patch.object(self.mixin, 'info', MagicMock()): + self.mixin._mixin_before_exit() + assert os_unlink.call_count == 1 + self.mixin.info.assert_called_with( + 'PIDfile could not be deleted: {}'.format(self.pid)) + # Hide the class from unittest framework when it searches for TestCase classes in the module del LogSettingsParserTests From 96f39a420b974f8658de182d3af72b2a7e9f8b9b Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Wed, 20 Sep 2017 16:35:01 +0200 Subject: [PATCH 123/633] Fixed linting Fix for "String format call with un-indexed curly braces". --- tests/unit/utils/parsers_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/parsers_test.py b/tests/unit/utils/parsers_test.py index ab3abf86ba..254daa7e8c 100644 --- a/tests/unit/utils/parsers_test.py +++ b/tests/unit/utils/parsers_test.py @@ -966,7 +966,7 @@ class DaemonMixInTestCase(TestCase): self.mixin._mixin_before_exit() assert os_unlink.call_count == 1 self.mixin.info.assert_called_with( - 'PIDfile could not be deleted: {}'.format(self.pid)) + 'PIDfile could not be deleted: {0}'.format(self.pid)) # Hide the class from unittest framework when it searches for TestCase classes in the module del LogSettingsParserTests From 54842b501272c1730f7be9bec2bb1d5ce7187933 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 20 Sep 2017 16:58:17 -0400 Subject: [PATCH 124/633] Handle VPC/Subnet ID not found errors in boto_vpc module If a VPC or Subnet ID is not found when calling functions that are supposed to be checking for vpc/subnet ID existence, the return should be consistent by returning booleans/None instead of returning the NotFound error from AWS. The surrounding code blocks indicate that this is expected as well as unit test assertions. The moto library had a bug in it where it wasn't raising "x.NotFound" errors when it should have been. The latest version of moto has fixed this bug, causing our tests to fail since the boto_vpc module is not handling the "x.NotFound" errors separately from the generic BotoServerErrors. This fixes the test failures in the branch tests that were caused by upgrading the moto version to the latest release. --- salt/modules/boto_vpc.py | 102 ++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/salt/modules/boto_vpc.py b/salt/modules/boto_vpc.py index f18ae2d68a..bebaafdd57 100644 --- a/salt/modules/boto_vpc.py +++ b/salt/modules/boto_vpc.py @@ -598,9 +598,14 @@ def exists(vpc_id=None, name=None, cidr=None, tags=None, region=None, key=None, try: vpc_ids = _find_vpcs(vpc_id=vpc_id, vpc_name=name, cidr=cidr, tags=tags, region=region, key=key, keyid=keyid, profile=profile) - return {'exists': bool(vpc_ids)} - except BotoServerError as e: - return {'error': salt.utils.boto.get_error(e)} + except BotoServerError as err: + boto_err = salt.utils.boto.get_error(err) + if boto_err.get('aws', {}).get('code') == 'InvalidVpcID.NotFound': + # VPC was not found: handle the error and return False. + return {'exists': False} + return {'error': boto_err} + + return {'exists': bool(vpc_ids)} def create(cidr_block, instance_tenancy=None, vpc_name=None, @@ -722,27 +727,34 @@ def describe(vpc_id=None, vpc_name=None, region=None, key=None, try: conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) vpc_id = check_vpc(vpc_id, vpc_name, region, key, keyid, profile) - if not vpc_id: + except BotoServerError as err: + boto_err = salt.utils.boto.get_error(err) + if boto_err.get('aws', {}).get('code') == 'InvalidVpcID.NotFound': + # VPC was not found: handle the error and return None. return {'vpc': None} + return {'error': boto_err} - filter_parameters = {'vpc_ids': vpc_id} + if not vpc_id: + return {'vpc': None} + filter_parameters = {'vpc_ids': vpc_id} + + try: vpcs = conn.get_all_vpcs(**filter_parameters) + except BotoServerError as err: + return {'error': salt.utils.boto.get_error(err)} - if vpcs: - vpc = vpcs[0] # Found! - log.debug('Found VPC: {0}'.format(vpc.id)) + if vpcs: + vpc = vpcs[0] # Found! + log.debug('Found VPC: {0}'.format(vpc.id)) - keys = ('id', 'cidr_block', 'is_default', 'state', 'tags', - 'dhcp_options_id', 'instance_tenancy') - _r = dict([(k, getattr(vpc, k)) for k in keys]) - _r.update({'region': getattr(vpc, 'region').name}) - return {'vpc': _r} - else: - return {'vpc': None} - - except BotoServerError as e: - return {'error': salt.utils.boto.get_error(e)} + keys = ('id', 'cidr_block', 'is_default', 'state', 'tags', + 'dhcp_options_id', 'instance_tenancy') + _r = dict([(k, getattr(vpc, k)) for k in keys]) + _r.update({'region': getattr(vpc, 'region').name}) + return {'vpc': _r} + else: + return {'vpc': None} def describe_vpcs(vpc_id=None, name=None, cidr=None, tags=None, @@ -808,7 +820,7 @@ def _find_subnets(subnet_name=None, vpc_id=None, cidr=None, tags=None, conn=None Given subnet properties, find and return matching subnet ids ''' - if not any(subnet_name, tags, cidr): + if not any([subnet_name, tags, cidr]): raise SaltInvocationError('At least one of the following must be ' 'specified: subnet_name, cidr or tags.') @@ -926,34 +938,38 @@ def subnet_exists(subnet_id=None, name=None, subnet_name=None, cidr=None, try: conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) - filter_parameters = {'filters': {}} + except BotoServerError as err: + return {'error': salt.utils.boto.get_error(err)} - if subnet_id: - filter_parameters['subnet_ids'] = [subnet_id] - - if subnet_name: - filter_parameters['filters']['tag:Name'] = subnet_name - - if cidr: - filter_parameters['filters']['cidr'] = cidr - - if tags: - for tag_name, tag_value in six.iteritems(tags): - filter_parameters['filters']['tag:{0}'.format(tag_name)] = tag_value - - if zones: - filter_parameters['filters']['availability_zone'] = zones + filter_parameters = {'filters': {}} + if subnet_id: + filter_parameters['subnet_ids'] = [subnet_id] + if subnet_name: + filter_parameters['filters']['tag:Name'] = subnet_name + if cidr: + filter_parameters['filters']['cidr'] = cidr + if tags: + for tag_name, tag_value in six.iteritems(tags): + filter_parameters['filters']['tag:{0}'.format(tag_name)] = tag_value + if zones: + filter_parameters['filters']['availability_zone'] = zones + try: subnets = conn.get_all_subnets(**filter_parameters) - log.debug('The filters criteria {0} matched the following subnets:{1}'.format(filter_parameters, subnets)) - if subnets: - log.info('Subnet {0} exists.'.format(subnet_name or subnet_id)) - return {'exists': True} - else: - log.info('Subnet {0} does not exist.'.format(subnet_name or subnet_id)) + except BotoServerError as err: + boto_err = salt.utils.boto.get_error(err) + if boto_err.get('aws', {}).get('code') == 'InvalidSubnetID.NotFound': + # Subnet was not found: handle the error and return False. return {'exists': False} - except BotoServerError as e: - return {'error': salt.utils.boto.get_error(e)} + return {'error': boto_err} + + log.debug('The filters criteria {0} matched the following subnets:{1}'.format(filter_parameters, subnets)) + if subnets: + log.info('Subnet {0} exists.'.format(subnet_name or subnet_id)) + return {'exists': True} + else: + log.info('Subnet {0} does not exist.'.format(subnet_name or subnet_id)) + return {'exists': False} def get_subnet_association(subnets, region=None, key=None, keyid=None, From 625eabb83f4ec7bd1e2ef529ba36d4d809d069ab Mon Sep 17 00:00:00 2001 From: Silvio Moioli Date: Wed, 20 Sep 2017 14:32:47 +0200 Subject: [PATCH 125/633] multiprocessing minion option: documentation fixes --- doc/man/salt.7 | 1 + doc/ref/configuration/minion.rst | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/man/salt.7 b/doc/man/salt.7 index 7d4f5c2ed5..7bc0ab64d3 100644 --- a/doc/man/salt.7 +++ b/doc/man/salt.7 @@ -10795,6 +10795,7 @@ cmd_whitelist_glob: .UNINDENT .UNINDENT .SS Thread Settings +.SS \fBmultiprocessing\fP .sp Default: \fBTrue\fP .sp diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index ded0b72699..31317a06fc 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -2199,11 +2199,14 @@ Thread Settings .. conf_minion:: multiprocessing +``multiprocessing`` +------- + Default: ``True`` -If `multiprocessing` is enabled when a minion receives a +If ``multiprocessing`` is enabled when a minion receives a publication a new process is spawned and the command is executed therein. -Conversely, if `multiprocessing` is disabled the new publication will be run +Conversely, if ``multiprocessing`` is disabled the new publication will be run executed in a thread. From fda66f3e9265146fa11a706b68bfaadd11451095 Mon Sep 17 00:00:00 2001 From: Joaquin Veira Date: Thu, 21 Sep 2017 13:38:27 +0200 Subject: [PATCH 126/633] corrected identation --- salt/returners/zabbix_return.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/salt/returners/zabbix_return.py b/salt/returners/zabbix_return.py index bdf94d9749..a5e79ca8e0 100644 --- a/salt/returners/zabbix_return.py +++ b/salt/returners/zabbix_return.py @@ -57,20 +57,20 @@ def zbx(): def zabbix_send(key, host, output): with salt.utils.fopen(zbx()['zabbix_config'],'r') as file_handle: - for line in file_handle: - if "ServerActive" in line: - flag = "true" - server = line.rsplit('=') - server = server[1].rsplit(',') - for s in server: - cmd = zbx()['sender'] + " -z " + s.replace('\n','') + " -s " + host + " -k " + key + " -o \"" + output +"\"" - __salt__['cmd.shell'](cmd) - break - else: - flag = "false" - if flag == 'false': - cmd = zbx()['sender'] + " -c " + zbx()['config'] + " -s " + host + " -k " + key + " -o \"" + output +"\"" - f.close() + for line in file_handle: + if "ServerActive" in line: + flag = "true" + server = line.rsplit('=') + server = server[1].rsplit(',') + for s in server: + cmd = zbx()['sender'] + " -z " + s.replace('\n','') + " -s " + host + " -k " + key + " -o \"" + output +"\"" + __salt__['cmd.shell'](cmd) + break + else: + flag = "false" + if flag == 'false': + cmd = zbx()['sender'] + " -c " + zbx()['config'] + " -s " + host + " -k " + key + " -o \"" + output +"\"" + file_handle.close() def returner(ret): From 039d2369487bdfa7963909a7b11fa627070ec8d8 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Thu, 21 Sep 2017 16:44:53 +0300 Subject: [PATCH 127/633] Fixed `list` and `contains` redis cache logic. Wrong keys was used to retrieve the data. --- salt/cache/redis_cache.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/salt/cache/redis_cache.py b/salt/cache/redis_cache.py index b02a0851e5..0f52dfd6ad 100644 --- a/salt/cache/redis_cache.py +++ b/salt/cache/redis_cache.py @@ -421,18 +421,17 @@ def list_(bank): Lists entries stored in the specified bank. ''' redis_server = _get_redis_server() - bank_keys_redis_key = _get_bank_keys_redis_key(bank) - bank_keys = None + bank_redis_key = _get_bank_redis_key(bank) try: - bank_keys = redis_server.smembers(bank_keys_redis_key) + banks = redis_server.smembers(bank_redis_key) except (RedisConnectionError, RedisResponseError) as rerr: - mesg = 'Cannot list the Redis cache key {rkey}: {rerr}'.format(rkey=bank_keys_redis_key, + mesg = 'Cannot list the Redis cache key {rkey}: {rerr}'.format(rkey=bank_redis_key, rerr=rerr) log.error(mesg) raise SaltCacheError(mesg) - if not bank_keys: + if not banks: return [] - return list(bank_keys) + return list(banks) def contains(bank, key): @@ -440,15 +439,14 @@ def contains(bank, key): Checks if the specified bank contains the specified key. ''' redis_server = _get_redis_server() - bank_keys_redis_key = _get_bank_keys_redis_key(bank) - bank_keys = None + bank_redis_key = _get_bank_redis_key(bank) try: - bank_keys = redis_server.smembers(bank_keys_redis_key) + banks = redis_server.smembers(bank_redis_key) except (RedisConnectionError, RedisResponseError) as rerr: - mesg = 'Cannot retrieve the Redis cache key {rkey}: {rerr}'.format(rkey=bank_keys_redis_key, + mesg = 'Cannot retrieve the Redis cache key {rkey}: {rerr}'.format(rkey=bank_redis_key, rerr=rerr) log.error(mesg) raise SaltCacheError(mesg) - if not bank_keys: + if not banks: return False - return key in bank_keys + return key in banks From 3fb42bc238a7bf2e379686cfbf33db06884ffa58 Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 8 Sep 2017 17:10:07 +0200 Subject: [PATCH 128/633] Fix env_order in state.py Fixes #42165 --- salt/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/state.py b/salt/state.py index 729e740f5f..bb6f71b149 100644 --- a/salt/state.py +++ b/salt/state.py @@ -2906,7 +2906,7 @@ class BaseHighState(object): Returns: {'saltenv': ['state1', 'state2', ...]} ''' - matches = {} + matches = DefaultOrderedDict(OrderedDict) # pylint: disable=cell-var-from-loop for saltenv, body in six.iteritems(top): if self.opts['environment']: From d91c47c6f0422ec4c0e3d851a2b2275cb59d506b Mon Sep 17 00:00:00 2001 From: Raymond Piller Date: Wed, 20 Sep 2017 21:41:08 -0500 Subject: [PATCH 129/633] Salt Repo has Deb 9 and 8 --- doc/topics/installation/debian.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/topics/installation/debian.rst b/doc/topics/installation/debian.rst index 36a47fa8ff..369991ebaa 100644 --- a/doc/topics/installation/debian.rst +++ b/doc/topics/installation/debian.rst @@ -18,7 +18,7 @@ Installation from official Debian and Raspbian repositories is described Installation from the Official SaltStack Repository =================================================== -Packages for Debian 8 (Jessie) and Debian 7 (Wheezy) are available in the +Packages for Debian 9 (Stretch) and Debian 8 (Jessie) are available in the Official SaltStack repository. Instructions are at https://repo.saltstack.com/#debian. From 2fd88e94fabbd9d1110689772fcdb6a62a2b168d Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 21 Sep 2017 10:11:28 -0500 Subject: [PATCH 130/633] Fix RST headers for runners (2016.11 branch) To conform with the rest of the rst files for runner docs, they should only contain the module name. --- doc/ref/runners/all/salt.runners.auth.rst | 4 ++-- doc/ref/runners/all/salt.runners.event.rst | 4 ++-- doc/ref/runners/all/salt.runners.smartos_vmadm.rst | 4 ++-- doc/ref/runners/all/salt.runners.vistara.rst | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/ref/runners/all/salt.runners.auth.rst b/doc/ref/runners/all/salt.runners.auth.rst index b82907d4d1..a3d933f2e4 100644 --- a/doc/ref/runners/all/salt.runners.auth.rst +++ b/doc/ref/runners/all/salt.runners.auth.rst @@ -1,5 +1,5 @@ -salt.runners.auth module -======================== +salt.runners.auth +================= .. automodule:: salt.runners.auth :members: diff --git a/doc/ref/runners/all/salt.runners.event.rst b/doc/ref/runners/all/salt.runners.event.rst index 9b07aa9988..c2d505a1f2 100644 --- a/doc/ref/runners/all/salt.runners.event.rst +++ b/doc/ref/runners/all/salt.runners.event.rst @@ -1,5 +1,5 @@ -salt.runners.event module -========================= +salt.runners.event +================== .. automodule:: salt.runners.event :members: diff --git a/doc/ref/runners/all/salt.runners.smartos_vmadm.rst b/doc/ref/runners/all/salt.runners.smartos_vmadm.rst index 5ee3d03eb1..7b5a7c4834 100644 --- a/doc/ref/runners/all/salt.runners.smartos_vmadm.rst +++ b/doc/ref/runners/all/salt.runners.smartos_vmadm.rst @@ -1,5 +1,5 @@ -salt.runners.smartos_vmadm module -================================= +salt.runners.smartos_vmadm +========================== .. automodule:: salt.runners.smartos_vmadm :members: diff --git a/doc/ref/runners/all/salt.runners.vistara.rst b/doc/ref/runners/all/salt.runners.vistara.rst index a66b06f6d2..0f1400f4c7 100644 --- a/doc/ref/runners/all/salt.runners.vistara.rst +++ b/doc/ref/runners/all/salt.runners.vistara.rst @@ -1,5 +1,5 @@ -salt.runners.vistara module -=========================== +salt.runners.vistara +==================== .. automodule:: salt.runners.vistara :members: From c0a79c70a447271fef957f735f53405b01d1b720 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 21 Sep 2017 10:27:12 -0500 Subject: [PATCH 131/633] Fix RST headers for runners (2017.7 branch) To conform with the rest of the rst files for runner docs, they should only contain the module name. --- doc/ref/runners/all/salt.runners.digicertapi.rst | 4 ++-- doc/ref/runners/all/salt.runners.mattermost.rst | 4 ++-- doc/ref/runners/all/salt.runners.vault.rst | 4 ++-- doc/ref/runners/all/salt.runners.venafiapi.rst | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/ref/runners/all/salt.runners.digicertapi.rst b/doc/ref/runners/all/salt.runners.digicertapi.rst index 10919c8a91..280fc059fa 100644 --- a/doc/ref/runners/all/salt.runners.digicertapi.rst +++ b/doc/ref/runners/all/salt.runners.digicertapi.rst @@ -1,5 +1,5 @@ -salt.runners.digicertapi module -=============================== +salt.runners.digicertapi +======================== .. automodule:: salt.runners.digicertapi :members: diff --git a/doc/ref/runners/all/salt.runners.mattermost.rst b/doc/ref/runners/all/salt.runners.mattermost.rst index 7fa1e2f3d4..c33a9f459b 100644 --- a/doc/ref/runners/all/salt.runners.mattermost.rst +++ b/doc/ref/runners/all/salt.runners.mattermost.rst @@ -1,5 +1,5 @@ -salt.runners.mattermost module -============================== +salt.runners.mattermost +======================= **Note for 2017.7 releases!** diff --git a/doc/ref/runners/all/salt.runners.vault.rst b/doc/ref/runners/all/salt.runners.vault.rst index 7c424f24ee..434774b0dd 100644 --- a/doc/ref/runners/all/salt.runners.vault.rst +++ b/doc/ref/runners/all/salt.runners.vault.rst @@ -1,5 +1,5 @@ -salt.runners.vault module -========================= +salt.runners.vault +================== .. automodule:: salt.runners.vault :members: diff --git a/doc/ref/runners/all/salt.runners.venafiapi.rst b/doc/ref/runners/all/salt.runners.venafiapi.rst index 9fd9c41de4..d7e4d545eb 100644 --- a/doc/ref/runners/all/salt.runners.venafiapi.rst +++ b/doc/ref/runners/all/salt.runners.venafiapi.rst @@ -1,5 +1,5 @@ -salt.runners.venafiapi module -============================= +salt.runners.venafiapi +====================== .. automodule:: salt.runners.venafiapi :members: From 6fcb7e7739cc88d7684857b7a1939af62b9debdf Mon Sep 17 00:00:00 2001 From: Vladimir Nadvornik Date: Thu, 21 Sep 2017 17:43:05 +0200 Subject: [PATCH 132/633] Minor bugfixes - do not try to add devices if assemble failed - sort uuid list in error message --- salt/states/mdadm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/states/mdadm.py b/salt/states/mdadm.py index 4588d859fa..64cae4a6d6 100644 --- a/salt/states/mdadm.py +++ b/salt/states/mdadm.py @@ -107,7 +107,8 @@ def present(name, new_devices.append(dev) if len(uuid_dict) > 1: - ret['comment'] = 'Devices are a mix of RAID constituents with multiple MD_UUIDs: {0}.'.format(uuid_dict.keys()) + ret['comment'] = 'Devices are a mix of RAID constituents with multiple MD_UUIDs: {0}.'.format( + sorted(uuid_dict.keys())) ret['result'] = False return ret elif len(uuid_dict) == 1: @@ -192,7 +193,7 @@ def present(name, else: ret['comment'] = 'Raid {0} already present.'.format(name) - if (do_assemble or present) and len(new_devices) > 0: + if (do_assemble or present) and len(new_devices) > 0 and ret['result']: for d in new_devices: res = __salt__['raid.add'](name, d) if not res: From 1c006db2a687ffbab858e73b762adc20e355dda9 Mon Sep 17 00:00:00 2001 From: Vladimir Nadvornik Date: Thu, 21 Sep 2017 17:46:15 +0200 Subject: [PATCH 133/633] Fix and extend mdadm unit tests --- tests/unit/states/test_mdadm.py | 138 ++++++++++++++++++++++++++------ 1 file changed, 115 insertions(+), 23 deletions(-) diff --git a/tests/unit/states/test_mdadm.py b/tests/unit/states/test_mdadm.py index 9095a1926d..3e0cbdf7ea 100644 --- a/tests/unit/states/test_mdadm.py +++ b/tests/unit/states/test_mdadm.py @@ -32,41 +32,133 @@ class MdadmTestCase(TestCase, LoaderModuleMockMixin): ''' Test to verify that the raid is present ''' - ret = [{'changes': {}, 'comment': 'Raid salt already present', + ret = [{'changes': {}, 'comment': 'Raid salt already present.', 'name': 'salt', 'result': True}, {'changes': {}, - 'comment': "Devices are a mix of RAID constituents" - " (['dev0']) and non-RAID-constituents(['dev1']).", + 'comment': "Devices are a mix of RAID constituents with multiple MD_UUIDs:" + " ['6be5fc45:05802bba:1c2d6722:666f0e03', 'ffffffff:ffffffff:ffffffff:ffffffff'].", 'name': 'salt', 'result': False}, {'changes': {}, 'comment': 'Raid will be created with: True', 'name': 'salt', 'result': None}, {'changes': {}, 'comment': 'Raid salt failed to be created.', + 'name': 'salt', 'result': False}, + {'changes': {'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}, 'comment': 'Raid salt created.', + 'name': 'salt', 'result': True}, + {'changes': {'added': ['dev1'], 'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}, + 'comment': 'Raid salt assembled. Added new device dev1 to salt.\n', + 'name': 'salt', 'result': True}, + {'changes': {'added': ['dev1']}, + 'comment': 'Raid salt already present. Added new device dev1 to salt.\n', + 'name': 'salt', 'result': True}, + {'changes': {}, 'comment': 'Raid salt failed to be assembled.', 'name': 'salt', 'result': False}] - mock = MagicMock(side_effect=[{'salt': True}, {'salt': False}, - {'salt': False}, {'salt': False}, - {'salt': False}]) - with patch.dict(mdadm.__salt__, {'raid.list': mock}): - self.assertEqual(mdadm.present("salt", 5, "dev0"), ret[0]) + mock_raid_list_exists = MagicMock(return_value={'salt': {'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}}) + mock_raid_list_missing = MagicMock(return_value={}) - mock = MagicMock(side_effect=[0, 1]) - with patch.dict(mdadm.__salt__, {'cmd.retcode': mock}): - self.assertDictEqual(mdadm.present("salt", 5, - ["dev0", "dev1"]), - ret[1]) + mock_file_access_ok = MagicMock(return_value=True) - mock = MagicMock(return_value=True) - with patch.dict(mdadm.__salt__, {'cmd.retcode': mock}): - with patch.dict(mdadm.__opts__, {'test': True}): - with patch.dict(mdadm.__salt__, {'raid.create': mock}): - self.assertDictEqual(mdadm.present("salt", 5, "dev0"), - ret[2]) + mock_raid_examine_ok = MagicMock(return_value={'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}) + mock_raid_examine_missing = MagicMock(return_value={}) - with patch.dict(mdadm.__opts__, {'test': False}): - with patch.dict(mdadm.__salt__, {'raid.create': mock}): - self.assertDictEqual(mdadm.present("salt", 5, "dev0"), - ret[3]) + mock_raid_create_success = MagicMock(return_value=True) + mock_raid_create_fail = MagicMock(return_value=False) + + mock_raid_assemble_success = MagicMock(return_value=True) + mock_raid_assemble_fail = MagicMock(return_value=False) + + mock_raid_add_success = MagicMock(return_value=True) + + mock_raid_save_config = MagicMock(return_value=True) + + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_exists, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_ok + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertEqual(mdadm.present("salt", 5, "dev0"), ret[0]) + + mock_raid_examine_mixed = MagicMock(side_effect=[ + {'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}, {'MD_UUID': 'ffffffff:ffffffff:ffffffff:ffffffff'}, + ]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_missing, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_mixed + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertEqual(mdadm.present("salt", 5, ["dev0", "dev1"]), ret[1]) + + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_missing, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_missing, + 'raid.create': mock_raid_create_success + }): + with patch.dict(mdadm.__opts__, {'test': True}): + self.assertDictEqual(mdadm.present("salt", 5, "dev0"), ret[2]) + + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_missing, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_missing, + 'raid.create': mock_raid_create_fail + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, "dev0"), ret[3]) + + mock_raid_list_create = MagicMock(side_effect=[{}, {'salt': {'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}}]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_create, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_missing, + 'raid.create': mock_raid_create_success, + 'raid.save_config': mock_raid_save_config + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, "dev0"), ret[4]) + + mock_raid_examine_replaced = MagicMock(side_effect=[ + {'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}, {}, + ]) + mock_raid_list_create = MagicMock(side_effect=[{}, {'salt': {'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}}]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_create, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_replaced, + 'raid.assemble': mock_raid_assemble_success, + 'raid.add': mock_raid_add_success, + 'raid.save_config': mock_raid_save_config + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, ["dev0", "dev1"]), ret[5]) + + mock_raid_examine_replaced = MagicMock(side_effect=[ + {'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}, {}, + ]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_exists, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_replaced, + 'raid.add': mock_raid_add_success, + 'raid.save_config': mock_raid_save_config + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, ["dev0", "dev1"]), ret[6]) + + mock_raid_examine_replaced = MagicMock(side_effect=[ + {'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}, {}, + ]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_missing, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_replaced, + 'raid.assemble': mock_raid_assemble_fail, + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, ["dev0", "dev1"]), ret[7]) def test_absent(self): ''' From 9b74634b23044315fed3898e6f9f005198ec1ea7 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 21 Sep 2017 10:54:27 -0500 Subject: [PATCH 134/633] Fix badly-formatted RST in mattermost runner docstring --- salt/runners/mattermost.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/runners/mattermost.py b/salt/runners/mattermost.py index 2bd3d928c4..686c9602ef 100644 --- a/salt/runners/mattermost.py +++ b/salt/runners/mattermost.py @@ -6,9 +6,10 @@ Module for sending messages to Mattermost :configuration: This module can be used by either passing an api_url and hook directly or by specifying both in a configuration profile in the salt - master/minion config. - For example: + master/minion config. For example: + .. code-block:: yaml + mattermost: hook: peWcBiMOS9HrZG15peWcBiMOS9HrZG15 api_url: https://example.com From 292f8c79b8694aaded47375ae867d152fcd9c545 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Thu, 21 Sep 2017 10:26:00 -0600 Subject: [PATCH 135/633] correct default value for salt.cache.Cache --- salt/cache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cache/__init__.py b/salt/cache/__init__.py index 94d7a36f1e..fc5e5f0972 100644 --- a/salt/cache/__init__.py +++ b/salt/cache/__init__.py @@ -73,7 +73,7 @@ class Cache(object): self.cachedir = opts.get('cachedir', salt.syspaths.CACHE_DIR) else: self.cachedir = cachedir - self.driver = opts.get('cache', salt.config.DEFAULT_MASTER_OPTS) + self.driver = opts.get('cache', salt.config.DEFAULT_MASTER_OPTS['cache']) self.serial = Serial(opts) self._modules = None self._kwargs = kwargs From 84f34c93beee2cdf74cd1ee85d743d963bac4dba Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 21 Sep 2017 11:55:40 -0500 Subject: [PATCH 136/633] Backport the non-fileclient changes from PR 43518 to 2017.7 This fixes the unnecessary re-downloading reported in #38971 in 2017.7 without using the new fileclient capabilities added in develop. It includes a helper function in the `file.cached` state that will need to be removed once we merge forward into develop. --- salt/modules/file.py | 30 +-- salt/states/archive.py | 234 ++++++++++++----------- salt/states/file.py | 416 +++++++++++++++++++++++++++++++++++++++-- salt/utils/files.py | 22 +++ 4 files changed, 548 insertions(+), 154 deletions(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index b8f1cdb00c..21a60dda51 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -54,7 +54,8 @@ import salt.utils.files import salt.utils.locales import salt.utils.templates import salt.utils.url -from salt.exceptions import CommandExecutionError, MinionError, SaltInvocationError, get_error_message as _get_error_message +from salt.exceptions import CommandExecutionError, SaltInvocationError, get_error_message as _get_error_message +from salt.utils.files import HASHES, HASHES_REVMAP log = logging.getLogger(__name__) @@ -62,16 +63,6 @@ __func_alias__ = { 'makedirs_': 'makedirs' } -HASHES = { - 'sha512': 128, - 'sha384': 96, - 'sha256': 64, - 'sha224': 56, - 'sha1': 40, - 'md5': 32, -} -HASHES_REVMAP = dict([(y, x) for x, y in six.iteritems(HASHES)]) - def __virtual__(): ''' @@ -3627,14 +3618,8 @@ def source_list(source, source_hash, saltenv): ret = (single_src, single_hash) break elif proto.startswith('http') or proto == 'ftp': - try: - if __salt__['cp.cache_file'](single_src): - ret = (single_src, single_hash) - break - except MinionError as exc: - # Error downloading file. Log the caught exception and - # continue on to the next source. - log.exception(exc) + ret = (single_src, single_hash) + break elif proto == 'file' and os.path.exists(urlparsed_single_src.path): ret = (single_src, single_hash) break @@ -3654,9 +3639,8 @@ def source_list(source, source_hash, saltenv): ret = (single, source_hash) break elif proto.startswith('http') or proto == 'ftp': - if __salt__['cp.cache_file'](single): - ret = (single, source_hash) - break + ret = (single, source_hash) + break elif single.startswith('/') and os.path.exists(single): ret = (single, source_hash) break @@ -4478,7 +4462,7 @@ def check_file_meta( ''' changes = {} if not source_sum: - source_sum = dict() + source_sum = {} lstats = stats(name, hash_type=source_sum.get('hash_type', None), follow_symlinks=False) if not lstats: changes['newfile'] = name diff --git a/salt/states/archive.py b/salt/states/archive.py index f053d3c207..a992adb8b7 100644 --- a/salt/states/archive.py +++ b/salt/states/archive.py @@ -61,16 +61,30 @@ def _gen_checksum(path): 'hash_type': __opts__['hash_type']} -def _update_checksum(cached_source): - cached_source_sum = '.'.join((cached_source, 'hash')) - source_sum = _gen_checksum(cached_source) +def _checksum_file_path(path): + relpath = '.'.join((os.path.relpath(path, __opts__['cachedir']), 'hash')) + if re.match(r'..[/\\]', relpath): + # path is a local file + relpath = salt.utils.path_join( + 'local', + os.path.splitdrive(path)[-1].lstrip('/\\'), + ) + return salt.utils.path_join(__opts__['cachedir'], 'archive_hash', relpath) + + +def _update_checksum(path): + checksum_file = _checksum_file_path(path) + checksum_dir = os.path.dirname(checksum_file) + if not os.path.isdir(checksum_dir): + os.makedirs(checksum_dir) + source_sum = _gen_checksum(path) hash_type = source_sum.get('hash_type') hsum = source_sum.get('hsum') if hash_type and hsum: lines = [] try: try: - with salt.utils.fopen(cached_source_sum, 'r') as fp_: + with salt.utils.fopen(checksum_file, 'r') as fp_: for line in fp_: try: lines.append(line.rstrip('\n').split(':', 1)) @@ -80,7 +94,7 @@ def _update_checksum(cached_source): if exc.errno != errno.ENOENT: raise - with salt.utils.fopen(cached_source_sum, 'w') as fp_: + with salt.utils.fopen(checksum_file, 'w') as fp_: for line in lines: if line[0] == hash_type: line[1] = hsum @@ -90,16 +104,16 @@ def _update_checksum(cached_source): except (IOError, OSError) as exc: log.warning( 'Failed to update checksum for %s: %s', - cached_source, exc.__str__() + path, exc.__str__(), exc_info=True ) -def _read_cached_checksum(cached_source, form=None): +def _read_cached_checksum(path, form=None): if form is None: form = __opts__['hash_type'] - path = '.'.join((cached_source, 'hash')) + checksum_file = _checksum_file_path(path) try: - with salt.utils.fopen(path, 'r') as fp_: + with salt.utils.fopen(checksum_file, 'r') as fp_: for line in fp_: # Should only be one line in this file but just in case it # isn't, read only a single line to avoid overuse of memory. @@ -114,9 +128,9 @@ def _read_cached_checksum(cached_source, form=None): return {'hash_type': hash_type, 'hsum': hsum} -def _compare_checksum(cached_source, source_sum): +def _compare_checksum(cached, source_sum): cached_sum = _read_cached_checksum( - cached_source, + cached, form=source_sum.get('hash_type', __opts__['hash_type']) ) return source_sum == cached_sum @@ -152,7 +166,6 @@ def extracted(name, user=None, group=None, if_missing=None, - keep=False, trim_output=False, use_cmd_unzip=None, extract_perms=True, @@ -389,6 +402,22 @@ def extracted(name, .. versionadded:: 2016.3.4 + keep_source : True + For ``source`` archives not local to the minion (i.e. from the Salt + fileserver or a remote source such as ``http(s)`` or ``ftp``), Salt + will need to download the archive to the minion cache before they can + be extracted. To remove the downloaded archive after extraction, set + this argument to ``False``. + + .. versionadded:: 2017.7.3 + + keep : True + Same as ``keep_source``, kept for backward-compatibility. + + .. note:: + If both ``keep_source`` and ``keep`` are used, ``keep`` will be + ignored. + password **For ZIP archives only.** Password used for extraction. @@ -527,13 +556,6 @@ def extracted(name, simply checked for existence and extraction will be skipped if if is present. - keep : False - For ``source`` archives not local to the minion (i.e. from the Salt - fileserver or a remote source such as ``http(s)`` or ``ftp``), Salt - will need to download the archive to the minion cache before they can - be extracted. After extraction, these source archives will be removed - unless this argument is set to ``True``. - trim_output : False Useful for archives with many files in them. This can either be set to ``True`` (in which case only the first 100 files extracted will be @@ -635,6 +657,21 @@ def extracted(name, # Remove pub kwargs as they're irrelevant here. kwargs = salt.utils.clean_kwargs(**kwargs) + if 'keep_source' in kwargs and 'keep' in kwargs: + ret.setdefault('warnings', []).append( + 'Both \'keep_source\' and \'keep\' were used. Since these both ' + 'do the same thing, \'keep\' was ignored.' + ) + keep_source = bool(kwargs.pop('keep_source')) + kwargs.pop('keep') + elif 'keep_source' in kwargs: + keep_source = bool(kwargs.pop('keep_source')) + elif 'keep' in kwargs: + keep_source = bool(kwargs.pop('keep')) + else: + # Neither was passed, default is True + keep_source = True + if not _path_is_abs(name): ret['comment'] = '{0} is not an absolute path'.format(name) return ret @@ -730,10 +767,10 @@ def extracted(name, urlparsed_source = _urlparse(source_match) source_hash_basename = urlparsed_source.path or urlparsed_source.netloc - source_is_local = urlparsed_source.scheme in ('', 'file') + source_is_local = urlparsed_source.scheme in salt.utils.files.LOCAL_PROTOS if source_is_local: # Get rid of "file://" from start of source_match - source_match = urlparsed_source.path + source_match = os.path.realpath(os.path.expanduser(urlparsed_source.path)) if not os.path.isfile(source_match): ret['comment'] = 'Source file \'{0}\' does not exist'.format(source_match) return ret @@ -882,95 +919,59 @@ def extracted(name, source_sum = {} if source_is_local: - cached_source = source_match + cached = source_match else: - cached_source = os.path.join( - __opts__['cachedir'], - 'files', - __env__, - re.sub(r'[:/\\]', '_', source_hash_basename), - ) - - if os.path.isdir(cached_source): - # Prevent a traceback from attempting to read from a directory path - salt.utils.rm_rf(cached_source) - - existing_cached_source_sum = _read_cached_checksum(cached_source) - - if source_is_local: - # No need to download archive, it's local to the minion - update_source = False - else: - if not os.path.isfile(cached_source): - # Archive not cached, we need to download it - update_source = True - else: - # Archive is cached, keep=True likely used in prior run. If we need - # to verify the hash, then we *have* to update the source archive - # to know whether or not the hash changed. Hence the below - # statement. bool(source_hash) will be True if source_hash was - # passed, and otherwise False. - update_source = bool(source_hash) - - if update_source: if __opts__['test']: ret['result'] = None ret['comment'] = ( - 'Archive {0} would be downloaded to cache and checked to ' - 'discover if extraction is necessary'.format( + 'Archive {0} would be ached (if necessary) and checked to ' + 'discover if extraction is needed'.format( salt.utils.url.redact_http_basic_auth(source_match) ) ) return ret - # NOTE: This will result in more than one copy of the source archive on - # the minion. The reason this is necessary is because if we are - # tracking the checksum using source_hash_update, we need a location - # where we can place the checksum file alongside the cached source - # file, where it won't be overwritten by caching a file with the same - # name in the same parent dir as the source file. Long term, we should - # come up with a better solution for this. - file_result = __states__['file.managed'](cached_source, - source=source_match, - source_hash=source_hash, - source_hash_name=source_hash_name, - makedirs=True, - skip_verify=skip_verify) - log.debug('file.managed: {0}'.format(file_result)) - - # Prevent a traceback if errors prevented the above state from getting - # off the ground. - if isinstance(file_result, list): - try: - ret['comment'] = '\n'.join(file_result) - except TypeError: - ret['comment'] = '\n'.join([str(x) for x in file_result]) + if 'file.cached' not in __states__: + # Shouldn't happen unless there is a traceback keeping + # salt/states/file.py from being processed through the loader. If + # that is the case, we have much more important problems as _all_ + # file states would be unavailable. + ret['comment'] = ( + 'Unable to cache {0}, file.cached state not available'.format( + source_match + ) + ) return ret try: - if not file_result['result']: - log.debug( - 'failed to download %s', - salt.utils.url.redact_http_basic_auth(source_match) - ) - return file_result - except TypeError: - if not file_result: - log.debug( - 'failed to download %s', - salt.utils.url.redact_http_basic_auth(source_match) - ) - return file_result + result = __states__['file.cached'](source_match, + source_hash=source_hash, + source_hash_name=source_hash_name, + skip_verify=skip_verify, + saltenv=__env__) + except Exception as exc: + msg = 'Failed to cache {0}: {1}'.format(source_match, exc.__str__()) + log.exception(msg) + ret['comment'] = msg + return ret + else: + log.debug('file.cached: {0}'.format(result)) - else: - log.debug( - 'Archive %s is already in cache', - salt.utils.url.redact_http_basic_auth(source_match) - ) + if result['result']: + # Get the path of the file in the minion cache + cached = __salt__['cp.is_cached'](source_match) + else: + log.debug( + 'failed to download %s', + salt.utils.url.redact_http_basic_auth(source_match) + ) + return result + + existing_cached_source_sum = _read_cached_checksum(cached) if source_hash and source_hash_update and not skip_verify: # Create local hash sum file if we're going to track sum update - _update_checksum(cached_source) + _update_checksum(cached) if archive_format == 'zip' and not password: log.debug('Checking %s to see if it is password-protected', @@ -979,7 +980,7 @@ def extracted(name, # implicitly enabled by setting the "options" argument. try: encrypted_zip = __salt__['archive.is_encrypted']( - cached_source, + cached, clean=False, saltenv=__env__) except CommandExecutionError: @@ -997,7 +998,7 @@ def extracted(name, return ret try: - contents = __salt__['archive.list'](cached_source, + contents = __salt__['archive.list'](cached, archive_format=archive_format, options=list_options, strip_components=strip_components, @@ -1166,7 +1167,7 @@ def extracted(name, if not extraction_needed \ and source_hash_update \ and existing_cached_source_sum is not None \ - and not _compare_checksum(cached_source, existing_cached_source_sum): + and not _compare_checksum(cached, existing_cached_source_sum): extraction_needed = True source_hash_trigger = True else: @@ -1224,13 +1225,13 @@ def extracted(name, __states__['file.directory'](name, user=user, makedirs=True) created_destdir = True - log.debug('Extracting {0} to {1}'.format(cached_source, name)) + log.debug('Extracting {0} to {1}'.format(cached, name)) try: if archive_format == 'zip': if use_cmd_unzip: try: files = __salt__['archive.cmd_unzip']( - cached_source, + cached, name, options=options, trim_output=trim_output, @@ -1240,7 +1241,7 @@ def extracted(name, ret['comment'] = exc.strerror return ret else: - files = __salt__['archive.unzip'](cached_source, + files = __salt__['archive.unzip'](cached, name, options=options, trim_output=trim_output, @@ -1248,7 +1249,7 @@ def extracted(name, **kwargs) elif archive_format == 'rar': try: - files = __salt__['archive.unrar'](cached_source, + files = __salt__['archive.unrar'](cached, name, trim_output=trim_output, **kwargs) @@ -1258,7 +1259,7 @@ def extracted(name, else: if options is None: try: - with closing(tarfile.open(cached_source, 'r')) as tar: + with closing(tarfile.open(cached, 'r')) as tar: tar.extractall(name) files = tar.getnames() if trim_output: @@ -1266,7 +1267,7 @@ def extracted(name, except tarfile.ReadError: if salt.utils.which('xz'): if __salt__['cmd.retcode']( - ['xz', '-t', cached_source], + ['xz', '-t', cached], python_shell=False, ignore_retcode=True) == 0: # XZ-compressed data @@ -1282,7 +1283,7 @@ def extracted(name, # pipe it to tar for extraction. cmd = 'xz --decompress --stdout {0} | tar xvf -' results = __salt__['cmd.run_all']( - cmd.format(_cmd_quote(cached_source)), + cmd.format(_cmd_quote(cached)), cwd=name, python_shell=True) if results['retcode'] != 0: @@ -1352,7 +1353,7 @@ def extracted(name, tar_cmd.append(tar_shortopts) tar_cmd.extend(tar_longopts) - tar_cmd.extend(['-f', cached_source]) + tar_cmd.extend(['-f', cached]) results = __salt__['cmd.run_all'](tar_cmd, cwd=name, @@ -1523,18 +1524,15 @@ def extracted(name, for item in enforce_failed: ret['comment'] += '\n- {0}'.format(item) - if not source_is_local and not keep: - for path in (cached_source, __salt__['cp.is_cached'](source_match)): - if not path: - continue - log.debug('Cleaning cached source file %s', path) - try: - os.remove(path) - except OSError as exc: - if exc.errno != errno.ENOENT: - log.error( - 'Failed to clean cached source file %s: %s', - cached_source, exc.__str__() - ) + if not source_is_local: + if keep_source: + log.debug('Keeping cached source file %s', cached) + else: + log.debug('Cleaning cached source file %s', cached) + result = __states__['file.not_cached'](source_match, saltenv=__env__) + if not result['result']: + # Don't let failure to delete cached file cause the state + # itself to fail, just drop it in the warnings. + ret.setdefault('warnings', []).append(result['comment']) return ret diff --git a/salt/states/file.py b/salt/states/file.py index 8d81998016..3a2de6047c 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -294,6 +294,7 @@ if salt.utils.is_windows(): # Import 3rd-party libs import salt.ext.six as six from salt.ext.six.moves import zip_longest +from salt.ext.six.moves.urllib.parse import urlparse as _urlparse # pylint: disable=no-name-in-module if salt.utils.is_windows(): import pywintypes import win32com.client @@ -1519,6 +1520,7 @@ def managed(name, source=None, source_hash='', source_hash_name=None, + keep_source=True, user=None, group=None, mode=None, @@ -1717,6 +1719,15 @@ def managed(name, .. versionadded:: 2016.3.5 + keep_source : True + Set to ``False`` to discard the cached copy of the source file once the + state completes. This can be useful for larger files to keep them from + taking up space in minion cache. However, keep in mind that discarding + the source file will result in the state needing to re-download the + source file if the state is run again. + + .. versionadded:: 2017.7.3 + user The user to own the file, this defaults to the user salt is running as on the minion @@ -2415,8 +2426,9 @@ def managed(name, except Exception as exc: ret['changes'] = {} log.debug(traceback.format_exc()) - if os.path.isfile(tmp_filename): - os.remove(tmp_filename) + salt.utils.files.remove(tmp_filename) + if not keep_source and sfn: + salt.utils.files.remove(sfn) return _error(ret, 'Unable to check_cmd file: {0}'.format(exc)) # file being updated to verify using check_cmd @@ -2434,15 +2446,9 @@ def managed(name, cret = mod_run_check_cmd(check_cmd, tmp_filename, **check_cmd_opts) if isinstance(cret, dict): ret.update(cret) - if os.path.isfile(tmp_filename): - os.remove(tmp_filename) - if sfn and os.path.isfile(sfn): - os.remove(sfn) + salt.utils.files.remove(tmp_filename) return ret - if sfn and os.path.isfile(sfn): - os.remove(sfn) - # Since we generated a new tempfile and we are not returning here # lets change the original sfn to the new tempfile or else we will # get file not found @@ -2490,10 +2496,10 @@ def managed(name, log.debug(traceback.format_exc()) return _error(ret, 'Unable to manage file: {0}'.format(exc)) finally: - if tmp_filename and os.path.isfile(tmp_filename): - os.remove(tmp_filename) - if sfn and os.path.isfile(sfn): - os.remove(sfn) + if tmp_filename: + salt.utils.files.remove(tmp_filename) + if not keep_source and sfn: + salt.utils.files.remove(sfn) _RECURSE_TYPES = ['user', 'group', 'mode', 'ignore_files', 'ignore_dirs'] @@ -3022,6 +3028,7 @@ def directory(name, def recurse(name, source, + keep_source=True, clean=False, require=None, user=None, @@ -3053,6 +3060,15 @@ def recurse(name, located on the master in the directory named spam, and is called eggs, the source string is salt://spam/eggs + keep_source : True + Set to ``False`` to discard the cached copy of the source file once the + state completes. This can be useful for larger files to keep them from + taking up space in minion cache. However, keep in mind that discarding + the source file will result in the state needing to re-download the + source file if the state is run again. + + .. versionadded:: 2017.7.3 + clean Make sure that only files that are set up by salt and required by this function are kept. If this option is set then everything in this @@ -3333,6 +3349,7 @@ def recurse(name, _ret = managed( path, source=source, + keep_source=keep_source, user=user, group=group, mode='keep' if keep_mode else file_mode, @@ -6423,3 +6440,376 @@ def shortcut( ret['comment'] += (', but was unable to set ownership to ' '{0}'.format(user)) return ret + + +def cached(name, + source_hash='', + source_hash_name=None, + skip_verify=False, + saltenv='base'): + ''' + .. versionadded:: 2017.7.3 + + Ensures that a file is saved to the minion's cache. This state is primarily + invoked by other states to ensure that we do not re-download a source file + if we do not need to. + + name + The URL of the file to be cached. To cache a file from an environment + other than ``base``, either use the ``saltenv`` argument or include the + saltenv in the URL (e.g. ``salt://path/to/file.conf?saltenv=dev``). + + .. note:: + A list of URLs is not supported, this must be a single URL. If a + local file is passed here, then the state will obviously not try to + download anything, but it will compare a hash if one is specified. + + source_hash + See the documentation for this same argument in the + :py:func:`file.managed ` state. + + .. note:: + For remote files not originating from the ``salt://`` fileserver, + such as http(s) or ftp servers, this state will not re-download the + file if the locally-cached copy matches this hash. This is done to + prevent unnecessary downloading on repeated runs of this state. To + update the cached copy of a file, it is necessary to update this + hash. + + source_hash_name + See the documentation for this same argument in the + :py:func:`file.managed ` state. + + skip_verify + See the documentation for this same argument in the + :py:func:`file.managed ` state. + + .. note:: + Setting this to ``True`` will result in a copy of the file being + downloaded from a remote (http(s), ftp, etc.) source each time the + state is run. + + saltenv + Used to specify the environment from which to download a file from the + Salt fileserver (i.e. those with ``salt://`` URL). + + + This state will in most cases not be useful in SLS files, but it is useful + when writing a state or remote-execution module that needs to make sure + that a file at a given URL has been downloaded to the cachedir. One example + of this is in the :py:func:`archive.extracted ` + state: + + .. code-block:: python + + result = __states__['file.cached'](source_match, + source_hash=source_hash, + source_hash_name=source_hash_name, + skip_verify=skip_verify, + saltenv=__env__) + + This will return a dictionary containing the state's return data, including + a ``result`` key which will state whether or not the state was successful. + Note that this will not catch exceptions, so it is best used within a + try/except. + + Once this state has been run from within another state or remote-execution + module, the actual location of the cached file can be obtained using + :py:func:`cp.is_cached `: + + .. code-block:: python + + cached = __salt__['cp.is_cached'](source_match) + + This function will return the cached path of the file, or an empty string + if the file is not present in the minion cache. + + This state will in most cases not be useful in SLS files, but it is useful + when writing a state or remote-execution module that needs to make sure + that a file at a given URL has been downloaded to the cachedir. One example + of this is in the :py:func:`archive.extracted ` + state: + + .. code-block:: python + + result = __states__['file.cached'](source_match, + source_hash=source_hash, + source_hash_name=source_hash_name, + skip_verify=skip_verify, + saltenv=__env__) + + This will return a dictionary containing the state's return data, including + a ``result`` key which will state whether or not the state was successful. + Note that this will not catch exceptions, so it is best used within a + try/except. + + Once this state has been run from within another state or remote-execution + module, the actual location of the cached file can be obtained using + :py:func:`cp.is_cached `: + + .. code-block:: python + + cached = __salt__['cp.is_cached'](source_match) + + This function will return the cached path of the file, or an empty string + if the file is not present in the minion cache. + ''' + ret = {'changes': {}, + 'comment': '', + 'name': name, + 'result': False} + + try: + parsed = _urlparse(name) + except Exception: + ret['comment'] = 'Only URLs or local file paths are valid input' + return ret + + # This if statement will keep the state from proceeding if a remote source + # is specified and no source_hash is presented (unless we're skipping hash + # verification). + if not skip_verify \ + and not source_hash \ + and parsed.scheme in salt.utils.files.REMOTE_PROTOS: + ret['comment'] = ( + 'Unable to verify upstream hash of source file {0}, please set ' + 'source_hash or set skip_verify to True'.format(name) + ) + return ret + + if source_hash: + # Get the hash and hash type from the input. This takes care of parsing + # the hash out of a file containing checksums, if that is how the + # source_hash was specified. + try: + source_sum = __salt__['file.get_source_sum']( + source=name, + source_hash=source_hash, + source_hash_name=source_hash_name, + saltenv=saltenv) + except CommandExecutionError as exc: + ret['comment'] = exc.strerror + return ret + else: + if not source_sum: + # We shouldn't get here, problems in retrieving the hash in + # file.get_source_sum should result in a CommandExecutionError + # being raised, which we catch above. Nevertheless, we should + # provide useful information in the event that + # file.get_source_sum regresses. + ret['comment'] = ( + 'Failed to get source hash from {0}. This may be a bug. ' + 'If this error persists, please report it and set ' + 'skip_verify to True to work around it.'.format(source_hash) + ) + return ret + else: + source_sum = {} + + if parsed.scheme in salt.utils.files.LOCAL_PROTOS: + # Source is a local file path + full_path = os.path.realpath(os.path.expanduser(parsed.path)) + if os.path.exists(full_path): + if not skip_verify and source_sum: + # Enforce the hash + local_hash = __salt__['file.get_hash']( + full_path, + source_sum.get('hash_type', __opts__['hash_type'])) + if local_hash == source_sum['hsum']: + ret['result'] = True + ret['comment'] = ( + 'File {0} is present on the minion and has hash ' + '{1}'.format(full_path, local_hash) + ) + else: + ret['comment'] = ( + 'File {0} is present on the minion, but the hash ({1}) ' + 'does not match the specified hash ({2})'.format( + full_path, local_hash, source_sum['hsum'] + ) + ) + return ret + else: + ret['result'] = True + ret['comment'] = 'File {0} is present on the minion'.format( + full_path + ) + return ret + else: + ret['comment'] = 'File {0} is not present on the minion'.format( + full_path + ) + return ret + + local_copy = __salt__['cp.is_cached'](name, saltenv=saltenv) + + if local_copy: + # File is already cached + pre_hash = __salt__['file.get_hash']( + local_copy, + source_sum.get('hash_type', __opts__['hash_type'])) + + if not skip_verify and source_sum: + # Get the local copy's hash to compare with the hash that was + # specified via source_hash. If it matches, we can exit early from + # the state without going any further, because the file is cached + # with the correct hash. + if pre_hash == source_sum['hsum']: + ret['result'] = True + ret['comment'] = ( + 'File is already cached to {0} with hash {1}'.format( + local_copy, pre_hash + ) + ) + else: + pre_hash = None + + def _try_cache(path, checksum): + ''' + This helper is not needed anymore in develop as the fileclient in the + develop branch now has means of skipping a download if the existing + hash matches one passed to cp.cache_file. Remove this helper and the + code that invokes it, once we have merged forward into develop. + ''' + if not path or not checksum: + return True + form = salt.utils.files.HASHES_REVMAP.get(len(checksum)) + if form is None: + # Shouldn't happen, an invalid checksum length should be caught + # before we get here. But in the event this gets through, don't let + # it cause any trouble, and just return True. + return True + try: + return salt.utils.get_hash(path, form=form) != checksum + except (IOError, OSError, ValueError): + # Again, shouldn't happen, but don't let invalid input/permissions + # in the call to get_hash blow this up. + return True + + # Cache the file. Note that this will not actually download the file if + # either of the following is true: + # 1. source is a salt:// URL and the fileserver determines that the hash + # of the minion's copy matches that of the fileserver. + # 2. File is remote (http(s), ftp, etc.) and the specified source_hash + # matches the cached copy. + # Remote, non salt:// sources _will_ download if a copy of the file was + # not already present in the minion cache. + if _try_cache(local_copy, source_sum.get('hsum')): + # The _try_cache helper is obsolete in the develop branch. Once merged + # forward, remove the helper as well as this if statement, and dedent + # the below block. + try: + local_copy = __salt__['cp.cache_file']( + name, + saltenv=saltenv) + # Once this is merged into develop, uncomment the source_hash + # line below and add it to the list of arguments to + # cp.cache_file (note that this also means removing the + # close-parenthesis above and replacing it with a comma). The + # develop branch has modifications to the fileclient which will + # allow it to skip the download if the source_hash matches what + # is passed to cp.cache_file, so the helper is just a stopgap + # for the 2017.7 release cycle. + #source_hash=source_sum.get('hsum')) + except Exception as exc: + ret['comment'] = exc.__str__() + return ret + + if not local_copy: + ret['comment'] = ( + 'Failed to cache {0}, check minion log for more ' + 'information'.format(name) + ) + return ret + + post_hash = __salt__['file.get_hash']( + local_copy, + source_sum.get('hash_type', __opts__['hash_type'])) + + if pre_hash != post_hash: + ret['changes']['hash'] = {'old': pre_hash, 'new': post_hash} + + # Check the hash, if we're enforcing one. Note that this will be the first + # hash check if the file was not previously cached, and the 2nd hash check + # if it was cached and the + if not skip_verify and source_sum: + if post_hash == source_sum['hsum']: + ret['result'] = True + ret['comment'] = ( + 'File is already cached to {0} with hash {1}'.format( + local_copy, post_hash + ) + ) + else: + ret['comment'] = ( + 'File is cached to {0}, but the hash ({1}) does not match ' + 'the specified hash ({2})'.format( + local_copy, post_hash, source_sum['hsum'] + ) + ) + return ret + + # We're not enforcing a hash, and we already know that the file was + # successfully cached, so we know the state was successful. + ret['result'] = True + ret['comment'] = 'File is cached to {0}'.format(local_copy) + return ret + + +def not_cached(name, saltenv='base'): + ''' + Ensures that a file is saved to the minion's cache. This state is primarily + invoked by other states to ensure that we do not re-download a source file + if we do not need to. + + name + The URL of the file to be cached. To cache a file from an environment + other than ``base``, either use the ``saltenv`` argument or include the + saltenv in the URL (e.g. ``salt://path/to/file.conf?saltenv=dev``). + + .. note:: + A list of URLs is not supported, this must be a single URL. If a + local file is passed here, the state will take no action. + + saltenv + Used to specify the environment from which to download a file from the + Salt fileserver (i.e. those with ``salt://`` URL). + ''' + ret = {'changes': {}, + 'comment': '', + 'name': name, + 'result': False} + + try: + parsed = _urlparse(name) + except Exception: + ret['comment'] = 'Only URLs or local file paths are valid input' + return ret + else: + if parsed.scheme in salt.utils.files.LOCAL_PROTOS: + full_path = os.path.realpath(os.path.expanduser(parsed.path)) + ret['result'] = True + ret['comment'] = ( + 'File {0} is a local path, no action taken'.format( + full_path + ) + ) + return ret + + local_copy = __salt__['cp.is_cached'](name, saltenv=saltenv) + + if local_copy: + try: + os.remove(local_copy) + except Exception as exc: + ret['comment'] = 'Failed to delete {0}: {1}'.format( + local_copy, exc.__str__() + ) + else: + ret['result'] = True + ret['changes']['deleted'] = True + ret['comment'] = '{0} was deleted'.format(local_copy) + else: + ret['result'] = True + ret['comment'] = '{0} is not cached'.format(name) + return ret diff --git a/salt/utils/files.py b/salt/utils/files.py index 8d463756d9..605e9710d8 100644 --- a/salt/utils/files.py +++ b/salt/utils/files.py @@ -23,10 +23,21 @@ from salt.ext import six log = logging.getLogger(__name__) +LOCAL_PROTOS = ('', 'file') REMOTE_PROTOS = ('http', 'https', 'ftp', 'swift', 's3') VALID_PROTOS = ('salt', 'file') + REMOTE_PROTOS TEMPFILE_PREFIX = '__salt.tmp.' +HASHES = { + 'sha512': 128, + 'sha384': 96, + 'sha256': 64, + 'sha224': 56, + 'sha1': 40, + 'md5': 32, +} +HASHES_REVMAP = dict([(y, x) for x, y in six.iteritems(HASHES)]) + def guess_archive_type(name): ''' @@ -296,3 +307,14 @@ def safe_filepath(file_path_name): return os.sep.join([drive, path]) else: return path + + +def remove(path): + ''' + Runs os.remove(path) and suppresses the OSError if the file doesn't exist + ''' + try: + os.remove(path) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise From b1e64b11fbcaf9dedb6d4cb6ee7a06801cbba877 Mon Sep 17 00:00:00 2001 From: Michal Kurtak Date: Thu, 21 Sep 2017 21:51:22 +0200 Subject: [PATCH 137/633] yumpkg.py: install calls list_repo_pkgs only if wildcard in pkg name is used Fixes #43396 --- salt/modules/yumpkg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index 8a15867a8c..51e855e636 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -1262,6 +1262,7 @@ def install(name=None, to_install = [] to_downgrade = [] to_reinstall = [] + _available = {} # The above three lists will be populated with tuples containing the # package name and the string being used for this particular package # modification. The reason for this method is that the string we use for @@ -1281,7 +1282,8 @@ def install(name=None, if pkg_type == 'repository': has_wildcards = [x for x, y in six.iteritems(pkg_params) if y is not None and '*' in y] - _available = list_repo_pkgs(*has_wildcards, byrepo=False, **kwargs) + if has_wildcards: + _available = list_repo_pkgs(*has_wildcards, byrepo=False, **kwargs) pkg_params_items = six.iteritems(pkg_params) elif pkg_type == 'advisory': pkg_params_items = [] From 7f9a7e2857cc60e0828167a966efc8d73edd019a Mon Sep 17 00:00:00 2001 From: Tom Williams Date: Thu, 21 Sep 2017 20:31:18 -0400 Subject: [PATCH 138/633] INFRA-5292 - add support to fluent logger for Graylog and GELF output formats --- salt/log/handlers/fluent_mod.py | 196 ++++++++++++++++++++++++++------ 1 file changed, 159 insertions(+), 37 deletions(-) diff --git a/salt/log/handlers/fluent_mod.py b/salt/log/handlers/fluent_mod.py index d049920d47..a923dd36d7 100644 --- a/salt/log/handlers/fluent_mod.py +++ b/salt/log/handlers/fluent_mod.py @@ -11,7 +11,18 @@ Fluent Logging Handler ------------------- - In the salt configuration file: + In the `fluent` configuration file: + + .. code-block:: text + + + type forward + bind localhost + port 24224 + + + Then, to send logs via fluent in Logstash format, add the + following to the salt (master and/or minion) configuration file: .. code-block:: yaml @@ -19,14 +30,32 @@ host: localhost port: 24224 - In the `fluent`_ configuration file: + To send logs via fluent in the Graylog raw json format, add the + following to the salt (master and/or minion) configuration file: - .. code-block:: text + .. code-block:: yaml - - type forward - port 24224 - + fluent_handler: + host: localhost + port: 24224 + payload_type: graylog + tags: + - salt_master.SALT + + The above also illustrates the `tags` option, which allows + one to set descriptive (or useful) tags on records being + sent. If not provided, this defaults to the single tag: + 'salt'. Also note that, via Graylog "magic", the 'facility' + of the logged message is set to 'SALT' (the portion of the + tag after the first period), while the tag itself will be + set to simply 'salt_master'. This is a feature, not a bug :) + + Note: + There is a third emitter, for the GELF format, but it is + largely untested, and I don't currently have a setup supporting + this config, so while it runs cleanly and outputs what LOOKS to + be valid GELF, any real-world feedback on its usefulness, and + correctness, will be appreciated. Log Level ......... @@ -53,7 +82,7 @@ import time import datetime import socket import threading - +import types # Import salt libs from salt.log.setup import LOG_LEVELS @@ -91,6 +120,18 @@ __virtualname__ = 'fluent' _global_sender = None +# Python logger's idea of "level" is wildly at variance with +# Graylog's (and, incidentally, the rest of the civilized world). +syslog_levels = { + 'EMERG': 0, + 'ALERT': 2, + 'CRIT': 2, + 'ERR': 3, + 'WARNING': 4, + 'NOTICE': 5, + 'INFO': 6, + 'DEBUG': 7 +} def setup(tag, **kwargs): host = kwargs.get('host', 'localhost') @@ -116,55 +157,133 @@ def __virtual__(): def setup_handlers(): - host = port = address = None + host = port = None if 'fluent_handler' in __opts__: host = __opts__['fluent_handler'].get('host', None) port = __opts__['fluent_handler'].get('port', None) - version = __opts__['fluent_handler'].get('version', 1) + payload_type = __opts__['fluent_handler'].get('payload_type', None) + # in general, you want the value of tag to ALSO be a member of tags + tags = __opts__['fluent_handler'].get('tags', ['salt']) + tag = tags[0] if len(tags) else 'salt' + if payload_type == 'graylog': + version = 0 + elif payload_type == 'gelf': + # We only support version 1.1 (the latest) of GELF... + version = 1.1 + else: + # Default to logstash for backwards compat + payload_type = 'logstash' + version = __opts__['fluent_handler'].get('version', 1) if host is None and port is None: log.debug( 'The required \'fluent_handler\' configuration keys, ' '\'host\' and/or \'port\', are not properly configured. Not ' - 'configuring the fluent logging handler.' + 'enabling the fluent logging handler.' ) else: - logstash_formatter = LogstashFormatter(version=version) - fluent_handler = FluentHandler('salt', host=host, port=port) - fluent_handler.setFormatter(logstash_formatter) + formatter = MessageFormatter(payload_type=payload_type, version=version, tags=tags) + fluent_handler = FluentHandler(tag, host=host, port=port) + fluent_handler.setFormatter(formatter) fluent_handler.setLevel( - LOG_LEVELS[ - __opts__['fluent_handler'].get( - 'log_level', - # Not set? Get the main salt log_level setting on the - # configuration file - __opts__.get( - 'log_level', - # Also not set?! Default to 'error' - 'error' - ) - ) - ] + LOG_LEVELS[__opts__['fluent_handler'].get('log_level', __opts__.get('log_level', 'error'))] ) yield fluent_handler - if host is None and port is None and address is None: + if host is None and port is None: yield False -class LogstashFormatter(logging.Formatter, NewStyleClassMixIn): - def __init__(self, msg_type='logstash', msg_path='logstash', version=1): - self.msg_path = msg_path - self.msg_type = msg_type +class MessageFormatter(logging.Formatter, NewStyleClassMixIn): + def __init__(self, payload_type, version, tags, msg_type=None, msg_path=None): + self.payload_type = payload_type self.version = version - self.format = getattr(self, 'format_v{0}'.format(version)) - super(LogstashFormatter, self).__init__(fmt=None, datefmt=None) + self.tag = tags[0] if len(tags) else 'salt' # 'salt' for backwards compat + self.tags = tags + self.msg_path = msg_path if msg_path else payload_type + self.msg_type = msg_type if msg_type else payload_type + format_func = 'format_{0}_v{1}'.format(payload_type, version).replace('.', '_') + self.format = getattr(self, format_func) + super(MessageFormatter, self).__init__(fmt=None, datefmt=None) def formatTime(self, record, datefmt=None): + if self.payload_type == 'gelf': # GELF uses epoch times + return record.created return datetime.datetime.utcfromtimestamp(record.created).isoformat()[:-3] + 'Z' - def format_v0(self, record): + def format_graylog_v0(self, record): + ''' + Graylog 'raw' format is essentially the raw record, minimally munged to provide + the bare minimum that td-agent requires to accept and route the event. This is + well suited to a config where the client td-agents log directly to Graylog. + ''' + message_dict = { + 'message': record.getMessage(), + 'timestamp': self.formatTime(record), + # Graylog uses syslog levels, not whatever it is Python does... + 'level': syslog_levels.get(record.levelname, 'ALERT'), + 'tag': self.tag + } + + if record.exc_info: + exc_info = self.formatException(record.exc_info) + message_dict.update({'full_message': exc_info}) + + # Add any extra attributes to the message field + for key, value in six.iteritems(record.__dict__): + if key in ('args', 'asctime', 'bracketlevel', 'bracketname', 'bracketprocess', + 'created', 'exc_info', 'exc_text', 'id', 'levelname', 'levelno', 'msecs', + 'msecs', 'message', 'msg', 'relativeCreated', 'version'): + # These are already handled above or explicitly pruned. + continue + + if isinstance(value, (six.string_types, bool, dict, float, int, list, types.NoneType)): + val = value + else: + val = repr(value) + message_dict.update({'{0}'.format(key): val}) + return message_dict + + def format_gelf_v1_1(self, record): + ''' + If your agent is (or can be) configured to forward pre-formed GELF to Graylog + with ZERO fluent processing, this function is for YOU, pal... + ''' + message_dict = { + 'version': self.version, + 'host': salt.utils.network.get_fqhostname(), + 'short_message': record.getMessage(), + 'timestamp': self.formatTime(record), + 'level': syslog_levels.get(record.levelname, 'ALERT'), + "_tag": self.tag + } + + if record.exc_info: + exc_info = self.formatException(record.exc_info) + message_dict.update({'full_message': exc_info}) + + # Add any extra attributes to the message field + for key, value in six.iteritems(record.__dict__): + if key in ('args', 'asctime', 'bracketlevel', 'bracketname', 'bracketprocess', + 'created', 'exc_info', 'exc_text', 'id', 'levelname', 'levelno', 'msecs', + 'msecs', 'message', 'msg', 'relativeCreated', 'version'): + # These are already handled above or explicitly avoided. + continue + + if isinstance(value, (six.string_types, bool, dict, float, int, list, types.NoneType)): + val = value + else: + val = repr(value) + # GELF spec require "non-standard" fields to be prefixed with '_' (underscore). + message_dict.update({'_{0}'.format(key): val}) + + return message_dict + + def format_logstash_v0(self, record): + ''' + Messages are formatted in logstash's expected format. + ''' host = salt.utils.network.get_fqhostname() message_dict = { '@timestamp': self.formatTime(record), @@ -186,7 +305,7 @@ class LogstashFormatter(logging.Formatter, NewStyleClassMixIn): ), '@source_host': host, '@source_path': self.msg_path, - '@tags': ['salt'], + '@tags': self.tags, '@type': self.msg_type, } @@ -216,7 +335,10 @@ class LogstashFormatter(logging.Formatter, NewStyleClassMixIn): message_dict['@fields'][key] = repr(value) return message_dict - def format_v1(self, record): + def format_logstash_v1(self, record): + ''' + Messages are formatted in logstash's expected format. + ''' message_dict = { '@version': 1, '@timestamp': self.formatTime(record), @@ -230,7 +352,7 @@ class LogstashFormatter(logging.Formatter, NewStyleClassMixIn): 'funcName': record.funcName, 'processName': record.processName, 'message': record.getMessage(), - 'tags': ['salt'], + 'tags': self.tags, 'type': self.msg_type } From 1c979d58096175aefba7a099d17f963d637fe085 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Fri, 22 Sep 2017 10:30:28 +0300 Subject: [PATCH 139/633] Update redis cache `contains` logic to use more efficient `sismember`. --- salt/cache/redis_cache.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/salt/cache/redis_cache.py b/salt/cache/redis_cache.py index 0f52dfd6ad..35bce55198 100644 --- a/salt/cache/redis_cache.py +++ b/salt/cache/redis_cache.py @@ -441,12 +441,9 @@ def contains(bank, key): redis_server = _get_redis_server() bank_redis_key = _get_bank_redis_key(bank) try: - banks = redis_server.smembers(bank_redis_key) + return redis_server.sismember(bank_redis_key, key) except (RedisConnectionError, RedisResponseError) as rerr: mesg = 'Cannot retrieve the Redis cache key {rkey}: {rerr}'.format(rkey=bank_redis_key, rerr=rerr) log.error(mesg) raise SaltCacheError(mesg) - if not banks: - return False - return key in banks From 9d450f77379d8f21336de414f433ff3ed5830df6 Mon Sep 17 00:00:00 2001 From: Vladimir Nadvornik Date: Fri, 22 Sep 2017 10:24:11 +0200 Subject: [PATCH 140/633] Fix python3 compatibility --- salt/states/mdadm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/states/mdadm.py b/salt/states/mdadm.py index 64cae4a6d6..8981a15dbe 100644 --- a/salt/states/mdadm.py +++ b/salt/states/mdadm.py @@ -112,7 +112,7 @@ def present(name, ret['result'] = False return ret elif len(uuid_dict) == 1: - uuid = uuid_dict.keys()[0] + uuid = list(uuid_dict.keys())[0] if present and present['uuid'] != uuid: ret['comment'] = 'Devices MD_UUIDs: {0} differs from present RAID uuid {1}.'.format(uuid, present['uuid']) ret['result'] = False From f6a8a969a47036ff9f46ff769d1acaf8c7b8b43b Mon Sep 17 00:00:00 2001 From: Tom Williams Date: Fri, 22 Sep 2017 05:19:51 -0400 Subject: [PATCH 141/633] INFRA-5292 - we must please pylint ... --- salt/log/handlers/fluent_mod.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/log/handlers/fluent_mod.py b/salt/log/handlers/fluent_mod.py index a923dd36d7..ccd56b5521 100644 --- a/salt/log/handlers/fluent_mod.py +++ b/salt/log/handlers/fluent_mod.py @@ -133,6 +133,7 @@ syslog_levels = { 'DEBUG': 7 } + def setup(tag, **kwargs): host = kwargs.get('host', 'localhost') port = kwargs.get('port', 24224) @@ -238,7 +239,7 @@ class MessageFormatter(logging.Formatter, NewStyleClassMixIn): # These are already handled above or explicitly pruned. continue - if isinstance(value, (six.string_types, bool, dict, float, int, list, types.NoneType)): + if isinstance(value, (six.string_types, bool, dict, float, int, list, types.NoneType)): # pylint: disable=W1699 val = value else: val = repr(value) @@ -271,7 +272,7 @@ class MessageFormatter(logging.Formatter, NewStyleClassMixIn): # These are already handled above or explicitly avoided. continue - if isinstance(value, (six.string_types, bool, dict, float, int, list, types.NoneType)): + if isinstance(value, (six.string_types, bool, dict, float, int, list, types.NoneType)): # pylint: disable=W1699 val = value else: val = repr(value) From 43ded5413206ea906430f543ac06fb2c65bacc39 Mon Sep 17 00:00:00 2001 From: Joaquin Veira Date: Fri, 22 Sep 2017 11:43:44 +0200 Subject: [PATCH 142/633] Update zabbix_return.py corrected indentation and other suggestions by jenkins --- salt/returners/zabbix_return.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/salt/returners/zabbix_return.py b/salt/returners/zabbix_return.py index a5e79ca8e0..9969e3365c 100644 --- a/salt/returners/zabbix_return.py +++ b/salt/returners/zabbix_return.py @@ -55,22 +55,21 @@ def zbx(): return False -def zabbix_send(key, host, output): - with salt.utils.fopen(zbx()['zabbix_config'],'r') as file_handle: - for line in file_handle: - if "ServerActive" in line: - flag = "true" - server = line.rsplit('=') - server = server[1].rsplit(',') - for s in server: - cmd = zbx()['sender'] + " -z " + s.replace('\n','') + " -s " + host + " -k " + key + " -o \"" + output +"\"" - __salt__['cmd.shell'](cmd) - break - else: - flag = "false" - if flag == 'false': - cmd = zbx()['sender'] + " -c " + zbx()['config'] + " -s " + host + " -k " + key + " -o \"" + output +"\"" - file_handle.close() +def zabbix_send(key, host, output): + with salt.utils.fopen(zbx()['zabbix_config'], 'r') as file_handle: + for line in file_handle: + if "ServerActive" in line: + flag = "true" + server = line.rsplit('=') + server = server[1].rsplit(',') + for s in server: + cmd = zbx()['sender'] + " -z " + s.replace('\n', '') + " -s " + host + " -k " + key + " -o \"" + output +"\"" + __salt__['cmd.shell'](cmd) + break + else: + flag = "false" + if flag == 'false': + cmd = zbx()['sender'] + " -c " + zbx()['config'] + " -s " + host + " -k " + key + " -o \"" + output +"\"" def returner(ret): From cbae45bec43bbe1e1fb9efe2c973402087694df5 Mon Sep 17 00:00:00 2001 From: rallytime Date: Fri, 22 Sep 2017 10:33:10 -0400 Subject: [PATCH 143/633] Lint: Remove extra line at end of file --- tests/unit/utils/test_parsers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/utils/test_parsers.py b/tests/unit/utils/test_parsers.py index 71b8cf62c9..ba4cc402d8 100644 --- a/tests/unit/utils/test_parsers.py +++ b/tests/unit/utils/test_parsers.py @@ -1002,4 +1002,3 @@ class DaemonMixInTestCase(TestCase): # Hide the class from unittest framework when it searches for TestCase classes in the module del LogSettingsParserTests - From da156583048f0e0cab85afa1a9b195910fcf67a2 Mon Sep 17 00:00:00 2001 From: "Z. Liu" Date: Fri, 22 Sep 2017 23:25:21 +0800 Subject: [PATCH 144/633] remove modify yaml constructor which will modify the default behavior of yaml load. Foe example, for following example (t.sls), it will cause the difference between the content of file testa and testb, but it should be identical! $ cat t {%- load_yaml as vars %} toaddr: - test@test.com {%- endload -%} {{ vars.toaddr }} $ cat t.sls /tmp/testa: file.managed: - source: salt://t - user: root - group: root - mode: "0755" - template: jinja sys-power/acpid: pkg.installed: - refresh: False /tmp/testb: file.managed: - source: salt://t - user: root - group: root - mode: "0755" - template: jinja $ touch /tmp/test{a,b} $ salt-call state.sls t local: ---------- ID: /tmp/testa Function: file.managed Result: None Comment: The file /tmp/testa is set to be changed Changes: ---------- diff: --- +++ @@ -0,0 +1 @@ +['test@test.com'] ---------- ID: /tmp/testb Function: file.managed Result: None Comment: The file /tmp/testb is set to be changed Changes: ---------- diff: --- +++ @@ -0,0 +1 @@ +[u'test@test.com'] --- salt/modules/heat.py | 2 -- salt/states/heat.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/salt/modules/heat.py b/salt/modules/heat.py index 1f94f2e605..e2b3f97ded 100644 --- a/salt/modules/heat.py +++ b/salt/modules/heat.py @@ -102,8 +102,6 @@ def _construct_yaml_str(self, node): Construct for yaml ''' return self.construct_scalar(node) -YamlLoader.add_constructor(u'tag:yaml.org,2002:str', - _construct_yaml_str) YamlLoader.add_constructor(u'tag:yaml.org,2002:timestamp', _construct_yaml_str) diff --git a/salt/states/heat.py b/salt/states/heat.py index c5f40f1687..a042751225 100644 --- a/salt/states/heat.py +++ b/salt/states/heat.py @@ -80,8 +80,6 @@ def _construct_yaml_str(self, node): Construct for yaml ''' return self.construct_scalar(node) -YamlLoader.add_constructor(u'tag:yaml.org,2002:str', - _construct_yaml_str) YamlLoader.add_constructor(u'tag:yaml.org,2002:timestamp', _construct_yaml_str) From 9e32ce72cc75edf39b40c0e2c70ec9256d2e003b Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:12:37 -0400 Subject: [PATCH 145/633] Added salt.utils.vmware.get_dvss that retrieves DVSs in a datacenter --- salt/utils/vmware.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index b239b269b0..91f86b4b82 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -981,6 +981,45 @@ def get_network_adapter_type(adapter_type): return vim.vm.device.VirtualE1000e() +def get_dvss(dc_ref, dvs_names=None, get_all_dvss=False): + ''' + Returns distributed virtual switches (DVSs) in a datacenter. + + dc_ref + The parent datacenter reference. + + dvs_names + The names of the DVSs to return. Default is None. + + get_all_dvss + Return all DVSs in the datacenter. Default is False. + ''' + dc_name = get_managed_object_name(dc_ref) + log.trace('Retrieving DVSs in datacenter \'{0}\', dvs_names=\'{1}\', ' + 'get_all_dvss={2}'.format(dc_name, + ','.join(dvs_names) if dvs_names + else None, + get_all_dvss)) + properties = ['name'] + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='networkFolder', + skip=True, + type=vim.Datacenter, + selectSet=[vmodl.query.PropertyCollector.TraversalSpec( + path='childEntity', + skip=False, + type=vim.Folder)]) + service_instance = get_service_instance_from_managed_object(dc_ref) + items = [i['object'] for i in + get_mors_with_properties(service_instance, + vim.DistributedVirtualSwitch, + container_ref=dc_ref, + property_list=properties, + traversal_spec=traversal_spec) + if get_all_dvss or (dvs_names and i['name'] in dvs_names)] + return items + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From 173a697be2c0296424609448907b1ae405ee4999 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:14:55 -0400 Subject: [PATCH 146/633] Added comments and imports for dvs functions in salt.utils.vmware --- tests/unit/utils/vmware/test_dvs.py | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/unit/utils/vmware/test_dvs.py diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py new file mode 100644 index 0000000000..27c7886eb5 --- /dev/null +++ b/tests/unit/utils/vmware/test_dvs.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu ` + + Tests for dvs related functions in salt.utils.vmware +''' + +# Import python libraries +from __future__ import absolute_import +import logging + +# Import Salt testing libraries +from tests.support.unit import TestCase, skipIf +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, call, \ + PropertyMock +from salt.exceptions import VMwareObjectRetrievalError, VMwareApiError, \ + ArgumentValueError, VMwareRuntimeError + +#i Import Salt libraries +import salt.utils.vmware as vmware +# Import Third Party Libs +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + +# Get Logging Started +log = logging.getLogger(__name__) + + +class FakeTaskClass(object): + pass From 3584a9169269ed5f672b9e62ad8455a8bfddc2a3 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:15:46 -0400 Subject: [PATCH 147/633] Added tests for salt.utils.vmware.get_dvss --- tests/unit/utils/vmware/test_dvs.py | 75 +++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index 27c7886eb5..31f87d5b13 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -31,3 +31,78 @@ log = logging.getLogger(__name__) class FakeTaskClass(object): pass + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetDvssTestCase(TestCase): + def setUp(self): + self.mock_si = MagicMock() + self.mock_dc_ref = MagicMock() + self.mock_traversal_spec = MagicMock() + self.mock_items = [{'object': MagicMock(), + 'name': 'fake_dvs1'}, + {'object': MagicMock(), + 'name': 'fake_dvs2'}, + {'object': MagicMock(), + 'name': 'fake_dvs3'}] + self.mock_get_mors = MagicMock(return_value=self.mock_items) + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock()), + ('salt.utils.vmware.get_mors_with_properties', + self.mock_get_mors), + ('salt.utils.vmware.get_service_instance_from_managed_object', + MagicMock(return_value=self.mock_si)), + ('salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + MagicMock(return_value=self.mock_traversal_spec))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_dc_ref', 'mock_traversal_spec', + 'mock_items', 'mock_get_mors'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.get_dvss(self.mock_dc_ref) + mock_get_managed_object_name.assert_called_once_with(self.mock_dc_ref) + + def test_traversal_spec(self): + mock_traversal_spec = MagicMock(return_value='traversal_spec') + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec): + + vmware.get_dvss(self.mock_dc_ref) + mock_traversal_spec.assert_called( + call(path='networkFolder', skip=True, type=vim.Datacenter, + selectSet=['traversal_spec']), + call(path='childEntity', skip=False, type=vim.Folder)) + + def test_get_mors_with_properties(self): + vmware.get_dvss(self.mock_dc_ref) + self.mock_get_mors.assert_called_once_with( + self.mock_si, vim.DistributedVirtualSwitch, + container_ref=self.mock_dc_ref, property_list=['name'], + traversal_spec=self.mock_traversal_spec) + + def test_get_no_dvss(self): + ret = vmware.get_dvss(self.mock_dc_ref) + self.assertEqual(ret, []) + + def test_get_all_dvss(self): + ret = vmware.get_dvss(self.mock_dc_ref, get_all_dvss=True) + self.assertEqual(ret, [i['object'] for i in self.mock_items]) + + def test_filtered_all_dvss(self): + ret = vmware.get_dvss(self.mock_dc_ref, + dvs_names=['fake_dvs1', 'fake_dvs3', 'no_dvs']) + self.assertEqual(ret, [self.mock_items[0]['object'], + self.mock_items[2]['object']]) From c0040aaa1a457b0d8a09b1ad8a25c46fea4e37bc Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:18:17 -0400 Subject: [PATCH 148/633] Added salt.utils.vmware.get_network_folder that retrieves the network folder --- salt/utils/vmware.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 91f86b4b82..fb621a3dcd 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1020,6 +1020,30 @@ def get_dvss(dc_ref, dvs_names=None, get_all_dvss=False): return items +def get_network_folder(dc_ref): + ''' + Retrieves the network folder of a datacenter + ''' + dc_name = get_managed_object_name(dc_ref) + log.trace('Retrieving network folder in datacenter ' + '\'{0}\''.format(dc_name)) + service_instance = get_service_instance_from_managed_object(dc_ref) + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='networkFolder', + skip=False, + type=vim.Datacenter) + entries = get_mors_with_properties(service_instance, + vim.Folder, + container_ref=dc_ref, + property_list=['name'], + traversal_spec=traversal_spec) + if not entries: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Network folder in datacenter \'{0}\' wasn\'t retrieved' + ''.format(dc_name)) + return entries[0]['object'] + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From 4f09bf5e880bfd55b3b24fdffa690723fa2554b7 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:20:17 -0400 Subject: [PATCH 149/633] Added tests for salt.utils.vmware.get_network_folder --- tests/unit/utils/vmware/test_dvs.py | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index 31f87d5b13..548a2e8909 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -106,3 +106,70 @@ class GetDvssTestCase(TestCase): dvs_names=['fake_dvs1', 'fake_dvs3', 'no_dvs']) self.assertEqual(ret, [self.mock_items[0]['object'], self.mock_items[2]['object']]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetNetworkFolderTestCase(TestCase): + def setUp(self): + self.mock_si = MagicMock() + self.mock_dc_ref = MagicMock() + self.mock_traversal_spec = MagicMock() + self.mock_entries = [{'object': MagicMock(), + 'name': 'fake_netw_folder'}] + self.mock_get_mors = MagicMock(return_value=self.mock_entries) + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_dc')), + ('salt.utils.vmware.get_service_instance_from_managed_object', + MagicMock(return_value=self.mock_si)), + ('salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + MagicMock(return_value=self.mock_traversal_spec)), + ('salt.utils.vmware.get_mors_with_properties', + self.mock_get_mors)) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_dc_ref', 'mock_traversal_spec', + 'mock_entries', 'mock_get_mors'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.get_network_folder(self.mock_dc_ref) + mock_get_managed_object_name.assert_called_once_with(self.mock_dc_ref) + + def test_traversal_spec(self): + mock_traversal_spec = MagicMock(return_value='traversal_spec') + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec): + + vmware.get_network_folder(self.mock_dc_ref) + mock_traversal_spec.assert_called_once_with( + path='networkFolder', skip=False, type=vim.Datacenter) + + def test_get_mors_with_properties(self): + vmware.get_network_folder(self.mock_dc_ref) + self.mock_get_mors.assert_called_once_with( + self.mock_si, vim.Folder, container_ref=self.mock_dc_ref, + property_list=['name'], traversal_spec=self.mock_traversal_spec) + + def test_get_no_network_folder(self): + with patch('salt.utils.vmware.get_mors_with_properties', + MagicMock(return_value=[])): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vmware.get_network_folder(self.mock_dc_ref) + self.assertEqual(excinfo.exception.strerror, + 'Network folder in datacenter \'fake_dc\' wasn\'t ' + 'retrieved') + + def test_get_network_folder(self): + ret = vmware.get_network_folder(self.mock_dc_ref) + self.assertEqual(ret, self.mock_entries[0]['object']) From 793acab99fb6416922589c6ca3e4b9b2744b13eb Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:21:33 -0400 Subject: [PATCH 150/633] Added for salt.utils.vmware.create_dvs --- salt/utils/vmware.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index fb621a3dcd..6055cf5ce2 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1044,6 +1044,46 @@ def get_network_folder(dc_ref): return entries[0]['object'] +def create_dvs(dc_ref, dvs_name, dvs_create_spec=None): + ''' + Creates a distributed virtual switches (DVS) in a datacenter. + Returns the reference to the newly created distributed virtual switch. + + dc_ref + The parent datacenter reference. + + dvs_name + The name of the DVS to create. + + dvs_create_spec + The DVS spec (vim.DVSCreateSpec) to use when creating the DVS. + Default is None. + ''' + dc_name = get_managed_object_name(dc_ref) + log.trace('Creating DVS \'{0}\' in datacenter ' + '\'{1}\''.format(dvs_name, dc_name)) + if not dvs_create_spec: + dvs_create_spec = vim.DVSCreateSpec() + if not dvs_create_spec.configSpec: + dvs_create_spec.configSpec = vim.VMwareDVSConfigSpec() + dvs_create_spec.configSpec.name = dvs_name + netw_folder_ref = get_network_folder(dc_ref) + try: + task = netw_folder_ref.CreateDVS_Task(dvs_create_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + wait_for_task(task, dvs_name, str(task.__class__)) + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From d31d98c2d39723186dac777485b5a833b00e0ea7 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:23:00 -0400 Subject: [PATCH 151/633] Added tests for salt.utils.vmware.create_dvs --- tests/unit/utils/vmware/test_dvs.py | 102 ++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index 548a2e8909..da49c91f8c 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -173,3 +173,105 @@ class GetNetworkFolderTestCase(TestCase): def test_get_network_folder(self): ret = vmware.get_network_folder(self.mock_dc_ref) self.assertEqual(ret, self.mock_entries[0]['object']) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class CreateDvsTestCase(TestCase): + def setUp(self): + self.mock_dc_ref = MagicMock() + self.mock_dvs_create_spec = MagicMock() + self.mock_task = MagicMock(spec=FakeTaskClass) + self.mock_netw_folder = \ + MagicMock(CreateDVS_Task=MagicMock( + return_value=self.mock_task)) + self.mock_wait_for_task = MagicMock() + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_dc')), + ('salt.utils.vmware.get_network_folder', + MagicMock(return_value=self.mock_netw_folder)), + ('salt.utils.vmware.wait_for_task', self.mock_wait_for_task)) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_dc_ref', 'mock_dvs_create_spec', + 'mock_task', 'mock_netw_folder', 'mock_wait_for_task'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.create_dvs(self.mock_dc_ref, 'fake_dvs') + mock_get_managed_object_name.assert_called_once_with(self.mock_dc_ref) + + def test_no_dvs_create_spec(self): + mock_spec = MagicMock(configSpec=None) + mock_config_spec = MagicMock() + mock_dvs_create_spec = MagicMock(return_value=mock_spec) + mock_vmware_dvs_config_spec = \ + MagicMock(return_value=mock_config_spec) + with patch('salt.utils.vmware.vim.DVSCreateSpec', + mock_dvs_create_spec): + with patch('salt.utils.vmware.vim.VMwareDVSConfigSpec', + mock_vmware_dvs_config_spec): + vmware.create_dvs(self.mock_dc_ref, 'fake_dvs') + mock_dvs_create_spec.assert_called_once_with() + mock_vmware_dvs_config_spec.assert_called_once_with() + self.assertEqual(mock_spec.configSpec, mock_config_spec) + self.assertEqual(mock_config_spec.name, 'fake_dvs') + self.mock_netw_folder.CreateDVS_Task.assert_called_once_with(mock_spec) + + def test_get_network_folder(self): + mock_get_network_folder = MagicMock() + with patch('salt.utils.vmware.get_network_folder', + mock_get_network_folder): + vmware.create_dvs(self.mock_dc_ref, 'fake_dvs') + mock_get_network_folder.assert_called_once_with(self.mock_dc_ref) + + def test_create_dvs_task_passed_in_spec(self): + vmware.create_dvs(self.mock_dc_ref, 'fake_dvs', + dvs_create_spec=self.mock_dvs_create_spec) + self.mock_netw_folder.CreateDVS_Task.assert_called_once_with( + self.mock_dvs_create_spec) + + def test_create_dvs_task_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_netw_folder.CreateDVS_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.create_dvs(self.mock_dc_ref, 'fake_dvs', + dvs_create_spec=self.mock_dvs_create_spec) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_create_dvs_task_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_netw_folder.CreateDVS_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.create_dvs(self.mock_dc_ref, 'fake_dvs', + dvs_create_spec=self.mock_dvs_create_spec) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_create_dvs_task_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_netw_folder.CreateDVS_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vmware.create_dvs(self.mock_dc_ref, 'fake_dvs', + dvs_create_spec=self.mock_dvs_create_spec) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_wait_for_tasks(self): + vmware.create_dvs(self.mock_dc_ref, 'fake_dvs', + dvs_create_spec=self.mock_dvs_create_spec) + self.mock_wait_for_task.assert_called_once_with( + self.mock_task, 'fake_dvs', + '') From ce6e8c8522d8205ac5bd914ff32fc98f2244d826 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:24:09 -0400 Subject: [PATCH 152/633] Added salt.utils.vmware.update_dvs --- salt/utils/vmware.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 6055cf5ce2..96da8309e3 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1084,6 +1084,35 @@ def create_dvs(dc_ref, dvs_name, dvs_create_spec=None): wait_for_task(task, dvs_name, str(task.__class__)) +def update_dvs(dvs_ref, dvs_config_spec): + ''' + Updates a distributed virtual switch with the config_spec. + + dvs_ref + The DVS reference. + + dvs_config_spec + The updated config spec (vim.VMwareDVSConfigSpec) to be applied to + the DVS. + ''' + dvs_name = get_managed_object_name(dvs_ref) + log.trace('Updating dvs \'{0}\''.format(dvs_name)) + try: + task = dvs_ref.ReconfigureDvs_Task(dvs_config_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + wait_for_task(task, dvs_name, str(task.__class__)) + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From f21187446242ec394f4b6ef4c5fcbedb719486e5 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:26:12 -0400 Subject: [PATCH 153/633] Added tests for salt.utils.vmware.update_dvs --- tests/unit/utils/vmware/test_dvs.py | 69 +++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index da49c91f8c..e772007bb8 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -275,3 +275,72 @@ class CreateDvsTestCase(TestCase): self.mock_wait_for_task.assert_called_once_with( self.mock_task, 'fake_dvs', '') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class UpdateDvsTestCase(TestCase): + def setUp(self): + self.mock_task = MagicMock(spec=FakeTaskClass) + self.mock_dvs_ref = MagicMock( + ReconfigureDvs_Task=MagicMock(return_value=self.mock_task)) + self.mock_dvs_spec = MagicMock() + self.mock_wait_for_task = MagicMock() + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_dvs')), + ('salt.utils.vmware.wait_for_task', self.mock_wait_for_task)) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_dvs_ref', 'mock_task', 'mock_dvs_spec', + 'mock_wait_for_task'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.update_dvs(self.mock_dvs_ref, self.mock_dvs_spec) + mock_get_managed_object_name.assert_called_once_with(self.mock_dvs_ref) + + def test_reconfigure_dvs_task(self): + vmware.update_dvs(self.mock_dvs_ref, self.mock_dvs_spec) + self.mock_dvs_ref.ReconfigureDvs_Task.assert_called_once_with( + self.mock_dvs_spec) + + def test_reconfigure_dvs_task_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_dvs_ref.ReconfigureDvs_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.update_dvs(self.mock_dvs_ref, self.mock_dvs_spec) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_reconfigure_dvs_task_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_dvs_ref.ReconfigureDvs_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.update_dvs(self.mock_dvs_ref, self.mock_dvs_spec) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_reconfigure_dvs_task_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_dvs_ref.ReconfigureDvs_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vmware.update_dvs(self.mock_dvs_ref, self.mock_dvs_spec) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_wait_for_tasks(self): + vmware.update_dvs(self.mock_dvs_ref, self.mock_dvs_spec) + self.mock_wait_for_task.assert_called_once_with( + self.mock_task, 'fake_dvs', + '') From 77a815dbed46522d0d13839c53ef14d616d375b5 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:27:36 -0400 Subject: [PATCH 154/633] Added salt.utils.vmware.set_dvs_network_resource_management_enabled --- salt/utils/vmware.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 96da8309e3..ac5bcbb6d3 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1113,6 +1113,34 @@ def update_dvs(dvs_ref, dvs_config_spec): wait_for_task(task, dvs_name, str(task.__class__)) +def set_dvs_network_resource_management_enabled(dvs_ref, enabled): + ''' + Sets whether NIOC is enabled on a DVS. + + dvs_ref + The DVS reference. + + enabled + Flag specifying whether NIOC is enabled. + ''' + dvs_name = get_managed_object_name(dvs_ref) + log.trace('Setting network resource management enable to {0} on ' + 'dvs \'{1}\''.format(enabled, dvs_name)) + try: + dvs_ref.EnableNetworkResourceManagement(enable=enabled) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From aa247b43b8936badc8b82eefead8c268f6a5e189 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:29:19 -0400 Subject: [PATCH 155/633] Added tests for salt.utils.vmware.set_dvs_network_resource_management_enabled --- tests/unit/utils/vmware/test_dvs.py | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index e772007bb8..df1f3afd98 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -344,3 +344,69 @@ class UpdateDvsTestCase(TestCase): self.mock_wait_for_task.assert_called_once_with( self.mock_task, 'fake_dvs', '') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class SetDvsNetworkResourceManagementEnabledTestCase(TestCase): + def setUp(self): + self.mock_enabled = MagicMock() + self.mock_dvs_ref = MagicMock( + EnableNetworkResourceManagement=MagicMock()) + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_dvs')),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_dvs_ref', 'mock_enabled'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.set_dvs_network_resource_management_enabled( + self.mock_dvs_ref, self.mock_enabled) + mock_get_managed_object_name.assert_called_once_with(self.mock_dvs_ref) + + def test_enable_network_resource_management(self): + vmware.set_dvs_network_resource_management_enabled( + self.mock_dvs_ref, self.mock_enabled) + self.mock_dvs_ref.EnableNetworkResourceManagement.assert_called_once_with( + enable=self.mock_enabled) + + def test_enable_network_resource_management_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_dvs_ref.EnableNetworkResourceManagement = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.set_dvs_network_resource_management_enabled( + self.mock_dvs_ref, self.mock_enabled) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_enable_network_resource_management_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_dvs_ref.EnableNetworkResourceManagement = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.set_dvs_network_resource_management_enabled( + self.mock_dvs_ref, self.mock_enabled) + + def test_enable_network_resource_management_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_dvs_ref.EnableNetworkResourceManagement = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vmware.set_dvs_network_resource_management_enabled( + self.mock_dvs_ref, self.mock_enabled) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') From 16b71d8ab1975f1aeaa11afe2a6576e46671a977 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:30:59 -0400 Subject: [PATCH 156/633] Added salt.utils.vmware.get_dvportgroups to retrieve distributed virtual portgroups --- salt/utils/vmware.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index ac5bcbb6d3..ee671eeb1a 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1141,6 +1141,60 @@ def set_dvs_network_resource_management_enabled(dvs_ref, enabled): raise salt.exceptions.VMwareRuntimeError(exc.msg) +def get_dvportgroups(parent_ref, portgroup_names=None, + get_all_portgroups=False): + ''' + Returns distributed virtual porgroups (dvportgroups). + The parent object can be either a datacenter or a dvs. + + parent_ref + The parent object reference. Can be either a datacenter or a dvs. + + portgroup_names + The names of the dvss to return. Default is None. + + get_all_portgroups + Return all portgroups in the parent. Default is False. + ''' + if not (isinstance(parent_ref, vim.Datacenter) or + isinstance(parent_ref, vim.DistributedVirtualSwitch)): + raise salt.exceptions.ArgumentValueError( + 'Parent has to be either a datacenter, ' + 'or a distributed virtual switch') + parent_name = get_managed_object_name(parent_ref) + log.trace('Retrieving portgroup in {0} \'{1}\', portgroups_names=\'{2}\', ' + 'get_all_portgroups={3}'.format( + type(parent_ref).__name__, parent_name, + ','.join(portgroup_names) if portgroup_names else None, + get_all_portgroups)) + properties = ['name'] + if isinstance(parent_ref, vim.Datacenter): + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='networkFolder', + skip=True, + type=vim.Datacenter, + selectSet=[vmodl.query.PropertyCollector.TraversalSpec( + path='childEntity', + skip=False, + type=vim.Folder)]) + else: # parent is distributed virtual switch + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='portgroup', + skip=False, + type=vim.DistributedVirtualSwitch) + + service_instance = get_service_instance_from_managed_object(parent_ref) + items = [i['object'] for i in + get_mors_with_properties(service_instance, + vim.DistributedVirtualPortgroup, + container_ref=parent_ref, + property_list=properties, + traversal_spec=traversal_spec) + if get_all_portgroups or + (portgroup_names and i['name'] in portgroup_names)] + return items + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From 82f6ae368880a0453955bdd82bd9d02d458f3505 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:32:30 -0400 Subject: [PATCH 157/633] Added tests for salt.utils.vmware.get_dvportgroups --- tests/unit/utils/vmware/test_dvs.py | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index df1f3afd98..da3a3883de 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -410,3 +410,97 @@ class SetDvsNetworkResourceManagementEnabledTestCase(TestCase): vmware.set_dvs_network_resource_management_enabled( self.mock_dvs_ref, self.mock_enabled) self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetDvportgroupsTestCase(TestCase): + def setUp(self): + self.mock_si = MagicMock() + self.mock_dc_ref = MagicMock(spec=vim.Datacenter) + self.mock_dvs_ref = MagicMock(spec=vim.DistributedVirtualSwitch) + self.mock_traversal_spec = MagicMock() + self.mock_items = [{'object': MagicMock(), + 'name': 'fake_pg1'}, + {'object': MagicMock(), + 'name': 'fake_pg2'}, + {'object': MagicMock(), + 'name': 'fake_pg3'}] + self.mock_get_mors = MagicMock(return_value=self.mock_items) + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock()), + ('salt.utils.vmware.get_mors_with_properties', + self.mock_get_mors), + ('salt.utils.vmware.get_service_instance_from_managed_object', + MagicMock(return_value=self.mock_si)), + ('salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + MagicMock(return_value=self.mock_traversal_spec))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_dc_ref', 'mock_dvs_ref', + 'mock_traversal_spec', 'mock_items', 'mock_get_mors'): + delattr(self, attr) + + def test_unsupported_parrent(self): + with self.assertRaises(ArgumentValueError) as excinfo: + vmware.get_dvportgroups(MagicMock()) + self.assertEqual(excinfo.exception.strerror, + 'Parent has to be either a datacenter, or a ' + 'distributed virtual switch') + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.get_dvportgroups(self.mock_dc_ref) + mock_get_managed_object_name.assert_called_once_with(self.mock_dc_ref) + + def test_traversal_spec_datacenter_parent(self): + mock_traversal_spec = MagicMock(return_value='traversal_spec') + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec): + + vmware.get_dvportgroups(self.mock_dc_ref) + mock_traversal_spec.assert_called( + call(path='networkFolder', skip=True, type=vim.Datacenter, + selectSet=['traversal_spec']), + call(path='childEntity', skip=False, type=vim.Folder)) + + def test_traversal_spec_dvs_parent(self): + mock_traversal_spec = MagicMock(return_value='traversal_spec') + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec): + + vmware.get_dvportgroups(self.mock_dvs_ref) + mock_traversal_spec.assert_called_once_with( + path='portgroup', skip=False, type=vim.DistributedVirtualSwitch) + + def test_get_mors_with_properties(self): + vmware.get_dvportgroups(self.mock_dvs_ref) + self.mock_get_mors.assert_called_once_with( + self.mock_si, vim.DistributedVirtualPortgroup, + container_ref=self.mock_dvs_ref, property_list=['name'], + traversal_spec=self.mock_traversal_spec) + + def test_get_no_pgs(self): + ret = vmware.get_dvportgroups(self.mock_dvs_ref) + self.assertEqual(ret, []) + + def test_get_all_pgs(self): + ret = vmware.get_dvportgroups(self.mock_dvs_ref, + get_all_portgroups=True) + self.assertEqual(ret, [i['object'] for i in self.mock_items]) + + def test_filtered_pgs(self): + ret = vmware.get_dvss(self.mock_dc_ref, + dvs_names=['fake_pg1', 'fake_pg3', 'no_pg']) + self.assertEqual(ret, [self.mock_items[0]['object'], + self.mock_items[2]['object']]) From 35fa6df4ec116508ff905a7c2f6fc455be339bfb Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:33:59 -0400 Subject: [PATCH 158/633] Added salt.utils.vmware.get_uplink_dvportgroup to retrieve the uplink distributed virtual portgroup --- salt/utils/vmware.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index ee671eeb1a..0c0c42767e 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1195,6 +1195,35 @@ def get_dvportgroups(parent_ref, portgroup_names=None, return items +def get_uplink_dvportgroup(dvs_ref): + ''' + Returns the uplink distributed virtual portgroup of a distributed virtual + switch (dvs) + + dvs_ref + The dvs reference + ''' + dvs_name = get_managed_object_name(dvs_ref) + log.trace('Retrieving uplink portgroup of dvs \'{0}\''.format(dvs_name)) + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='portgroup', + skip=False, + type=vim.DistributedVirtualSwitch) + service_instance = get_service_instance_from_managed_object(dvs_ref) + items = [entry['object'] for entry in + get_mors_with_properties(service_instance, + vim.DistributedVirtualPortgroup, + container_ref=dvs_ref, + property_list=['tag'], + traversal_spec=traversal_spec) + if entry['tag'] and + [t for t in entry['tag'] if t.key == 'SYSTEM/DVS.UPLINKPG']] + if not items: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Uplink portgroup of DVS \'{0}\' wasn\'t found'.format(dvs_name)) + return items[0] + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From b8bc8fd581f3b3cd7503f113e7c35c7f2f4dda87 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:35:00 -0400 Subject: [PATCH 159/633] Added tests for salt.utils.vmware.get_uplink_dvportgroup --- tests/unit/utils/vmware/test_dvs.py | 69 +++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index da3a3883de..2388afe94b 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -504,3 +504,72 @@ class GetDvportgroupsTestCase(TestCase): dvs_names=['fake_pg1', 'fake_pg3', 'no_pg']) self.assertEqual(ret, [self.mock_items[0]['object'], self.mock_items[2]['object']]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetUplinkDvportgroupTestCase(TestCase): + def setUp(self): + self.mock_si = MagicMock() + self.mock_dvs_ref = MagicMock(spec=vim.DistributedVirtualSwitch) + self.mock_traversal_spec = MagicMock() + self.mock_items = [{'object': MagicMock(), + 'tag': [MagicMock(key='fake_tag')]}, + {'object': MagicMock(), + 'tag': [MagicMock(key='SYSTEM/DVS.UPLINKPG')]}] + self.mock_get_mors = MagicMock(return_value=self.mock_items) + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_dvs')), + ('salt.utils.vmware.get_mors_with_properties', + self.mock_get_mors), + ('salt.utils.vmware.get_service_instance_from_managed_object', + MagicMock(return_value=self.mock_si)), + ('salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + MagicMock(return_value=self.mock_traversal_spec))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_dvs_ref', 'mock_traversal_spec', + 'mock_items', 'mock_get_mors'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.get_uplink_dvportgroup(self.mock_dvs_ref) + mock_get_managed_object_name.assert_called_once_with(self.mock_dvs_ref) + + def test_traversal_spec(self): + mock_traversal_spec = MagicMock(return_value='traversal_spec') + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec): + + vmware.get_uplink_dvportgroup(self.mock_dvs_ref) + mock_traversal_spec.assert_called_once_with( + path='portgroup', skip=False, type=vim.DistributedVirtualSwitch) + + def test_get_mors_with_properties(self): + vmware.get_uplink_dvportgroup(self.mock_dvs_ref) + self.mock_get_mors.assert_called_once_with( + self.mock_si, vim.DistributedVirtualPortgroup, + container_ref=self.mock_dvs_ref, property_list=['tag'], + traversal_spec=self.mock_traversal_spec) + + def test_get_no_uplink_pg(self): + with patch('salt.utils.vmware.get_mors_with_properties', + MagicMock(return_value=[])): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vmware.get_uplink_dvportgroup(self.mock_dvs_ref) + self.assertEqual(excinfo.exception.strerror, + 'Uplink portgroup of DVS \'fake_dvs\' wasn\'t found') + + def test_get_uplink_pg(self): + ret = vmware.get_uplink_dvportgroup(self.mock_dvs_ref) + self.assertEqual(ret, self.mock_items[1]['object']) From 13b4e0e426d6bdf414b9a5515fb7983863a1d0c5 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:36:34 -0400 Subject: [PATCH 160/633] Added salt.utils.vmware.create_dvportgroup to create a distributed virtual portgroup --- salt/utils/vmware.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 0c0c42767e..7b92e86d8e 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1224,6 +1224,37 @@ def get_uplink_dvportgroup(dvs_ref): return items[0] +def create_dvportgroup(dvs_ref, spec): + ''' + Creates a distributed virtual portgroup on a distributed virtual switch + (dvs) + + dvs_ref + The dvs reference + + spec + Portgroup spec (vim.DVPortgroupConfigSpec) + ''' + dvs_name = get_managed_object_name(dvs_ref) + log.trace('Adding portgroup {0} to dvs ' + '\'{1}\''.format(spec.name, dvs_name)) + log.trace('spec = {}'.format(spec)) + try: + task = dvs_ref.CreateDVPortgroup_Task(spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + wait_for_task(task, dvs_name, str(task.__class__)) + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From 294fad1de0c4fe6d5fe47230cdb87852539dfe5e Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:38:08 -0400 Subject: [PATCH 161/633] Added salt.utils.vmware.update_dvportgroup to update a distributed virtual portgroup --- salt/utils/vmware.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 7b92e86d8e..e006f80322 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1255,6 +1255,34 @@ def create_dvportgroup(dvs_ref, spec): wait_for_task(task, dvs_name, str(task.__class__)) +def update_dvportgroup(portgroup_ref, spec): + ''' + Updates a distributed virtual portgroup + + portgroup_ref + The portgroup reference + + spec + Portgroup spec (vim.DVPortgroupConfigSpec) + ''' + pg_name = get_managed_object_name(portgroup_ref) + log.trace('Updating portgrouo {0}'.format(pg_name)) + try: + task = portgroup_ref.ReconfigureDVPortgroup_Task(spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + wait_for_task(task, pg_name, str(task.__class__)) + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From 8a84f27adffdf414a38832eded7ec67f276d344f Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:39:13 -0400 Subject: [PATCH 162/633] Added tests for salt.utils.vmware.create_dvportgroup --- tests/unit/utils/vmware/test_dvs.py | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index 2388afe94b..8cfc1e04a5 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -573,3 +573,73 @@ class GetUplinkDvportgroupTestCase(TestCase): def test_get_uplink_pg(self): ret = vmware.get_uplink_dvportgroup(self.mock_dvs_ref) self.assertEqual(ret, self.mock_items[1]['object']) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class CreateDvportgroupTestCase(TestCase): + def setUp(self): + self.mock_pg_spec = MagicMock() + self.mock_task = MagicMock(spec=FakeTaskClass) + self.mock_dvs_ref = \ + MagicMock(CreateDVPortgroup_Task=MagicMock( + return_value=self.mock_task)) + self.mock_wait_for_task = MagicMock() + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_dvs')), + ('salt.utils.vmware.wait_for_task', self.mock_wait_for_task)) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_pg_spec', 'mock_dvs_ref', 'mock_task', + 'mock_wait_for_task'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.create_dvportgroup(self.mock_dvs_ref, self.mock_pg_spec) + mock_get_managed_object_name.assert_called_once_with(self.mock_dvs_ref) + + def test_create_dvporgroup_task(self): + vmware.create_dvportgroup(self.mock_dvs_ref, self.mock_pg_spec) + self.mock_dvs_ref.CreateDVPortgroup_Task.assert_called_once_with( + self.mock_pg_spec) + + def test_create_dvporgroup_task_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_dvs_ref.CreateDVPortgroup_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.create_dvportgroup(self.mock_dvs_ref, self.mock_pg_spec) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_create_dvporgroup_task_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_dvs_ref.CreateDVPortgroup_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.create_dvportgroup(self.mock_dvs_ref, self.mock_pg_spec) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_create_dvporgroup_task_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_dvs_ref.CreateDVPortgroup_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vmware.create_dvportgroup(self.mock_dvs_ref, self.mock_pg_spec) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_wait_for_tasks(self): + vmware.create_dvportgroup(self.mock_dvs_ref, self.mock_pg_spec) + self.mock_wait_for_task.assert_called_once_with( + self.mock_task, 'fake_dvs', + '') From ca3d999be097f21e6660e65846ebcfb930714104 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:40:01 -0400 Subject: [PATCH 163/633] Added salt.utils.vmware.remove_dvportgroup --- salt/utils/vmware.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index e006f80322..27b728ca69 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1283,6 +1283,31 @@ def update_dvportgroup(portgroup_ref, spec): wait_for_task(task, pg_name, str(task.__class__)) +def remove_dvportgroup(portgroup_ref): + ''' + Removes a distributed virtual portgroup + + portgroup_ref + The portgroup reference + ''' + pg_name = get_managed_object_name(portgroup_ref) + log.trace('Removing portgrouo {0}'.format(pg_name)) + try: + task = portgroup_ref.Destroy_Task() + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + wait_for_task(task, pg_name, str(task.__class__)) + + def list_objects(service_instance, vim_object, properties=None): ''' Returns a simple list of objects from a given service instance. From d7474f8d30cd57f905fbd549982fae8c9379fa0d Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 06:17:29 -0400 Subject: [PATCH 164/633] Added tests for salt.utils.vmware.update_dvportgroup --- tests/unit/utils/vmware/test_dvs.py | 73 +++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index 8cfc1e04a5..31459d261c 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -643,3 +643,76 @@ class CreateDvportgroupTestCase(TestCase): self.mock_wait_for_task.assert_called_once_with( self.mock_task, 'fake_dvs', '') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class UpdateDvportgroupTestCase(TestCase): + def setUp(self): + self.mock_pg_spec = MagicMock() + self.mock_task = MagicMock(spec=FakeTaskClass) + self.mock_pg_ref = \ + MagicMock(ReconfigureDVPortgroup_Task=MagicMock( + return_value=self.mock_task)) + self.mock_wait_for_task = MagicMock() + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_pg')), + ('salt.utils.vmware.wait_for_task', self.mock_wait_for_task)) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_pg_spec', 'mock_pg_ref', 'mock_task', + 'mock_wait_for_task'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.update_dvportgroup(self.mock_pg_ref, self.mock_pg_spec) + mock_get_managed_object_name.assert_called_once_with(self.mock_pg_ref) + + def test_reconfigure_dvporgroup_task(self): + vmware.update_dvportgroup(self.mock_pg_ref, self.mock_pg_spec) + self.mock_pg_ref.ReconfigureDVPortgroup_Task.assert_called_once_with( + self.mock_pg_spec) + + def test_reconfigure_dvporgroup_task_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_pg_ref.ReconfigureDVPortgroup_Task = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.update_dvportgroup(self.mock_pg_ref, self.mock_pg_spec) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_reconfigure_dvporgroup_task_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_pg_ref.ReconfigureDVPortgroup_Task = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.update_dvportgroup(self.mock_pg_ref, self.mock_pg_spec) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_reconfigure_dvporgroup_task_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_pg_ref.ReconfigureDVPortgroup_Task = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vmware.update_dvportgroup(self.mock_pg_ref, self.mock_pg_spec) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_wait_for_tasks(self): + vmware.update_dvportgroup(self.mock_pg_ref, self.mock_pg_spec) + self.mock_wait_for_task.assert_called_once_with( + self.mock_task, 'fake_pg', + '') From d4d6ad99c22a71b34242dcd6e3872f8ac0ca878c Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 06:17:53 -0400 Subject: [PATCH 165/633] Added tests for salt.utils.vmware.remove_dvportgroup --- tests/unit/utils/vmware/test_dvs.py | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index 31459d261c..6f88484877 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -716,3 +716,70 @@ class UpdateDvportgroupTestCase(TestCase): self.mock_wait_for_task.assert_called_once_with( self.mock_task, 'fake_pg', '') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class RemoveDvportgroupTestCase(TestCase): + def setUp(self): + self.mock_task = MagicMock(spec=FakeTaskClass) + self.mock_pg_ref = \ + MagicMock(Destroy_Task=MagicMock( + return_value=self.mock_task)) + self.mock_wait_for_task = MagicMock() + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_pg')), + ('salt.utils.vmware.wait_for_task', self.mock_wait_for_task)) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_pg_ref', 'mock_task', 'mock_wait_for_task'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vmware.remove_dvportgroup(self.mock_pg_ref) + mock_get_managed_object_name.assert_called_once_with(self.mock_pg_ref) + + def test_destroy_task(self): + vmware.remove_dvportgroup(self.mock_pg_ref) + self.mock_pg_ref.Destroy_Task.assert_called_once_with() + + def test_destroy_task_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_pg_ref.Destroy_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.remove_dvportgroup(self.mock_pg_ref) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_destroy_treconfigure_dvporgroup_task_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_pg_ref.Destroy_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vmware.remove_dvportgroup(self.mock_pg_ref) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_destroy_treconfigure_dvporgroup_task_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_pg_ref.Destroy_Task = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vmware.remove_dvportgroup(self.mock_pg_ref) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_wait_for_tasks(self): + vmware.remove_dvportgroup(self.mock_pg_ref) + self.mock_wait_for_task.assert_called_once_with( + self.mock_task, 'fake_pg', + '') From b65c7be7b4d0e6cddc762bd7f93852a9d93e6a8e Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:46:07 -0400 Subject: [PATCH 166/633] Added private functions to convert a vim.VMwareDistributedVirtualSwitch into a dict representation --- salt/modules/vsphere.py | 105 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index d6aabb74e4..b2bb5666b2 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -3622,6 +3622,111 @@ def vsan_enable(host, username, password, protocol=None, port=None, host_names=N return ret +def _get_dvs_config_dict(dvs_name, dvs_config): + ''' + Returns the dict representation of the DVS config + + dvs_name + The name of the DVS + + dvs_config + The DVS config + ''' + log.trace('Building the dict of the DVS \'{0}\' config'.format(dvs_name)) + conf_dict = {'name': dvs_name, + 'contact_email': dvs_config.contact.contact, + 'contact_name': dvs_config.contact.name, + 'description': dvs_config.description, + 'lacp_api_version': dvs_config.lacpApiVersion, + 'network_resource_control_version': + dvs_config.networkResourceControlVersion, + 'network_resource_management_enabled': + dvs_config.networkResourceManagementEnabled, + 'max_mtu': dvs_config.maxMtu} + if isinstance(dvs_config.uplinkPortPolicy, + vim.DVSNameArrayUplinkPortPolicy): + conf_dict.update( + {'uplink_names': dvs_config.uplinkPortPolicy.uplinkPortName}) + return conf_dict + + +def _get_dvs_link_discovery_protocol(dvs_name, dvs_link_disc_protocol): + ''' + Returns the dict representation of the DVS link discovery protocol + + dvs_name + The name of the DVS + + dvs_link_disc_protocl + The DVS link discovery protocol + ''' + log.trace('Building the dict of the DVS \'{0}\' link discovery ' + 'protocol'.format(dvs_name)) + return {'operation': dvs_link_disc_protocol.operation, + 'protocol': dvs_link_disc_protocol.protocol} + + +def _get_dvs_product_info(dvs_name, dvs_product_info): + ''' + Returns the dict representation of the DVS product_info + + dvs_name + The name of the DVS + + dvs_product_info + The DVS product info + ''' + log.trace('Building the dict of the DVS \'{0}\' product ' + 'info'.format(dvs_name)) + return {'name': dvs_product_info.name, + 'vendor': dvs_product_info.vendor, + 'version': dvs_product_info.version} + + +def _get_dvs_capability(dvs_name, dvs_capability): + ''' + Returns the dict representation of the DVS product_info + + dvs_name + The name of the DVS + + dvs_capability + The DVS capability + ''' + log.trace('Building the dict of the DVS \'{0}\' capability' + ''.format(dvs_name)) + return {'operation_supported': dvs_capability.dvsOperationSupported, + 'portgroup_operation_supported': + dvs_capability.dvPortGroupOperationSupported, + 'port_operation_supported': dvs_capability.dvPortOperationSupported} + + +def _get_dvs_infrastructure_traffic_resources(dvs_name, + dvs_infra_traffic_ress): + ''' + Returns a list of dict representations of the DVS infrastructure traffic + resource + + dvs_name + The name of the DVS + + dvs_infra_traffic_ress + The DVS infrastructure traffic resources + ''' + log.trace('Building the dicts of the DVS \'{0}\' infrastructure traffic ' + 'resources'.format(dvs_name)) + res_dicts = [] + for res in dvs_infra_traffic_ress: + res_dict = {'key': res.key, + 'limit': res.allocationInfo.limit, + 'reservation': res.allocationInfo.reservation} + if res.allocationInfo.shares: + res_dict.update({'num_shares': res.allocationInfo.shares.shares, + 'share_level': res.allocationInfo.shares.level}) + res_dicts.append(res_dict) + return res_dicts + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 3657bad621bfeb74dfc161db99c2e050729dd7ff Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:49:53 -0400 Subject: [PATCH 167/633] Added salt.modules.vsphere.list_dvss to list dict representations of a DVS --- salt/modules/vsphere.py | 66 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index b2bb5666b2..8c4571b919 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -3727,6 +3727,72 @@ def _get_dvs_infrastructure_traffic_resources(dvs_name, return res_dicts +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'esxcluster') +@gets_service_instance_via_proxy +def list_dvss(datacenter=None, dvs_names=None, service_instance=None): + ''' + Returns a list of distributed virtual switches (DVSs). + The list can be filtered by the datacenter or DVS names. + + datacenter + The datacenter to look for DVSs in. + Default value is None. + + dvs_names + List of DVS names to look for. If None, all DVSs are returned. + Default value is None. + + .. code-block:: bash + + salt '*' vsphere.list_dvss + + salt '*' vsphere.list_dvss dvs_names=[dvs1,dvs2] + ''' + ret_dict = [] + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + + for dvs in salt.utils.vmware.get_dvss(dc_ref, dvs_names, (not dvs_names)): + dvs_dict = {} + # XXX: Because of how VMware did DVS object inheritance we can\'t + # be more restrictive when retrieving the dvs config, we have to + # retrieve the entire object + props = salt.utils.vmware.get_properties_of_managed_object( + dvs, ['name', 'config', 'capability', 'networkResourcePool']) + dvs_dict = _get_dvs_config_dict(props['name'], props['config']) + # Product info + dvs_dict.update( + {'product_info': + _get_dvs_product_info(props['name'], + props['config'].productInfo)}) + # Link Discovery Protocol + if props['config'].linkDiscoveryProtocolConfig: + dvs_dict.update( + {'link_discovery_protocol': + _get_dvs_link_discovery_protocol( + props['name'], + props['config'].linkDiscoveryProtocolConfig)}) + # Capability + dvs_dict.update({'capability': + _get_dvs_capability(props['name'], + props['capability'])}) + # InfrastructureTrafficResourceConfig - available with vSphere 6.0 + if hasattr(props['config'], 'infrastructureTrafficResourceConfig'): + dvs_dict.update({ + 'infrastructure_traffic_resource_pools': + _get_dvs_infrastructure_traffic_resources( + props['name'], + props['config'].infrastructureTrafficResourceConfig)}) + ret_dict.append(dvs_dict) + return ret_dict + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 9f6981806a4c932d03abe1c464ff1a10ac0cfd72 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:51:41 -0400 Subject: [PATCH 168/633] Added private functions to apply a DVS dict representation to a VMware spec object --- salt/modules/vsphere.py | 129 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 8c4571b919..03a0afc321 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -3793,6 +3793,135 @@ def list_dvss(datacenter=None, dvs_names=None, service_instance=None): return ret_dict +def _apply_dvs_config(config_spec, config_dict): + ''' + Applies the values of the config dict dictionary to a config spec + (vim.VMwareDVSConfigSpec) + ''' + if config_dict.get('name'): + config_spec.name = config_dict['name'] + if config_dict.get('contact_email') or config_dict.get('contact_name'): + if not config_spec.contact: + config_spec.contact = vim.DVSContactInfo() + config_spec.contact.contact = config_dict.get('contact_email') + config_spec.contact.name = config_dict.get('contact_name') + if config_dict.get('description'): + config_spec.description = config_dict.get('description') + if config_dict.get('max_mtu'): + config_spec.maxMtu = config_dict.get('max_mtu') + if config_dict.get('lacp_api_version'): + config_spec.lacpApiVersion = config_dict.get('lacp_api_version') + if config_dict.get('network_resource_control_version'): + config_spec.networkResourceControlVersion = \ + config_dict.get('network_resource_control_version') + if config_dict.get('uplink_names'): + if not config_spec.uplinkPortPolicy or \ + not isinstance(config_spec.uplinkPortPolicy, + vim.DVSNameArrayUplinkPortPolicy): + + config_spec.uplinkPortPolicy = \ + vim.DVSNameArrayUplinkPortPolicy() + config_spec.uplinkPortPolicy.uplinkPortName = \ + config_dict['uplink_names'] + + +def _apply_dvs_link_discovery_protocol(disc_prot_config, disc_prot_dict): + ''' + Applies the values of the disc_prot_dict dictionary to a link discovery + protocol config object (vim.LinkDiscoveryProtocolConfig) + ''' + disc_prot_config.operation = disc_prot_dict['operation'] + disc_prot_config.protocol = disc_prot_dict['protocol'] + + +def _apply_dvs_product_info(product_info_spec, product_info_dict): + ''' + Applies the values of the product_info_dict dictionary to a product info + spec (vim.DistributedVirtualSwitchProductSpec) + ''' + if product_info_dict.get('name'): + product_info_spec.name = product_info_dict['name'] + if product_info_dict.get('vendor'): + product_info_spec.vendor = product_info_dict['vendor'] + if product_info_dict.get('version'): + product_info_spec.version = product_info_dict['version'] + + +def _apply_dvs_capability(capability_spec, capability_dict): + ''' + Applies the values of the capability_dict dictionary to a DVS capability + object (vim.vim.DVSCapability) + ''' + if 'operation_supported' in capability_dict: + capability_spec.dvsOperationSupported = \ + capability_dict['operation_supported'] + if 'port_operation_supported' in capability_dict: + capability_spec.dvPortOperationSupported = \ + capability_dict['port_operation_supported'] + if 'portgroup_operation_supported' in capability_dict: + capability_spec.dvPortGroupOperationSupported = \ + capability_dict['portgroup_operation_supported'] + + +def _apply_dvs_infrastructure_traffic_resources(infra_traffic_resources, + resource_dicts): + ''' + Applies the values of the resource dictionaries to infra traffic resources, + creating the infra traffic resource if required + (vim.DistributedVirtualSwitchProductSpec) + ''' + for res_dict in resource_dicts: + ress = [r for r in infra_traffic_resources if r.key == res_dict['key']] + if ress: + res = ress[0] + else: + res = vim.DvsHostInfrastructureTrafficResource() + res.key = res_dict['key'] + res.allocationInfo = \ + vim.DvsHostInfrastructureTrafficResourceAllocation() + infra_traffic_resources.append(res) + if res_dict.get('limit'): + res.allocationInfo.limit = res_dict['limit'] + if res_dict.get('reservation'): + res.allocationInfo.reservation = res_dict['reservation'] + if res_dict.get('num_shares') or res_dict.get('share_level'): + if not res.allocationInfo.shares: + res.allocationInfo.shares = vim.SharesInfo() + if res_dict.get('share_level'): + res.allocationInfo.shares.level = \ + vim.SharesLevel(res_dict['share_level']) + if res_dict.get('num_shares'): + #XXX Even though we always set the number of shares if provided, + #the vCenter will ignore it unless the share level is 'custom'. + res.allocationInfo.shares.shares=res_dict['num_shares'] + + +def _apply_dvs_network_resource_pools(network_resource_pools, resource_dicts): + ''' + Applies the values of the resource dictionaries to network resource pools, + creating the resource pools if required + (vim.DVSNetworkResourcePoolConfigSpec) + ''' + for res_dict in resource_dicts: + ress = [r for r in network_resource_pools if r.key == res_dict['key']] + if ress: + res = ress[0] + else: + res = vim.DVSNetworkResourcePoolConfigSpec() + res.key = res_dict['key'] + res.allocationInfo = \ + vim.DVSNetworkResourcePoolAllocationInfo() + network_resource_pools.append(res) + if res_dict.get('limit'): + res.allocationInfo.limit = res_dict['limit'] + if res_dict.get('num_shares') and res_dict.get('share_level'): + if not res.allocationInfo.shares: + res.allocationInfo.shares = vim.SharesInfo() + res.allocationInfo.shares.shares=res_dict['num_shares'] + res.allocationInfo.shares.level = \ + vim.SharesLevel(res_dict['share_level']) + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 34a841a669572d41d55d661c335176b3951416b7 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 18:55:52 -0400 Subject: [PATCH 169/633] Added salt.modules.vsphere.create_dvs to create a DVS based on a dict representations --- salt/modules/vsphere.py | 72 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 03a0afc321..8583f77125 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -3922,6 +3922,78 @@ def _apply_dvs_network_resource_pools(network_resource_pools, resource_dicts): vim.SharesLevel(res_dict['share_level']) +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'esxcluster') +@gets_service_instance_via_proxy +def create_dvs(dvs_dict, dvs_name, service_instance=None): + ''' + Creates a distributed virtual switch (DVS). + + Note: The ``dvs_name`` param will override any name set in ``dvs_dict``. + + dvs_dict + Dict representation of the new DVS (exmaple in salt.states.dvs) + + dvs_name + Name of the DVS to be created. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.create_dvs dvs dict=$dvs_dict dvs_name=dvs_name + ''' + log.trace('Creating dvs \'{0}\' with dict = {1}'.format(dvs_name, + dvs_dict)) + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + # Make the name of the DVS consistent with the call + dvs_dict['name'] = dvs_name + # Build the config spec from the input + dvs_create_spec = vim.DVSCreateSpec() + dvs_create_spec.configSpec = vim.VMwareDVSConfigSpec() + _apply_dvs_config(dvs_create_spec.configSpec, dvs_dict) + if dvs_dict.get('product_info'): + dvs_create_spec.productInfo = vim.DistributedVirtualSwitchProductSpec() + _apply_dvs_product_info(dvs_create_spec.productInfo, + dvs_dict['product_info']) + if dvs_dict.get('capability'): + dvs_create_spec.capability = vim.DVSCapability() + _apply_dvs_capability(dvs_create_spec.capability, + dvs_dict['capability']) + if dvs_dict.get('link_discovery_protocol'): + dvs_create_spec.configSpec.linkDiscoveryProtocolConfig = \ + vim.LinkDiscoveryProtocolConfig() + _apply_dvs_link_discovery_protocol( + dvs_create_spec.configSpec.linkDiscoveryProtocolConfig, + dvs_dict['link_discovery_protocol']) + if dvs_dict.get('infrastructure_traffic_resource_pools'): + dvs_create_spec.configSpec.infrastructureTrafficResourceConfig = [] + _apply_dvs_infrastructure_traffic_resources( + dvs_create_spec.configSpec.infrastructureTrafficResourceConfig, + dvs_dict['infrastructure_traffic_resource_pools']) + log.trace('dvs_create_spec = {}'.format(dvs_create_spec)) + salt.utils.vmware.create_dvs(dc_ref, dvs_name, dvs_create_spec) + if 'network_resource_management_enabled' in dvs_dict: + dvs_refs = salt.utils.vmware.get_dvss(dc_ref, + dvs_names=[dvs_name]) + if not dvs_refs: + raise excs.VMwareObjectRetrievalError( + 'DVS \'{0}\' wasn\'t found in datacenter \'{1}\'' + ''.format(dvs_name, datacenter)) + dvs_ref = dvs_refs[0] + salt.utils.vmware.set_dvs_network_resource_management_enabled( + dvs_ref, dvs_dict['network_resource_management_enabled']) + return True + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From c576d3ca959ed900ceeffa9fa694b7af57544d60 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:03:04 -0400 Subject: [PATCH 170/633] Added salt.modules.vsphere.update_dvs to update a DVS based on a dict representations --- salt/modules/vsphere.py | 77 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 8583f77125..e140195cf1 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -3993,6 +3993,83 @@ def create_dvs(dvs_dict, dvs_name, service_instance=None): dvs_ref, dvs_dict['network_resource_management_enabled']) return True +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'esxcluster') +@gets_service_instance_via_proxy +def update_dvs(dvs_dict, dvs, service_instance=None): + ''' + Updates a distributed virtual switch (DVS). + + Note: Updating the product info, capability, uplinks of a DVS is not + supported so the corresponding entries in ``dvs_dict`` will be + ignored. + + dvs_dict + Dictionary with the values the DVS should be update with + (exmaple in salt.states.dvs) + + dvs + Name of the DVS to be updated. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.update_dvs dvs_dict=$dvs_dict dvs=dvs1 + ''' + # Remove ignored properties + log.trace('Updating dvs \'{0}\' with dict = {1}'.format(dvs, dvs_dict)) + for prop in ['product_info', 'capability', 'uplink_names', 'name']: + if prop in dvs_dict: + del dvs_dict[prop] + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + dvs_refs = salt.utils.vmware.get_dvss(dc_ref, dvs_names=[dvs]) + if not dvs_refs: + raise VMwareObjectRetrievalError('DVS \'{0}\' wasn\'t found in ' + 'datacenter \'{1}\'' + ''.format(dvs, datacenter)) + dvs_ref = dvs_refs[0] + # Build the config spec from the input + dvs_props = salt.utils.vmware.get_properties_of_managed_object( + dvs_ref, ['config', 'capability']) + dvs_config = vim.VMwareDVSConfigSpec() + # Copy all of the properties in the config of the of the DVS to a + # DvsConfigSpec + skipped_properties = ['host'] + for prop in dvs_config.__dict__.keys(): + if prop in skipped_properties: + continue + if hasattr(dvs_props['config'], prop): + setattr(dvs_config, prop, getattr(dvs_props['config'], prop)) + _apply_dvs_config(dvs_config, dvs_dict) + if dvs_dict.get('link_discovery_protocol'): + if not dvs_config.linkDiscoveryProtocolConfig: + dvs_config.linkDiscoveryProtocolConfig = \ + vim.LinkDiscoveryProtocolConfig() + _apply_dvs_link_discovery_protocol( + dvs_config.linkDiscoveryProtocolConfig, + dvs_dict['link_discovery_protocol']) + if dvs_dict.get('infrastructure_traffic_resource_pools'): + if not dvs_config.infrastructureTrafficResourceConfig: + dvs_config.infrastructureTrafficResourceConfig = [] + _apply_dvs_infrastructure_traffic_resources( + dvs_config.infrastructureTrafficResourceConfig, + dvs_dict['infrastructure_traffic_resource_pools']) + log.trace('dvs_config= {}'.format(dvs_config)) + salt.utils.vmware.update_dvs(dvs_ref, dvs_config_spec=dvs_config) + if 'network_resource_management_enabled' in dvs_dict: + salt.utils.vmware.set_dvs_network_resource_management_enabled( + dvs_ref, dvs_dict['network_resource_management_enabled']) + return True + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') From 5c57e30d3155960946b3bdc5423a3006a030a2bd Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:09:05 -0400 Subject: [PATCH 171/633] Added salt.modules.vsphere.list_dvportgroups to list dict representations of a DVPortgroups --- salt/modules/vsphere.py | 169 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index e140195cf1..f3833b1a01 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4071,6 +4071,175 @@ def update_dvs(dvs_dict, dvs, service_instance=None): return True +def _get_dvportgroup_out_shaping(pg_name, pg_default_port_config): + ''' + Returns the out shaping policy of a distributed virtual portgroup + + pg_name + The name of the portgroup + + pg_default_port_config + The dafault port config of the portgroup + ''' + log.trace('Retrieving portgroup\'s \'{0}\' out shaping ' + 'config'.format(pg_name)) + out_shaping_policy = pg_default_port_config.outShapingPolicy + if not out_shaping_policy: + return {} + return {'average_bandwidth': out_shaping_policy.averageBandwidth.value, + 'burst_size': out_shaping_policy.burstSize.value, + 'enabled': out_shaping_policy.enabled.value, + 'peak_bandwidth': out_shaping_policy.peakBandwidth.value} + + +def _get_dvportgroup_security_policy(pg_name, pg_default_port_config): + ''' + Returns the security policy of a distributed virtual portgroup + + pg_name + The name of the portgroup + + pg_default_port_config + The dafault port config of the portgroup + ''' + log.trace('Retrieving portgroup\'s \'{0}\' security policy ' + 'config'.format(pg_name)) + sec_policy = pg_default_port_config.securityPolicy + if not sec_policy: + return {} + return {'allow_promiscuous': sec_policy.allowPromiscuous.value, + 'forged_transmits': sec_policy.forgedTransmits.value, + 'mac_changes': sec_policy.macChanges.value} + + +def _get_dvportgroup_teaming(pg_name, pg_default_port_config): + ''' + Returns the teaming of a distributed virtual portgroup + + pg_name + The name of the portgroup + + pg_default_port_config + The dafault port config of the portgroup + ''' + log.trace('Retrieving portgroup\'s \'{0}\' teaming' + 'config'.format(pg_name)) + teaming_policy = pg_default_port_config.uplinkTeamingPolicy + if not teaming_policy: + return {} + ret_dict = {'notify_switches': teaming_policy.notifySwitches.value, + 'policy': teaming_policy.policy.value, + 'reverse_policy': teaming_policy.reversePolicy.value, + 'rolling_order': teaming_policy.rollingOrder.value} + if teaming_policy.failureCriteria: + failure_criteria = teaming_policy.failureCriteria + ret_dict.update({'failure_criteria': { + 'check_beacon': failure_criteria.checkBeacon.value, + 'check_duplex': failure_criteria.checkDuplex.value, + 'check_error_percent': failure_criteria.checkErrorPercent.value, + 'check_speed': failure_criteria.checkSpeed.value, + 'full_duplex': failure_criteria.fullDuplex.value, + 'percentage': failure_criteria.percentage.value, + 'speed': failure_criteria.speed.value}}) + if teaming_policy.uplinkPortOrder: + uplink_order = teaming_policy.uplinkPortOrder + ret_dict.update({'port_order': { + 'active': uplink_order.activeUplinkPort, + 'standby': uplink_order.standbyUplinkPort}}) + return ret_dict + + +def _get_dvportgroup_dict(pg_ref): + ''' + Returns a dictionary with a distributed virutal portgroup data + + + pg_ref + Portgroup reference + ''' + props = salt.utils.vmware.get_properties_of_managed_object( + pg_ref, ['name', 'config.description', 'config.numPorts', + 'config.type', 'config.defaultPortConfig']) + pg_dict = {'name': props['name'], + 'description': props.get('config.description'), + 'num_ports': props['config.numPorts'], + 'type': props['config.type']} + if props['config.defaultPortConfig']: + dpg = props['config.defaultPortConfig'] + if dpg.vlan and \ + isinstance(dpg.vlan, + vim.VmwareDistributedVirtualSwitchVlanIdSpec): + + pg_dict.update({'vlan_id': dpg.vlan.vlanId}) + pg_dict.update({'out_shaping': + _get_dvportgroup_out_shaping( + props['name'], + props['config.defaultPortConfig'])}) + pg_dict.update({'security_policy': + _get_dvportgroup_security_policy( + props['name'], + props['config.defaultPortConfig'])}) + pg_dict.update({'teaming': + _get_dvportgroup_teaming( + props['name'], + props['config.defaultPortConfig'])}) + return pg_dict + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'esxcluster') +@gets_service_instance_via_proxy +def list_dvportgroups(dvs=None, portgroup_names=None, service_instance=None): + ''' + Returns a list of distributed virtual switch portgroups. + The list can be filtered by the portgroup names or by the DVS. + + dvs + Name of the DVS containing the portgroups. + Default value is None. + + portgroup_names + List of portgroup names to look for. If None, all portgroups are + returned. + Default value is None + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.list_dvporgroups + + salt '*' vsphere.list_dvportgroups dvs=dvs1 + + salt '*' vsphere.list_dvportgroups portgroup_names=[pg1] + + salt '*' vsphere.list_dvportgroups dvs=dvs1 portgroup_names=[pg1] + ''' + ret_dict = [] + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + if dvs: + dvs_refs = salt.utils.vmware.get_dvss(dc_ref, dvs_names=[dvs]) + if not dvs_refs: + raise VMwareObjectRetrievalError('DVS \'{0}\' was not ' + 'retrieved'.format(dvs)) + dvs_ref = dvs_refs[0] + get_all_portgroups = True if not portgroup_names else False + for pg_ref in salt.utils.vmware.get_dvportgroups( + parent_ref=dvs_ref if dvs else dc_ref, + portgroup_names=portgroup_names, + get_all_portgroups=get_all_portgroups): + + ret_dict.append(_get_dvportgroup_dict(pg_ref)) + return ret_dict + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 3753a1048985dadc69dd5fd00b5c8a6cc3fc62a7 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:11:22 -0400 Subject: [PATCH 172/633] Added salt.modules.vsphere.list_uplink_dvportgroup to list the dict representation of the uplink portgroup of a DVS --- salt/modules/vsphere.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index f3833b1a01..72a9d5ae0e 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4240,6 +4240,39 @@ def list_dvportgroups(dvs=None, portgroup_names=None, service_instance=None): return ret_dict +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'esxcluster') +@gets_service_instance_via_proxy +def list_uplink_dvportgroup(dvs, service_instance=None): + ''' + Returns the uplink portgroup of a distributed virtual switch. + + dvs + Name of the DVS containing the portgroup. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.list_uplink_dvportgroup dvs=dvs_name + ''' + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + dvs_refs = salt.utils.vmware.get_dvss(dc_ref, dvs_names=[dvs]) + if not dvs_refs: + raise VMwareObjectRetrievalError('DVS \'{0}\' was not ' + 'retrieved'.format(dvs)) + uplink_pg_ref = salt.utils.vmware.get_uplink_dvportgroup(dvs_refs[0]) + return _get_dvportgroup_dict(uplink_pg_ref) + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From e2fc69585e510e3eaafab8d414608ef09b58e18d Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:13:35 -0400 Subject: [PATCH 173/633] Added private functions to apply a DVPortgroup dict representation to a VMware spec object --- salt/modules/vsphere.py | 179 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 72a9d5ae0e..a84a1c9660 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4273,6 +4273,185 @@ def list_uplink_dvportgroup(dvs, service_instance=None): return _get_dvportgroup_dict(uplink_pg_ref) +def _apply_dvportgroup_out_shaping(pg_name, out_shaping, out_shaping_conf): + ''' + Applies the values in out_shaping_conf to an out_shaping object + + pg_name + The name of the portgroup + + out_shaping + The vim.DVSTrafficShapingPolicy to apply the config to + + out_shaping_conf + The out shaping config + ''' + log.trace('Building portgroup\'s \'{0}\' out shaping ' + 'policy'.format(pg_name)) + if out_shaping_conf.get('average_bandwidth'): + out_shaping.averageBandwidth = vim.LongPolicy() + out_shaping.averageBandwidth.value = \ + out_shaping_conf['average_bandwidth'] + if out_shaping_conf.get('burst_size'): + out_shaping.burstSize = vim.LongPolicy() + out_shaping.burstSize.value = out_shaping_conf['burst_size'] + if 'enabled' in out_shaping_conf: + out_shaping.enabled = vim.BoolPolicy() + out_shaping.enabled.value = out_shaping_conf['enabled'] + if out_shaping_conf.get('peak_bandwidth'): + out_shaping.peakBandwidth = vim.LongPolicy() + out_shaping.peakBandwidth.value = out_shaping_conf['peak_bandwidth'] + + +def _apply_dvportgroup_security_policy(pg_name, sec_policy, sec_policy_conf): + ''' + Applies the values in sec_policy_conf to a security policy object + + pg_name + The name of the portgroup + + sec_policy + The vim.DVSTrafficShapingPolicy to apply the config to + + sec_policy_conf + The out shaping config + ''' + log.trace('Building portgroup\'s \'{0}\' security policy '.format(pg_name)) + if 'allow_promiscuous' in sec_policy_conf: + sec_policy.allowPromiscuous = vim.BoolPolicy() + sec_policy.allowPromiscuous.value = \ + sec_policy_conf['allow_promiscuous'] + if 'forged_transmits' in sec_policy_conf: + sec_policy.forgedTransmits = vim.BoolPolicy() + sec_policy.forgedTransmits.value = sec_policy_conf['forged_transmits'] + if 'mac_changes' in sec_policy_conf: + sec_policy.macChanges = vim.BoolPolicy() + sec_policy.macChanges.value = sec_policy_conf['mac_changes'] + + +def _apply_dvportgroup_teaming(pg_name, teaming, teaming_conf): + ''' + Applies the values in teaming_conf to a teaming policy object + + pg_name + The name of the portgroup + + teaming + The vim.VmwareUplinkPortTeamingPolicy to apply the config to + + teaming_conf + The teaming config + ''' + log.trace('Building portgroup\'s \'{0}\' teaming'.format(pg_name)) + if 'notify_switches' in teaming_conf: + teaming.notifySwitches = vim.BoolPolicy() + teaming.notifySwitches.value = teaming_conf['notify_switches'] + if 'policy' in teaming_conf: + teaming.policy = vim.StringPolicy() + teaming.policy.value = teaming_conf['policy'] + if 'reverse_policy' in teaming_conf: + teaming.reversePolicy = vim.BoolPolicy() + teaming.reversePolicy.value = teaming_conf['reverse_policy'] + if 'rolling_order' in teaming_conf: + teaming.rollingOrder = vim.BoolPolicy() + teaming.rollingOrder.value = teaming_conf['rolling_order'] + if 'failure_criteria' in teaming_conf: + if not teaming.failureCriteria: + teaming.failureCriteria = vim.DVSFailureCriteria() + failure_criteria_conf = teaming_conf['failure_criteria'] + if 'check_beacon' in failure_criteria_conf: + teaming.failureCriteria.checkBeacon = vim.BoolPolicy() + teaming.failureCriteria.checkBeacon.value = \ + failure_criteria_conf['check_beacon'] + if 'check_duplex' in failure_criteria_conf: + teaming.failureCriteria.checkDuplex = vim.BoolPolicy() + teaming.failureCriteria.checkDuplex.value = \ + failure_criteria_conf['check_duplex'] + if 'check_error_percent' in failure_criteria_conf: + teaming.failureCriteria.checkErrorPercent = vim.BoolPolicy() + teaming.failureCriteria.checkErrorPercent.value = \ + failure_criteria_conf['check_error_percent'] + if 'check_speed' in failure_criteria_conf: + teaming.failureCriteria.checkSpeed = vim.StringPolicy() + teaming.failureCriteria.checkSpeed.value = \ + failure_criteria_conf['check_speed'] + if 'full_duplex' in failure_criteria_conf: + teaming.failureCriteria.fullDuplex = vim.BoolPolicy() + teaming.failureCriteria.fullDuplex.value = \ + failure_criteria_conf['full_duplex'] + if 'percentage' in failure_criteria_conf: + teaming.failureCriteria.percentage = vim.IntPolicy() + teaming.failureCriteria.percentage.value = \ + failure_criteria_conf['percentage'] + if 'speed' in failure_criteria_conf: + teaming.failureCriteria.speed = vim.IntPolicy() + teaming.failureCriteria.speed.value = \ + failure_criteria_conf['speed'] + if 'port_order' in teaming_conf: + if not teaming.uplinkPortOrder: + teaming.uplinkPortOrder = vim.VMwareUplinkPortOrderPolicy() + if 'active' in teaming_conf['port_order']: + teaming.uplinkPortOrder.activeUplinkPort = \ + teaming_conf['port_order']['active'] + if 'standby' in teaming_conf['port_order']: + teaming.uplinkPortOrder.standbyUplinkPort = \ + teaming_conf['port_order']['standby'] + + +def _apply_dvportgroup_config(pg_name, pg_spec, pg_conf): + ''' + Applies the values in conf to a distributed portgroup spec + + pg_name + The name of the portgroup + + pg_spec + The vim.DVPortgroupConfigSpec to apply the config to + + pg_conf + The portgroup config + ''' + log.trace('Building portgroup\'s \'{0}\' spec'.format(pg_name)) + if 'name' in pg_conf: + pg_spec.name = pg_conf['name'] + if 'description' in pg_conf: + pg_spec.description = pg_conf['description'] + if 'num_ports' in pg_conf: + pg_spec.numPorts = pg_conf['num_ports'] + if 'type' in pg_conf: + pg_spec.type = pg_conf['type'] + + if not pg_spec.defaultPortConfig: + for prop in ['vlan_id', 'out_shaping', 'security_policy', 'teaming']: + if prop in pg_conf: + pg_spec.defaultPortConfig = vim.VMwareDVSPortSetting() + if 'vlan_id' in pg_conf: + pg_spec.defaultPortConfig.vlan = \ + vim.VmwareDistributedVirtualSwitchVlanIdSpec() + pg_spec.defaultPortConfig.vlan.vlanId = pg_conf['vlan_id'] + if 'out_shaping' in pg_conf: + if not pg_spec.defaultPortConfig.outShapingPolicy: + pg_spec.defaultPortConfig.outShapingPolicy = \ + vim.DVSTrafficShapingPolicy() + _apply_dvportgroup_out_shaping( + pg_name, pg_spec.defaultPortConfig.outShapingPolicy, + pg_conf['out_shaping']) + if 'security_policy' in pg_conf: + if not pg_spec.defaultPortConfig.securityPolicy: + pg_spec.defaultPortConfig.securityPolicy = \ + vim.DVSSecurityPolicy() + _apply_dvportgroup_security_policy( + pg_name, pg_spec.defaultPortConfig.securityPolicy, + pg_conf['security_policy']) + if 'teaming' in pg_conf: + if not pg_spec.defaultPortConfig.uplinkTeamingPolicy: + pg_spec.defaultPortConfig.uplinkTeamingPolicy = \ + vim.VmwareUplinkPortTeamingPolicy() + _apply_dvportgroup_teaming( + pg_name, pg_spec.defaultPortConfig.uplinkTeamingPolicy, + pg_conf['teaming']) + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From b38f3255b743288cf86c10afe461533d3121dd42 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:15:33 -0400 Subject: [PATCH 174/633] Added salt.modules.vsphere.create_dvportgroup to create a DVPortgroup based on a dict representations --- salt/modules/vsphere.py | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index a84a1c9660..91f9870036 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4452,6 +4452,57 @@ def _apply_dvportgroup_config(pg_name, pg_spec, pg_conf): pg_conf['teaming']) +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'esxcluster') +@gets_service_instance_via_proxy +def create_dvportgroup(portgroup_dict, portgroup_name, dvs, + service_instance=None): + ''' + Creates a distributed virtual portgroup. + + Note: The ``portgroup_name`` param will override any name already set + in ``portgroup_dict``. + + portgroup_dict + Dictionary with the config values the portgroup should be created with + (exmaple in salt.states.dvs). + + portgroup_name + Name of the portgroup to be created. + + dvs + Name of the DVS that will contain the portgroup. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.create_dvportgroup portgroup_dict= + portgroup_name=pg1 dvs=dvs1 + ''' + log.trace('Creating portgroup\'{0}\' in dvs \'{1}\' ' + 'with dict = {2}'.format(portgroup_name, dvs, portgroup_dict)) + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + dvs_refs = salt.utils.vmware.get_dvss(dc_ref, dvs_names=[dvs]) + if not dvs_refs: + raise VMwareObjectRetrievalError('DVS \'{0}\' was not ' + 'retrieved'.format(dvs)) + # Make the name of the dvportgroup consistent with the parameter + portgroup_dict['name'] = portgroup_name + spec = vim.DVPortgroupConfigSpec() + _apply_dvportgroup_config(portgroup_name, spec, portgroup_dict) + salt.utils.vmware.create_dvportgroup(dvs_refs[0], spec) + return True + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 6e6756aa100a1bb0594babfd5388193d634583d1 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:17:50 -0400 Subject: [PATCH 175/633] Added salt.modules.vsphere.update_dvportgroup to update a DVPortgroup based on a dict representations --- salt/modules/vsphere.py | 60 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 91f9870036..3747ccf6b0 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4503,6 +4503,66 @@ def create_dvportgroup(portgroup_dict, portgroup_name, dvs, return True +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'esxcluster') +@gets_service_instance_via_proxy +def update_dvportgroup(portgroup_dict, portgroup, dvs, service_instance=True): + ''' + Updates a distributed virtual portgroup. + + portgroup_dict + Dictionary with the values the portgroup should be update with + (exmaple in salt.states.dvs). + + portgroup + Name of the portgroup to be updated. + + dvs + Name of the DVS containing the portgroups. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.update_dvportgroup portgroup_dict= + portgroup=pg1 + + salt '*' vsphere.update_dvportgroup portgroup_dict= + portgroup=pg1 dvs=dvs1 + ''' + log.trace('Updating portgroup\'{0}\' in dvs \'{1}\' ' + 'with dict = {2}'.format(portgroup, dvs, portgroup_dict)) + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + dvs_refs = salt.utils.vmware.get_dvss(dc_ref, dvs_names=[dvs]) + if not dvs_refs: + raise VMwareObjectRetrievalError('DVS \'{0}\' was not ' + 'retrieved'.format(dvs)) + pg_refs = salt.utils.vmware.get_dvportgroups(dvs_refs[0], + portgroup_names=[portgroup]) + if not pg_refs: + raise VMwareObjectRetrievalError('Portgroup \'{0}\' was not ' + 'retrieved'.format(portgroup)) + pg_props = salt.utils.vmware.get_properties_of_managed_object(pg_refs[0], + ['config']) + spec = vim.DVPortgroupConfigSpec() + # Copy existing properties in spec + for prop in ['autoExpand', 'configVersion', 'defaultPortConfig', + 'description', 'name', 'numPorts', 'policy', 'portNameFormat', + 'scope', 'type', 'vendorSpecificConfig']: + setattr(spec, prop, getattr(pg_props['config'], prop)) + _apply_dvportgroup_config(portgroup, spec, portgroup_dict) + salt.utils.vmware.update_dvportgroup(pg_refs[0], spec) + return True + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 0446c938dd0ea923859c6b811c07710326101890 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:19:21 -0400 Subject: [PATCH 176/633] Added salt.modules.vsphere.remove_dvportgroup to remove a DVPortgroup --- salt/modules/vsphere.py | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 3747ccf6b0..84edc69897 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4563,6 +4563,49 @@ def update_dvportgroup(portgroup_dict, portgroup, dvs, service_instance=True): return True +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'esxcluster') +@gets_service_instance_via_proxy +def remove_dvportgroup(portgroup, dvs, service_instance=None): + ''' + Removes a distributed virtual portgroup. + + portgroup + Name of the portgroup to be removed. + + dvs + Name of the DVS containing the portgroups. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.remove_dvportgroup portgroup=pg1 dvs=dvs1 + ''' + log.trace('Removing portgroup\'{0}\' in dvs \'{1}\' ' + ''.format(portgroup, dvs)) + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + dvs_refs = salt.utils.vmware.get_dvss(dc_ref, dvs_names=[dvs]) + if not dvs_refs: + raise VMwareObjectRetrievalError('DVS \'{0}\' was not ' + 'retrieved'.format(dvs)) + pg_refs = salt.utils.vmware.get_dvportgroups(dvs_refs[0], + portgroup_names=[portgroup]) + if not pg_refs: + raise VMwareObjectRetrievalError('Portgroup \'{0}\' was not ' + 'retrieved'.format(portgroup)) + salt.utils.vmware.remove_dvportgroup(pg_refs[0]) + return True + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From c83f471bffc0015238dbfefe1c5af4dfe6879590 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:21:38 -0400 Subject: [PATCH 177/633] Added comments and imports to dvs states --- salt/states/dvs.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 salt/states/dvs.py diff --git a/salt/states/dvs.py b/salt/states/dvs.py new file mode 100644 index 0000000000..d46bde966f --- /dev/null +++ b/salt/states/dvs.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +''' +Manage VMware distributed virtual switches (DVSs). + +Dependencies +============ + + +- pyVmomi Python Module + + +pyVmomi +------- + +PyVmomi can be installed via pip: + +.. code-block:: bash + + pip install pyVmomi + +.. note:: + + Version 6.0 of pyVmomi has some problems with SSL error handling on certain + versions of Python. If using version 6.0 of pyVmomi, Python 2.6, + Python 2.7.9, or newer must be present. This is due to an upstream dependency + in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the + version of Python is not in the supported range, you will need to install an + earlier version of pyVmomi. See `Issue #29537`_ for more information. + +.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 + +Based on the note above, to install an earlier version of pyVmomi than the +version currently listed in PyPi, run the following: + +.. code-block:: bash + + pip install pyVmomi==5.5.0.2014.1.1 + +The 5.5.0.2014.1.1 is a known stable version that this original ESXi State +Module was developed against. +''' + +# Import Python Libs +from __future__ import absolute_import +import logging +import traceback + +# Import Salt Libs +import salt.exceptions +from salt.utils.dictupdate import update as dict_merge +import salt.utils + +# Get Logging Started +log = logging.getLogger(__name__) + +def __virtual__(): + return True + + +def mod_init(low): + ''' + Init function + ''' + return True From 5b0d84208ad9c9c674c52d2c9ef957d16143c08d Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 06:51:19 -0400 Subject: [PATCH 178/633] Added sysdoc in states.dvs --- salt/states/dvs.py | 161 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/salt/states/dvs.py b/salt/states/dvs.py index d46bde966f..1423af1160 100644 --- a/salt/states/dvs.py +++ b/salt/states/dvs.py @@ -1,6 +1,165 @@ # -*- coding: utf-8 -*- ''' -Manage VMware distributed virtual switches (DVSs). +Manage VMware distributed virtual switches (DVSs) and their distributed virtual +portgroups (DVportgroups). + +Examples +======== + +Several settings can be changed for DVSs and DVporgroups. Here are two examples +covering all of the settings. Fewer settings can be used + +DVS +--- + +.. code-block:: python + + 'name': 'dvs1', + 'max_mtu': 1000, + 'uplink_names': [ + 'dvUplink1', + 'dvUplink2', + 'dvUplink3' + ], + 'capability': { + 'portgroup_operation_supported': false, + 'operation_supported': true, + 'port_operation_supported': false + }, + 'lacp_api_version': 'multipleLag', + 'contact_email': 'foo@email.com', + 'product_info': { + 'version': + '6.0.0', + 'vendor': + 'VMware, + Inc.', + 'name': + 'DVS' + }, + 'network_resource_management_enabled': true, + 'contact_name': 'me@email.com', + 'infrastructure_traffic_resource_pools': [ + { + 'reservation': 0, + 'limit': 1000, + 'share_level': 'high', + 'key': 'management', + 'num_shares': 100 + }, + { + 'reservation': 0, + 'limit': -1, + 'share_level': 'normal', + 'key': 'faultTolerance', + 'num_shares': 50 + }, + { + 'reservation': 0, + 'limit': 32000, + 'share_level': 'normal', + 'key': 'vmotion', + 'num_shares': 50 + }, + { + 'reservation': 10000, + 'limit': -1, + 'share_level': 'normal', + 'key': 'virtualMachine', + 'num_shares': 50 + }, + { + 'reservation': 0, + 'limit': -1, + 'share_level': 'custom', + 'key': 'iSCSI', + 'num_shares': 75 + }, + { + 'reservation': 0, + 'limit': -1, + 'share_level': 'normal', + 'key': 'nfs', + 'num_shares': 50 + }, + { + 'reservation': 0, + 'limit': -1, + 'share_level': 'normal', + 'key': 'hbr', + 'num_shares': 50 + }, + { + 'reservation': 8750, + 'limit': 15000, + 'share_level': 'high', + 'key': 'vsan', + 'num_shares': 100 + }, + { + 'reservation': 0, + 'limit': -1, + 'share_level': 'normal', + 'key': 'vdp', + 'num_shares': 50 + } + ], + 'link_discovery_protocol': { + 'operation': + 'listen', + 'protocol': + 'cdp' + }, + 'network_resource_control_version': 'version3', + 'description': 'Managed by Salt. Random settings.' + +Note: The mandatory attribute is: ``name``. + +Portgroup +--------- + +.. code-block:: python + 'security_policy': { + 'allow_promiscuous': true, + 'mac_changes': false, + 'forged_transmits': true + }, + 'name': 'vmotion-v702', + 'out_shaping': { + 'enabled': true, + 'average_bandwidth': 1500, + 'burst_size': 4096, + 'peak_bandwidth': 1500 + }, + 'num_ports': 128, + 'teaming': { + 'port_order': { + 'active': [ + 'dvUplink2' + ], + 'standby': [ + 'dvUplink1' + ] + }, + 'notify_switches': false, + 'reverse_policy': true, + 'rolling_order': false, + 'policy': 'failover_explicit', + 'failure_criteria': { + 'check_error_percent': true, + 'full_duplex': false, + 'check_duplex': false, + 'percentage': 50, + 'check_speed': 'minimum', + 'speed': 20, + 'check_beacon': true + } + }, + 'type': 'earlyBinding', + 'vlan_id': 100, + 'description': 'Managed by Salt. Random settings.' + +Note: The mandatory attributes are: ``name``, ``type``. Dependencies ============ From 8e56702598455bb2ea1350dd7065efcb4f6d52f3 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:23:14 -0400 Subject: [PATCH 179/633] Added dvs_configured state that configures/adds a DVS --- salt/states/dvs.py | 168 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/salt/states/dvs.py b/salt/states/dvs.py index 1423af1160..39632769b4 100644 --- a/salt/states/dvs.py +++ b/salt/states/dvs.py @@ -221,3 +221,171 @@ def mod_init(low): Init function ''' return True + + +def _get_datacenter_name(): + ''' + Returns the datacenter name configured on the proxy + + Supported proxies: esxcluster, esxdatacenter + ''' + + proxy_type = __salt__['vsphere.get_proxy_type']() + details = None + if proxy_type == 'esxcluster': + details = __salt__['esxcluster.get_details']() + elif proxy_type == 'esxdatacenter': + details = __salt__['esxdatacenter.get_details']() + if not details: + raise salt.exceptions.CommandExecutionError( + 'details for proxy type \'{0}\' not loaded'.format(proxy_type)) + return details['datacenter'] + + +def dvs_configured(name, dvs): + ''' + Configures a DVS. + + Creates a new DVS, if it doesn't exist in the provided datacenter or + reconfigures it if configured differently. + + dvs + DVS dict representations (see module sysdocs) + ''' + datacenter_name = _get_datacenter_name() + dvs_name = dvs['name'] if dvs.get('name') else name + log.info('Running state {0} for DVS \'{1}\' in datacenter ' + '\'{2}\''.format(name, dvs_name, datacenter_name)) + changes_required = False + ret = {'name': name, 'changes': {}, 'result': None, 'comment': None} + comments = [] + changes = {} + changes_required = False + + try: + #TODO dvs validation + si = __salt__['vsphere.get_service_instance_via_proxy']() + dvss = __salt__['vsphere.list_dvss'](dvs_names=[dvs_name], + service_instance=si) + if not dvss: + changes_required = True + if __opts__['test']: + comments.append('State {0} will create a new DVS ' + '\'{1}\' in datacenter \'{2}\'' + ''.format(name, dvs_name, datacenter_name)) + log.info(comments[-1]) + else: + dvs['name'] = dvs_name + __salt__['vsphere.create_dvs'](dvs_dict=dvs, + dvs_name=dvs_name, + service_instance=si) + comments.append('Created a new DVS \'{0}\' in datacenter ' + '\'{1}\''.format(dvs_name, datacenter_name)) + log.info(comments[-1]) + changes.update({'dvs': {'new': dvs}}) + else: + # DVS already exists. Checking various aspects of the config + props = ['description', 'contact_email', 'contact_name', + 'lacp_api_version', 'link_discovery_protocol', + 'max_mtu', 'network_resource_control_version', + 'network_resource_management_enabled'] + log.trace('DVS \'{0}\' found in datacenter \'{1}\'. Checking ' + 'for any updates in ' + '{2}'.format(dvs_name, datacenter_name, props)) + props_to_original_values = {} + props_to_updated_values = {} + current_dvs = dvss[0] + for prop in props: + if prop in dvs and dvs[prop] != current_dvs.get(prop): + props_to_original_values[prop] = current_dvs.get(prop) + props_to_updated_values[prop] = dvs[prop] + + # Simple infrastructure traffic resource control compare doesn't + # work because num_shares is optional if share_level is not custom + # We need to do a dedicated compare for this property + infra_prop = 'infrastructure_traffic_resource_pools' + original_infra_res_pools = [] + updated_infra_res_pools = [] + if infra_prop in dvs: + if not current_dvs.get(infra_prop): + updated_infra_res_pools = dvs[infra_prop] + else: + for idx in range(len(dvs[infra_prop])): + if 'num_shares' not in dvs[infra_prop][idx] and \ + current_dvs[infra_prop][idx]['share_level'] != \ + 'custom' and \ + 'num_shares' in current_dvs[infra_prop][idx]: + + del current_dvs[infra_prop][idx]['num_shares'] + if dvs[infra_prop][idx] != \ + current_dvs[infra_prop][idx]: + + original_infra_res_pools.append( + current_dvs[infra_prop][idx]) + updated_infra_res_pools.append( + dict(dvs[infra_prop][idx])) + if updated_infra_res_pools: + props_to_original_values[ + 'infrastructure_traffic_resource_pools'] = \ + original_infra_res_pools + props_to_updated_values[ + 'infrastructure_traffic_resource_pools'] = \ + updated_infra_res_pools + if props_to_updated_values: + if __opts__['test']: + changes_string = '' + for p in props_to_updated_values.keys(): + if p == 'infrastructure_traffic_resource_pools': + changes_string += \ + '\tinfrastructure_traffic_resource_pools:\n' + for idx in range(len(props_to_updated_values [p])): + d = props_to_updated_values[p][idx] + s = props_to_original_values[p][idx] + changes_string += \ + ('\t\t{0} from \'{1}\' to \'{2}\'\n' + ''.format(d['key'], s, d)) + else: + changes_string += \ + ('\t{0} from \'{1}\' to \'{2}\'\n' + ''.format(p, props_to_original_values[p], + props_to_updated_values[p])) + comments.append( + 'State dvs_configured will update DVS \'{0}\' ' + 'in datacenter \'{1}\':\n{2}' + ''.format(dvs_name, datacenter_name, changes_string)) + log.info(comments[-1]) + else: + __salt__['vsphere.update_dvs']( + dvs_dict=props_to_updated_values, + dvs=dvs_name, + service_instance=si) + comments.append('Updated DVS \'{0}\' in datacenter \'{1}\'' + ''.format(dvs_name, datacenter_name)) + log.info(comments[-1]) + changes.update({'dvs': {'new': props_to_updated_values, + 'old': props_to_original_values}}) + __salt__['vsphere.disconnect'](si) + except salt.exceptions.CommandExecutionError as exc: + log.error('Error: {0}\n{1}'.format(exc, traceback.format_exc())) + if si: + __salt__['vsphere.disconnect'](si) + if not __opts__['test']: + ret['result'] = False + ret.update({'comment': str(exc), + 'result': False if not __opts__['test'] else None}) + return ret + if not comments: + # We have no changes + ret.update({'comment': ('DVS \'{0}\' in datacenter \'{1}\' is ' + 'correctly configured. Nothing to be done.' + ''.format(dvs_name, datacenter_name)), + 'result': True}) + else: + ret.update({'comment': '\n'.join(comments)}) + if __opts__['test']: + ret.update({'pchanges': changes, + 'result': None}) + else: + ret.update({'changes': changes, + 'result': True}) + return ret From 903b8a989576c9f4441e7d89c00c33c7edc960cc Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:24:52 -0400 Subject: [PATCH 180/633] Added portgroups_configured state that configures/adds/removes DVPortgroups --- salt/states/dvs.py | 222 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/salt/states/dvs.py b/salt/states/dvs.py index 39632769b4..193557ed1a 100644 --- a/salt/states/dvs.py +++ b/salt/states/dvs.py @@ -389,3 +389,225 @@ def dvs_configured(name, dvs): ret.update({'changes': changes, 'result': True}) return ret + + +def _get_diff_dict(dict1, dict2): + ''' + Returns a dictionary with the diffs between two dictionaries + + It will ignore any key that doesn't exist in dict2 + ''' + ret_dict = {} + for p in dict2.keys(): + if p not in dict1: + ret_dict.update({p: {'val1': None, 'val2': dict2[p]}}) + elif dict1[p] != dict2[p]: + if isinstance(dict1[p], dict) and isinstance(dict2[p], dict): + sub_diff_dict = _get_diff_dict(dict1[p], dict2[p]) + if sub_diff_dict: + ret_dict.update({p: sub_diff_dict}) + else: + ret_dict.update({p: {'val1': dict1[p], 'val2': dict2[p]}}) + return ret_dict + + +def _get_val2_dict_from_diff_dict(diff_dict): + ''' + Returns a dictionaries with the values stored in val2 of a diff dict. + ''' + ret_dict = {} + for p in diff_dict.keys(): + if not isinstance(diff_dict[p], dict): + raise ValueError('Unexpected diff difct \'{0}\''.format(diff_dict)) + if 'val2' in diff_dict[p].keys(): + ret_dict.update({p: diff_dict[p]['val2']}) + else: + ret_dict.update( + {p: _get_val2_dict_from_diff_dict(diff_dict[p])}) + return ret_dict + + +def _get_val1_dict_from_diff_dict(diff_dict): + ''' + Returns a dictionaries with the values stored in val1 of a diff dict. + ''' + ret_dict = {} + for p in diff_dict.keys(): + if not isinstance(diff_dict[p], dict): + raise ValueError('Unexpected diff difct \'{0}\''.format(diff_dict)) + if 'val1' in diff_dict[p].keys(): + ret_dict.update({p: diff_dict[p]['val1']}) + else: + ret_dict.update( + {p: _get_val1_dict_from_diff_dict(diff_dict[p])}) + return ret_dict + + +def _get_changes_from_diff_dict(diff_dict): + ''' + Returns a list of string message of the differences in a diff dict. + + Each inner message is tabulated one tab deeper + ''' + changes_strings = [] + for p in diff_dict.keys(): + if not isinstance(diff_dict[p], dict): + raise ValueError('Unexpected diff difct \'{0}\''.format(diff_dict)) + if sorted(diff_dict[p].keys()) == ['val1', 'val2']: + # Some string formatting + from_str = diff_dict[p]['val1'] + if isinstance(diff_dict[p]['val1'], str): + from_str = '\'{0}\''.format(diff_dict[p]['val1']) + elif isinstance(diff_dict[p]['val1'], list): + from_str = '\'{0}\''.format(', '.join(diff_dict[p]['val1'])) + to_str = diff_dict[p]['val2'] + if isinstance(diff_dict[p]['val2'], str): + to_str = '\'{0}\''.format(diff_dict[p]['val2']) + elif isinstance(diff_dict[p]['val2'], list): + to_str = '\'{0}\''.format(', '.join(diff_dict[p]['val2'])) + changes_strings.append('{0} from {1} to {2}'.format( + p, from_str, to_str)) + else: + sub_changes = _get_changes_from_diff_dict(diff_dict[p]) + if sub_changes: + changes_strings.append('{0}:'.format(p)) + changes_strings.extend(['\t{0}'.format(c) + for c in sub_changes]) + return changes_strings + + +def portgroups_configured(name, dvs, portgroups): + ''' + Configures portgroups on a DVS. + + Creates/updates/removes portgroups in a provided DVS + + dvs + Name of the DVS + + portgroups + Portgroup dict representations (see module sysdocs) + ''' + datacenter = _get_datacenter_name() + log.info('Running state {0} on DVS \'{1}\', datacenter ' + '\'{2}\''.format(name, dvs, datacenter)) + changes_required = False + ret = {'name': name, 'changes': {}, 'result': None, 'comment': None, + 'pchanges': {}} + comments = [] + changes = {} + changes_required = False + + try: + #TODO portroups validation + si = __salt__['vsphere.get_service_instance_via_proxy']() + current_pgs = __salt__['vsphere.list_dvportgroups']( + dvs=dvs, service_instance=si) + expected_pg_names = [] + for pg in portgroups: + pg_name = pg['name'] + expected_pg_names.append(pg_name) + del pg['name'] + log.info('Checking pg \'{0}\''.format(pg_name)) + filtered_current_pgs = \ + [p for p in current_pgs if p.get('name') == pg_name] + if not filtered_current_pgs: + changes_required = True + if __opts__['test']: + comments.append('State {0} will create a new portgroup ' + '\'{1}\' in DVS \'{2}\', datacenter ' + '\'{3}\''.format(name, pg_name, dvs, + datacenter)) + else: + __salt__['vsphere.create_dvportgroup']( + portgroup_dict=pg, portgroup_name=pg_name, dvs=dvs, + service_instance=si) + comments.append('Created a new portgroup \'{0}\' in DVS ' + '\'{1}\', datacenter \'{2}\'' + ''.format(pg_name, dvs, datacenter)) + log.info(comments[-1]) + changes.update({pg_name: {'new': pg}}) + else: + # Porgroup already exists. Checking the config + log.trace('Portgroup \'{0}\' found in DVS \'{1}\', datacenter ' + '\'{2}\'. Checking for any updates.' + ''.format(pg_name, dvs, datacenter)) + current_pg = filtered_current_pgs[0] + diff_dict = _get_diff_dict(current_pg, pg) + + if diff_dict: + changes_required=True + if __opts__['test']: + changes_strings = \ + _get_changes_from_diff_dict(diff_dict) + log.trace('changes_strings = ' + '{0}'.format(changes_strings)) + comments.append( + 'State {0} will update portgroup \'{1}\' in ' + 'DVS \'{2}\', datacenter \'{3}\':\n{4}' + ''.format(name, pg_name, dvs, datacenter, + '\n'.join(['\t{0}'.format(c) for c in + changes_strings]))) + else: + __salt__['vsphere.update_dvportgroup']( + portgroup_dict=pg, portgroup=pg_name, dvs=dvs, + service_instance=si) + comments.append('Updated portgroup \'{0}\' in DVS ' + '\'{1}\', datacenter \'{2}\'' + ''.format(pg_name, dvs, datacenter)) + log.info(comments[-1]) + changes.update( + {pg_name: {'new': + _get_val2_dict_from_diff_dict(diff_dict), + 'old': + _get_val1_dict_from_diff_dict(diff_dict)}}) + # Add the uplink portgroup to the expected pg names + uplink_pg = __salt__['vsphere.list_uplink_dvportgroup']( + dvs=dvs, service_instance=si) + expected_pg_names.append(uplink_pg['name']) + # Remove any extra portgroups + for current_pg in current_pgs: + if current_pg['name'] not in expected_pg_names: + changes_required=True + if __opts__['test']: + comments.append('State {0} will remove ' + 'the portgroup \'{1}\' from DVS \'{2}\', ' + 'datacenter \'{3}\'' + ''.format(name, current_pg['name'], dvs, + datacenter)) + else: + __salt__['vsphere.remove_dvportgroup']( + portgroup=current_pg['name'], dvs=dvs, + service_instance=si) + comments.append('Removed the portgroup \'{0}\' from DVS ' + '\'{1}\', datacenter \'{2}\'' + ''.format(current_pg['name'], dvs, + datacenter)) + log.info(comments[-1]) + changes.update({current_pg['name']: + {'old': current_pg}}) + __salt__['vsphere.disconnect'](si) + except salt.exceptions.CommandExecutionError as exc: + log.error('Error: {0}\n{1}'.format(exc, traceback.format_exc())) + if si: + __salt__['vsphere.disconnect'](si) + if not __opts__['test']: + ret['result'] = False + ret.update({'comment': exc.strerror, + 'result': False if not __opts__['test'] else None}) + return ret + if not changes_required: + # We have no changes + ret.update({'comment': ('All portgroups in DVS \'{0}\', datacenter ' + '\'{1}\' exist and are correctly configured. ' + 'Nothing to be done.'.format(dvs, datacenter)), + 'result': True}) + else: + ret.update({'comment': '\n'.join(comments)}) + if __opts__['test']: + ret.update({'pchanges': changes, + 'result': None}) + else: + ret.update({'changes': changes, + 'result': True}) + return ret From 6b66fd75ae3b865ccf19474abcd7b422f958bff1 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 18 Sep 2017 19:25:49 -0400 Subject: [PATCH 181/633] Added uplink_portgroup_configured state that configures the uplink portgroup of a DVS --- salt/states/dvs.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/salt/states/dvs.py b/salt/states/dvs.py index 193557ed1a..897d5edebf 100644 --- a/salt/states/dvs.py +++ b/salt/states/dvs.py @@ -611,3 +611,88 @@ def portgroups_configured(name, dvs, portgroups): ret.update({'changes': changes, 'result': True}) return ret + + +def uplink_portgroup_configured(name, dvs, uplink_portgroup): + ''' + Configures the uplink portgroup on a DVS. The state assumes there is only + one uplink portgroup. + + dvs + Name of the DVS + + upling_portgroup + Uplink portgroup dict representations (see module sysdocs) + + ''' + datacenter = _get_datacenter_name() + log.info('Running {0} on DVS \'{1}\', datacenter \'{2}\'' + ''.format(name, dvs, datacenter)) + changes_required = False + ret = {'name': name, 'changes': {}, 'result': None, 'comment': None, + 'pchanges': {}} + comments = [] + changes = {} + changes_required = False + + try: + #TODO portroups validation + si = __salt__['vsphere.get_service_instance_via_proxy']() + current_uplink_portgroup = __salt__['vsphere.list_uplink_dvportgroup']( + dvs=dvs, service_instance=si) + log.trace('current_uplink_portgroup = ' + '{0}'.format(current_uplink_portgroup)) + diff_dict = _get_diff_dict(current_uplink_portgroup, uplink_portgroup) + if diff_dict: + changes_required=True + if __opts__['test']: + changes_strings = \ + _get_changes_from_diff_dict(diff_dict) + log.trace('changes_strings = ' + '{0}'.format(changes_strings)) + comments.append( + 'State {0} will update the ' + 'uplink portgroup in DVS \'{1}\', datacenter ' + '\'{2}\':\n{3}' + ''.format(name, dvs, datacenter, + '\n'.join(['\t{0}'.format(c) for c in + changes_strings]))) + else: + __salt__['vsphere.update_dvportgroup']( + portgroup_dict=uplink_portgroup, + portgroup=current_uplink_portgroup['name'], + dvs=dvs, + service_instance=si) + comments.append('Updated the uplink portgroup in DVS ' + '\'{0}\', datacenter \'{1}\'' + ''.format(dvs, datacenter)) + log.info(comments[-1]) + changes.update( + {'uplink_portgroup': + {'new': _get_val2_dict_from_diff_dict(diff_dict), + 'old': _get_val1_dict_from_diff_dict(diff_dict)}}) + __salt__['vsphere.disconnect'](si) + except salt.exceptions.CommandExecutionError as exc: + log.error('Error: {0}\n{1}'.format(exc, traceback.format_exc())) + if si: + __salt__['vsphere.disconnect'](si) + if not __opts__['test']: + ret['result'] = False + ret.update({'comment': exc.strerror, + 'result': False if not __opts__['test'] else None}) + return ret + if not changes_required: + # We have no changes + ret.update({'comment': ('Uplink portgroup in DVS \'{0}\', datacenter ' + '\'{1}\' is correctly configured. ' + 'Nothing to be done.'.format(dvs, datacenter)), + 'result': True}) + else: + ret.update({'comment': '\n'.join(comments)}) + if __opts__['test']: + ret.update({'pchanges': changes, + 'result': None}) + else: + ret.update({'changes': changes, + 'result': True}) + return ret From f811523e80973ff4e3ee90b1db80d6b780247db7 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 06:01:25 -0400 Subject: [PATCH 182/633] pylint --- salt/modules/vsphere.py | 9 +++++---- salt/states/dvs.py | 22 +++++++++++----------- salt/utils/vmware.py | 2 +- tests/unit/utils/vmware/test_dvs.py | 3 +-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 84edc69897..b3a8064153 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -3893,7 +3893,7 @@ def _apply_dvs_infrastructure_traffic_resources(infra_traffic_resources, if res_dict.get('num_shares'): #XXX Even though we always set the number of shares if provided, #the vCenter will ignore it unless the share level is 'custom'. - res.allocationInfo.shares.shares=res_dict['num_shares'] + res.allocationInfo.shares.shares = res_dict['num_shares'] def _apply_dvs_network_resource_pools(network_resource_pools, resource_dicts): @@ -3917,7 +3917,7 @@ def _apply_dvs_network_resource_pools(network_resource_pools, resource_dicts): if res_dict.get('num_shares') and res_dict.get('share_level'): if not res.allocationInfo.shares: res.allocationInfo.shares = vim.SharesInfo() - res.allocationInfo.shares.shares=res_dict['num_shares'] + res.allocationInfo.shares.shares = res_dict['num_shares'] res.allocationInfo.shares.level = \ vim.SharesLevel(res_dict['share_level']) @@ -3985,7 +3985,7 @@ def create_dvs(dvs_dict, dvs_name, service_instance=None): dvs_refs = salt.utils.vmware.get_dvss(dc_ref, dvs_names=[dvs_name]) if not dvs_refs: - raise excs.VMwareObjectRetrievalError( + raise VMwareObjectRetrievalError( 'DVS \'{0}\' wasn\'t found in datacenter \'{1}\'' ''.format(dvs_name, datacenter)) dvs_ref = dvs_refs[0] @@ -3993,6 +3993,7 @@ def create_dvs(dvs_dict, dvs_name, service_instance=None): dvs_ref, dvs_dict['network_resource_management_enabled']) return True + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy @@ -4394,7 +4395,7 @@ def _apply_dvportgroup_teaming(pg_name, teaming, teaming_conf): teaming.uplinkPortOrder.activeUplinkPort = \ teaming_conf['port_order']['active'] if 'standby' in teaming_conf['port_order']: - teaming.uplinkPortOrder.standbyUplinkPort = \ + teaming.uplinkPortOrder.standbyUplinkPort = \ teaming_conf['port_order']['standby'] diff --git a/salt/states/dvs.py b/salt/states/dvs.py index 897d5edebf..b48ab74f87 100644 --- a/salt/states/dvs.py +++ b/salt/states/dvs.py @@ -206,12 +206,12 @@ import traceback # Import Salt Libs import salt.exceptions -from salt.utils.dictupdate import update as dict_merge import salt.utils # Get Logging Started log = logging.getLogger(__name__) + def __virtual__(): return True @@ -285,10 +285,10 @@ def dvs_configured(name, dvs): changes.update({'dvs': {'new': dvs}}) else: # DVS already exists. Checking various aspects of the config - props = ['description', 'contact_email', 'contact_name', - 'lacp_api_version', 'link_discovery_protocol', - 'max_mtu', 'network_resource_control_version', - 'network_resource_management_enabled'] + props = ['description', 'contact_email', 'contact_name', + 'lacp_api_version', 'link_discovery_protocol', + 'max_mtu', 'network_resource_control_version', + 'network_resource_management_enabled'] log.trace('DVS \'{0}\' found in datacenter \'{1}\'. Checking ' 'for any updates in ' '{2}'.format(dvs_name, datacenter_name, props)) @@ -334,11 +334,11 @@ def dvs_configured(name, dvs): if props_to_updated_values: if __opts__['test']: changes_string = '' - for p in props_to_updated_values.keys(): + for p in props_to_updated_values: if p == 'infrastructure_traffic_resource_pools': changes_string += \ '\tinfrastructure_traffic_resource_pools:\n' - for idx in range(len(props_to_updated_values [p])): + for idx in range(len(props_to_updated_values[p])): d = props_to_updated_values[p][idx] s = props_to_original_values[p][idx] changes_string += \ @@ -536,7 +536,7 @@ def portgroups_configured(name, dvs, portgroups): diff_dict = _get_diff_dict(current_pg, pg) if diff_dict: - changes_required=True + changes_required = True if __opts__['test']: changes_strings = \ _get_changes_from_diff_dict(diff_dict) @@ -545,7 +545,7 @@ def portgroups_configured(name, dvs, portgroups): comments.append( 'State {0} will update portgroup \'{1}\' in ' 'DVS \'{2}\', datacenter \'{3}\':\n{4}' - ''.format(name, pg_name, dvs, datacenter, + ''.format(name, pg_name, dvs, datacenter, '\n'.join(['\t{0}'.format(c) for c in changes_strings]))) else: @@ -568,7 +568,7 @@ def portgroups_configured(name, dvs, portgroups): # Remove any extra portgroups for current_pg in current_pgs: if current_pg['name'] not in expected_pg_names: - changes_required=True + changes_required = True if __opts__['test']: comments.append('State {0} will remove ' 'the portgroup \'{1}\' from DVS \'{2}\', ' @@ -644,7 +644,7 @@ def uplink_portgroup_configured(name, dvs, uplink_portgroup): '{0}'.format(current_uplink_portgroup)) diff_dict = _get_diff_dict(current_uplink_portgroup, uplink_portgroup) if diff_dict: - changes_required=True + changes_required = True if __opts__['test']: changes_strings = \ _get_changes_from_diff_dict(diff_dict) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 27b728ca69..d54dbced04 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1177,7 +1177,7 @@ def get_dvportgroups(parent_ref, portgroup_names=None, path='childEntity', skip=False, type=vim.Folder)]) - else: # parent is distributed virtual switch + else: # parent is distributed virtual switch traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( path='portgroup', skip=False, diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index 6f88484877..3f2f493f5a 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -11,8 +11,7 @@ import logging # Import Salt testing libraries from tests.support.unit import TestCase, skipIf -from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, call, \ - PropertyMock +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, call from salt.exceptions import VMwareObjectRetrievalError, VMwareApiError, \ ArgumentValueError, VMwareRuntimeError From 8d80dc328a6b0e1f57b57305779f0c97ce006369 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 08:04:07 -0400 Subject: [PATCH 183/633] more pylint --- salt/states/dvs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/states/dvs.py b/salt/states/dvs.py index b48ab74f87..da4ba01209 100644 --- a/salt/states/dvs.py +++ b/salt/states/dvs.py @@ -3,6 +3,8 @@ Manage VMware distributed virtual switches (DVSs) and their distributed virtual portgroups (DVportgroups). +:codeauthor: :email:`Alexandru Bleotu ` + Examples ======== @@ -206,14 +208,13 @@ import traceback # Import Salt Libs import salt.exceptions -import salt.utils # Get Logging Started log = logging.getLogger(__name__) def __virtual__(): - return True + return 'dvs' def mod_init(low): From c65358d4fa84a806a7e78590ec90acf70f0ada75 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 18:02:02 -0400 Subject: [PATCH 184/633] Imported range from six --- salt/states/dvs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/states/dvs.py b/salt/states/dvs.py index da4ba01209..eeeae446f5 100644 --- a/salt/states/dvs.py +++ b/salt/states/dvs.py @@ -208,6 +208,7 @@ import traceback # Import Salt Libs import salt.exceptions +from salt.ext.six.moves import range # Get Logging Started log = logging.getLogger(__name__) From 3c7c202216124f7a6ff1019f1c8fc0e8ae291a87 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 21 Sep 2017 06:52:18 -0400 Subject: [PATCH 185/633] Fixed assert_has_calls in vmware.utils.dvs tests --- tests/unit/utils/vmware/test_dvs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/utils/vmware/test_dvs.py b/tests/unit/utils/vmware/test_dvs.py index 3f2f493f5a..458e240e28 100644 --- a/tests/unit/utils/vmware/test_dvs.py +++ b/tests/unit/utils/vmware/test_dvs.py @@ -80,10 +80,10 @@ class GetDvssTestCase(TestCase): mock_traversal_spec): vmware.get_dvss(self.mock_dc_ref) - mock_traversal_spec.assert_called( - call(path='networkFolder', skip=True, type=vim.Datacenter, - selectSet=['traversal_spec']), - call(path='childEntity', skip=False, type=vim.Folder)) + mock_traversal_spec.assert_has_calls( + [call(path='childEntity', skip=False, type=vim.Folder), + call(path='networkFolder', skip=True, type=vim.Datacenter, + selectSet=['traversal_spec'])]) def test_get_mors_with_properties(self): vmware.get_dvss(self.mock_dc_ref) @@ -467,10 +467,10 @@ class GetDvportgroupsTestCase(TestCase): mock_traversal_spec): vmware.get_dvportgroups(self.mock_dc_ref) - mock_traversal_spec.assert_called( - call(path='networkFolder', skip=True, type=vim.Datacenter, - selectSet=['traversal_spec']), - call(path='childEntity', skip=False, type=vim.Folder)) + mock_traversal_spec.assert_has_calls( + [call(path='childEntity', skip=False, type=vim.Folder), + call(path='networkFolder', skip=True, type=vim.Datacenter, + selectSet=['traversal_spec'])]) def test_traversal_spec_dvs_parent(self): mock_traversal_spec = MagicMock(return_value='traversal_spec') From f0a813b12660639b234e7a3d77010253e6f3839d Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 21 Sep 2017 15:46:38 -0400 Subject: [PATCH 186/633] Review changes --- salt/modules/vsphere.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index b3a8064153..bde7c9c98e 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -3749,7 +3749,7 @@ def list_dvss(datacenter=None, dvs_names=None, service_instance=None): salt '*' vsphere.list_dvss dvs_names=[dvs1,dvs2] ''' - ret_dict = [] + ret_list = [] proxy_type = get_proxy_type() if proxy_type == 'esxdatacenter': datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] @@ -3789,8 +3789,8 @@ def list_dvss(datacenter=None, dvs_names=None, service_instance=None): _get_dvs_infrastructure_traffic_resources( props['name'], props['config'].infrastructureTrafficResourceConfig)}) - ret_dict.append(dvs_dict) - return ret_dict + ret_list.append(dvs_dict) + return ret_list def _apply_dvs_config(config_spec, config_dict): @@ -3871,29 +3871,30 @@ def _apply_dvs_infrastructure_traffic_resources(infra_traffic_resources, (vim.DistributedVirtualSwitchProductSpec) ''' for res_dict in resource_dicts: - ress = [r for r in infra_traffic_resources if r.key == res_dict['key']] - if ress: - res = ress[0] + filtered_traffic_resources = \ + [r for r in infra_traffic_resources if r.key == res_dict['key']] + if filtered_traffic_resources: + traffic_res = filtered_traffic_resources[0] else: - res = vim.DvsHostInfrastructureTrafficResource() - res.key = res_dict['key'] - res.allocationInfo = \ + traffic_res = vim.DvsHostInfrastructureTrafficResource() + traffic_res.key = res_dict['key'] + traffic_res.allocationInfo = \ vim.DvsHostInfrastructureTrafficResourceAllocation() - infra_traffic_resources.append(res) + infra_traffic_resources.append(traffic_res) if res_dict.get('limit'): - res.allocationInfo.limit = res_dict['limit'] + traffic_res.allocationInfo.limit = res_dict['limit'] if res_dict.get('reservation'): - res.allocationInfo.reservation = res_dict['reservation'] + traffic_res.allocationInfo.reservation = res_dict['reservation'] if res_dict.get('num_shares') or res_dict.get('share_level'): - if not res.allocationInfo.shares: - res.allocationInfo.shares = vim.SharesInfo() + if not traffic_res.allocationInfo.shares: + traffic_res.allocationInfo.shares = vim.SharesInfo() if res_dict.get('share_level'): - res.allocationInfo.shares.level = \ + traffic_res.allocationInfo.shares.level = \ vim.SharesLevel(res_dict['share_level']) if res_dict.get('num_shares'): #XXX Even though we always set the number of shares if provided, #the vCenter will ignore it unless the share level is 'custom'. - res.allocationInfo.shares.shares = res_dict['num_shares'] + traffic_res.allocationInfo.shares.shares = res_dict['num_shares'] def _apply_dvs_network_resource_pools(network_resource_pools, resource_dicts): From c1d3bda729b5863f87a662d47b40037e65d4bd02 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 22 Sep 2017 14:49:47 -0400 Subject: [PATCH 187/633] Added python/pyvmomi compatibility check to salt.modules.vsphere + removed reference to Python 2.6 --- salt/states/dvs.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/salt/states/dvs.py b/salt/states/dvs.py index eeeae446f5..6b44a84c38 100644 --- a/salt/states/dvs.py +++ b/salt/states/dvs.py @@ -182,8 +182,8 @@ PyVmomi can be installed via pip: .. note:: Version 6.0 of pyVmomi has some problems with SSL error handling on certain - versions of Python. If using version 6.0 of pyVmomi, Python 2.6, - Python 2.7.9, or newer must be present. This is due to an upstream dependency + versions of Python. If using version 6.0 of pyVmomi, Python 2.7.9, + or newer must be present. This is due to an upstream dependency in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the version of Python is not in the supported range, you will need to install an earlier version of pyVmomi. See `Issue #29537`_ for more information. @@ -205,16 +205,33 @@ Module was developed against. from __future__ import absolute_import import logging import traceback +import sys # Import Salt Libs import salt.exceptions from salt.ext.six.moves import range +# Import Third Party Libs +try: + from pyVmomi import VmomiSupport + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + # Get Logging Started log = logging.getLogger(__name__) def __virtual__(): + if not HAS_PYVMOMI: + return False, 'State module did not load: pyVmomi not found' + + # We check the supported vim versions to infer the pyVmomi version + if 'vim25/6.0' in VmomiSupport.versionMap and \ + sys.version_info > (2, 7) and sys.version_info < (2, 7, 9): + + return False, ('State module did not load: Incompatible versions ' + 'of Python and pyVmomi present. See Issue #29537.') return 'dvs' From 03ce4d81b7fb7a47b668d7ef6c3fad12093124eb Mon Sep 17 00:00:00 2001 From: rallytime Date: Fri, 22 Sep 2017 15:01:11 -0400 Subject: [PATCH 188/633] Reactor Test: Fix incorrect merge conflict resolution --- tests/unit/utils/test_reactor.py | 36 -------------------------------- 1 file changed, 36 deletions(-) diff --git a/tests/unit/utils/test_reactor.py b/tests/unit/utils/test_reactor.py index bfc57095df..b0a10d581f 100644 --- a/tests/unit/utils/test_reactor.py +++ b/tests/unit/utils/test_reactor.py @@ -10,7 +10,6 @@ import yaml import salt.loader import salt.utils -import salt.utils.files import salt.utils.reactor as reactor from tests.support.unit import TestCase, skipIf @@ -380,41 +379,6 @@ WRAPPER_CALLS = { log = logging.getLogger(__name__) -@skipIf(NO_MOCK, NO_MOCK_REASON) -class TestReactorBasic(TestCase, AdaptedConfigurationTestCaseMixin): - def setUp(self): - self.opts = self.get_temp_config('master') - self.tempdir = tempfile.mkdtemp(dir=TMP) - self.sls_name = os.path.join(self.tempdir, 'test.sls') - with salt.utils.files.fopen(self.sls_name, 'w') as fh: - fh.write(''' -update_fileserver: - runner.fileserver.update -''') - - def tearDown(self): - if os.path.isdir(self.tempdir): - shutil.rmtree(self.tempdir) - del self.opts - del self.tempdir - del self.sls_name - - def test_basic(self): - reactor_config = [ - {'salt/tagA': ['/srv/reactor/A.sls']}, - {'salt/tagB': ['/srv/reactor/B.sls']}, - {'*': ['/srv/reactor/all.sls']}, - ] - wrap = reactor.ReactWrap(self.opts) - with patch.object(reactor.ReactWrap, 'local', MagicMock(side_effect=_args_sideffect)): - ret = wrap.run({'fun': 'test.ping', - 'state': 'local', - 'order': 1, - 'name': 'foo_action', - '__id__': 'foo_action'}) - raise Exception(ret) - - @skipIf(NO_MOCK, NO_MOCK_REASON) class TestReactor(TestCase, AdaptedConfigurationTestCaseMixin): ''' From 6baadf7a776338229275e5a502478970d7f120db Mon Sep 17 00:00:00 2001 From: Silvio Moioli Date: Wed, 20 Sep 2017 14:33:33 +0200 Subject: [PATCH 189/633] Introduce process_count_max minion configuration parameter This allows users to limit the number of processes or threads a minion will start in response to published messages, prevents resource exhaustion in case a high number of concurrent jobs is scheduled in a short time. --- salt/minion.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/salt/minion.py b/salt/minion.py index d51445be28..053b5b7fbd 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -1333,6 +1333,7 @@ class Minion(MinionBase): self._send_req_async(load, timeout, callback=lambda f: None) # pylint: disable=unexpected-keyword-arg return True + @tornado.gen.coroutine def _handle_decoded_payload(self, data): ''' Override this method if you wish to handle the decoded data @@ -1365,6 +1366,15 @@ class Minion(MinionBase): self.functions, self.returners, self.function_errors, self.executors = self._load_modules() self.schedule.functions = self.functions self.schedule.returners = self.returners + + process_count_max = self.opts.get('process_count_max') + if process_count_max > 0: + process_count = len(salt.utils.minion.running(self.opts)) + while process_count >= process_count_max: + log.warn("Maximum number of processes reached while executing jid {0}, waiting...".format(data['jid'])) + yield tornado.gen.sleep(10) + process_count = len(salt.utils.minion.running(self.opts)) + # We stash an instance references to allow for the socket # communication in Windows. You can't pickle functions, and thus # python needs to be able to reconstruct the reference on the other From 4d181ea5237918130b9aaca611479e57ff2696df Mon Sep 17 00:00:00 2001 From: Silvio Moioli Date: Wed, 20 Sep 2017 14:35:11 +0200 Subject: [PATCH 190/633] process_count_max: add defaults and documentation --- conf/minion | 6 ++++++ doc/ref/configuration/minion.rst | 17 +++++++++++++++++ salt/config/__init__.py | 4 ++++ 3 files changed, 27 insertions(+) diff --git a/conf/minion b/conf/minion index fa5caf317b..0cef29a6e1 100644 --- a/conf/minion +++ b/conf/minion @@ -689,6 +689,12 @@ # for a full explanation. #multiprocessing: True +# Limit the maximum amount of processes or threads created by salt-minion. +# This is useful to avoid resource exhaustion in case the minion receives more +# publications than it is able to handle, as it limits the number of spawned +# processes or threads. -1 disables the limit. +#process_count_max: 20 + ##### Logging settings ##### ########################################## diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index 3438bfca03..5dafffaadd 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -2419,6 +2419,23 @@ executed in a thread. multiprocessing: True +.. conf_minion:: process_count_max + +``process_count_max`` +------- + +.. versionadded:: Oxygen + +Default: ``20`` + +Limit the maximum amount of processes or threads created by ``salt-minion``. +This is useful to avoid resource exhaustion in case the minion receives more +publications than it is able to handle, as it limits the number of spawned +processes or threads. ``-1`` disables the limit. + +.. code-block:: yaml + + process_count_max: 20 .. _minion-logging-settings: diff --git a/salt/config/__init__.py b/salt/config/__init__.py index 6a89e1f485..fea68eb70a 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -337,6 +337,9 @@ VALID_OPTS = { # Whether or not processes should be forked when needed. The alternative is to use threading. 'multiprocessing': bool, + # Maximum number of concurrently active processes at any given point in time + 'process_count_max': int, + # Whether or not the salt minion should run scheduled mine updates 'mine_enabled': bool, @@ -1258,6 +1261,7 @@ DEFAULT_MINION_OPTS = { 'auto_accept': True, 'autosign_timeout': 120, 'multiprocessing': True, + 'process_count_max': 20, 'mine_enabled': True, 'mine_return_job': False, 'mine_interval': 60, From 04ab9a610287416b8674cf920fd00a9da381176a Mon Sep 17 00:00:00 2001 From: Silvio Moioli Date: Wed, 20 Sep 2017 16:53:09 +0200 Subject: [PATCH 191/633] process_count_max: adapt existing unit tests --- tests/unit/test_minion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_minion.py b/tests/unit/test_minion.py index e60e08edf3..13704f7580 100644 --- a/tests/unit/test_minion.py +++ b/tests/unit/test_minion.py @@ -69,7 +69,7 @@ class MinionTestCase(TestCase): mock_jid_queue = [123] try: minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop()) - ret = minion._handle_decoded_payload(mock_data) + ret = minion._handle_decoded_payload(mock_data).result() self.assertEqual(minion.jid_queue, mock_jid_queue) self.assertIsNone(ret) finally: @@ -98,7 +98,7 @@ class MinionTestCase(TestCase): # Call the _handle_decoded_payload function and update the mock_jid_queue to include the new # mock_jid. The mock_jid should have been added to the jid_queue since the mock_jid wasn't # previously included. The minion's jid_queue attribute and the mock_jid_queue should be equal. - minion._handle_decoded_payload(mock_data) + minion._handle_decoded_payload(mock_data).result() mock_jid_queue.append(mock_jid) self.assertEqual(minion.jid_queue, mock_jid_queue) finally: @@ -126,7 +126,7 @@ class MinionTestCase(TestCase): # Call the _handle_decoded_payload function and check that the queue is smaller by one item # and contains the new jid - minion._handle_decoded_payload(mock_data) + minion._handle_decoded_payload(mock_data).result() self.assertEqual(len(minion.jid_queue), 2) self.assertEqual(minion.jid_queue, [456, 789]) finally: From d53550de353c3f64c06381a6b623fca22740532f Mon Sep 17 00:00:00 2001 From: Silvio Moioli Date: Thu, 21 Sep 2017 10:00:00 +0200 Subject: [PATCH 192/633] process_count_max: add unit test --- tests/unit/test_minion.py | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/unit/test_minion.py b/tests/unit/test_minion.py index 13704f7580..b96d586ddd 100644 --- a/tests/unit/test_minion.py +++ b/tests/unit/test_minion.py @@ -18,6 +18,7 @@ import salt.utils.event as event from salt.exceptions import SaltSystemExit import salt.syspaths import tornado +from salt.ext.six.moves import range __opts__ = {} @@ -131,3 +132,49 @@ class MinionTestCase(TestCase): self.assertEqual(minion.jid_queue, [456, 789]) finally: minion.destroy() + + def test_process_count_max(self): + ''' + Tests that the _handle_decoded_payload function does not spawn more than the configured amount of processes, + as per process_count_max. + ''' + with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \ + patch('salt.utils.process.SignalHandlingMultiprocessingProcess.start', MagicMock(return_value=True)), \ + patch('salt.utils.process.SignalHandlingMultiprocessingProcess.join', MagicMock(return_value=True)), \ + patch('salt.utils.minion.running', MagicMock(return_value=[])), \ + patch('tornado.gen.sleep', MagicMock(return_value=tornado.concurrent.Future())): + process_count_max = 10 + mock_opts = salt.config.DEFAULT_MINION_OPTS + mock_opts['minion_jid_queue_hwm'] = 100 + mock_opts["process_count_max"] = process_count_max + + try: + io_loop = tornado.ioloop.IOLoop() + minion = salt.minion.Minion(mock_opts, jid_queue=[], io_loop=io_loop) + + # mock gen.sleep to throw a special Exception when called, so that we detect it + class SleepCalledEception(Exception): + """Thrown when sleep is called""" + pass + tornado.gen.sleep.return_value.set_exception(SleepCalledEception()) + + # up until process_count_max: gen.sleep does not get called, processes are started normally + for i in range(process_count_max): + mock_data = {'fun': 'foo.bar', + 'jid': i} + io_loop.run_sync(lambda data=mock_data: minion._handle_decoded_payload(data)) + self.assertEqual(salt.utils.process.SignalHandlingMultiprocessingProcess.start.call_count, i + 1) + self.assertEqual(len(minion.jid_queue), i + 1) + salt.utils.minion.running.return_value += [i] + + # above process_count_max: gen.sleep does get called, JIDs are created but no new processes are started + mock_data = {'fun': 'foo.bar', + 'jid': process_count_max + 1} + + self.assertRaises(SleepCalledEception, + lambda: io_loop.run_sync(lambda: minion._handle_decoded_payload(mock_data))) + self.assertEqual(salt.utils.process.SignalHandlingMultiprocessingProcess.start.call_count, + process_count_max) + self.assertEqual(len(minion.jid_queue), process_count_max + 1) + finally: + minion.destroy() From fd4194ade05059325fb9bc1ef4984a10e7700691 Mon Sep 17 00:00:00 2001 From: Silvio Moioli Date: Fri, 22 Sep 2017 15:37:43 +0200 Subject: [PATCH 193/633] process_count_max: disable by default --- conf/minion | 4 ++-- doc/ref/configuration/minion.rst | 6 +++--- salt/config/__init__.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conf/minion b/conf/minion index 0cef29a6e1..2946007e2f 100644 --- a/conf/minion +++ b/conf/minion @@ -692,8 +692,8 @@ # Limit the maximum amount of processes or threads created by salt-minion. # This is useful to avoid resource exhaustion in case the minion receives more # publications than it is able to handle, as it limits the number of spawned -# processes or threads. -1 disables the limit. -#process_count_max: 20 +# processes or threads. -1 is the default and disables the limit. +#process_count_max: -1 ##### Logging settings ##### diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index 5dafffaadd..e4fe7a44e6 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -2426,16 +2426,16 @@ executed in a thread. .. versionadded:: Oxygen -Default: ``20`` +Default: ``-1`` Limit the maximum amount of processes or threads created by ``salt-minion``. This is useful to avoid resource exhaustion in case the minion receives more publications than it is able to handle, as it limits the number of spawned -processes or threads. ``-1`` disables the limit. +processes or threads. ``-1`` is the default and disables the limit. .. code-block:: yaml - process_count_max: 20 + process_count_max: -1 .. _minion-logging-settings: diff --git a/salt/config/__init__.py b/salt/config/__init__.py index fea68eb70a..5a65b49d5a 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -1261,7 +1261,7 @@ DEFAULT_MINION_OPTS = { 'auto_accept': True, 'autosign_timeout': 120, 'multiprocessing': True, - 'process_count_max': 20, + 'process_count_max': -1, 'mine_enabled': True, 'mine_return_job': False, 'mine_interval': 60, From 9aecf5f8472ff9973bae6bc8fa07e774ed341014 Mon Sep 17 00:00:00 2001 From: Ric Klaren Date: Mon, 11 Sep 2017 15:48:41 -0500 Subject: [PATCH 194/633] Remove stderr spam when using salt-cloud with libvirt Install error handler and redirect stderr output to debug log. --- salt/cloud/clouds/libvirt.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/salt/cloud/clouds/libvirt.py b/salt/cloud/clouds/libvirt.py index c77b6d6a20..c9fbd1aeb6 100644 --- a/salt/cloud/clouds/libvirt.py +++ b/salt/cloud/clouds/libvirt.py @@ -82,9 +82,6 @@ from salt.exceptions import ( SaltCloudSystemExit ) -# Get logging started -log = logging.getLogger(__name__) - VIRT_STATE_NAME_MAP = {0: 'running', 1: 'running', 2: 'running', @@ -99,6 +96,18 @@ IP_LEARNING_XML = """ __virtualname__ = 'libvirt' +# Set up logging +log = logging.getLogger(__name__) + +def libvirtErrorHandler(ctx, error): + ''' + Redirect stderr prints from libvirt to salt logging. + ''' + log.debug("libvirt error {0}".format(error)) + + +if HAS_LIBVIRT: + libvirt.registerErrorHandler(f=libvirtErrorHandler, ctx=None) def __virtual__(): ''' From 235bec492ef9c5b7818e67fc50fd306465fa44ea Mon Sep 17 00:00:00 2001 From: Ric Klaren Date: Mon, 11 Sep 2017 12:59:01 -0500 Subject: [PATCH 195/633] salt-cloud + libvirt: Mention Fedora 26 support --- salt/cloud/clouds/libvirt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/cloud/clouds/libvirt.py b/salt/cloud/clouds/libvirt.py index c9fbd1aeb6..d147f7d578 100644 --- a/salt/cloud/clouds/libvirt.py +++ b/salt/cloud/clouds/libvirt.py @@ -41,6 +41,7 @@ Example profile: master_port: 5506 Tested on: +- Fedora 26 (libvirt 3.2.1, qemu 2.9.1) - Fedora 25 (libvirt 1.3.3.2, qemu 2.6.1) - Fedora 23 (libvirt 1.2.18, qemu 2.4.1) - Centos 7 (libvirt 1.2.17, qemu 1.5.3) From 88530c4cb6dc77a51b4a1c11139bcb844c1666e0 Mon Sep 17 00:00:00 2001 From: Ric Klaren Date: Fri, 22 Sep 2017 13:55:58 -0500 Subject: [PATCH 196/633] Lint fixes --- salt/cloud/clouds/libvirt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/salt/cloud/clouds/libvirt.py b/salt/cloud/clouds/libvirt.py index d147f7d578..1da5925f8f 100644 --- a/salt/cloud/clouds/libvirt.py +++ b/salt/cloud/clouds/libvirt.py @@ -100,7 +100,8 @@ __virtualname__ = 'libvirt' # Set up logging log = logging.getLogger(__name__) -def libvirtErrorHandler(ctx, error): + +def libvirt_error_handler(ctx, error): # pylint: disable=unused-argument ''' Redirect stderr prints from libvirt to salt logging. ''' @@ -108,7 +109,8 @@ def libvirtErrorHandler(ctx, error): if HAS_LIBVIRT: - libvirt.registerErrorHandler(f=libvirtErrorHandler, ctx=None) + libvirt.registerErrorHandler(f=libvirt_error_handler, ctx=None) + def __virtual__(): ''' From ae035f6b4d8d28976c8cca1b9e3fa0faf890080d Mon Sep 17 00:00:00 2001 From: Shane Hathaway Date: Fri, 22 Sep 2017 15:05:47 -0600 Subject: [PATCH 197/633] Fixed the 'status.procs' and 'status.pid' functions for openvzhn environments. In openvzhn environments, running the 'ps' grain requires python_shell=True. This may also be true of environments where the 'ps' grain has been customized. --- salt/modules/status.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/modules/status.py b/salt/modules/status.py index edb268267f..24d593d25f 100644 --- a/salt/modules/status.py +++ b/salt/modules/status.py @@ -132,7 +132,7 @@ def procs(): uind = 0 pind = 0 cind = 0 - plines = __salt__['cmd.run'](__grains__['ps']).splitlines() + plines = __salt__['cmd.run'](__grains__['ps'], python_shell=True).splitlines() guide = plines.pop(0).split() if 'USER' in guide: uind = guide.index('USER') @@ -1417,7 +1417,7 @@ def pid(sig): ''' cmd = __grains__['ps'] - output = __salt__['cmd.run_stdout'](cmd) + output = __salt__['cmd.run_stdout'](cmd, python_shell=True) pids = '' for line in output.splitlines(): From 85f7549cb9a0a99c36d3abfddd5c84abcab480d8 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Wed, 20 Sep 2017 15:01:46 -0700 Subject: [PATCH 198/633] Adding the ability to monitoring logins by user & specific time ranges in the btmp & wtmp beacons --- salt/beacons/btmp.py | 122 ++++++++++++++++++++++++++++++++++++++++++- salt/beacons/wtmp.py | 117 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 235 insertions(+), 4 deletions(-) diff --git a/salt/beacons/btmp.py b/salt/beacons/btmp.py index 40b50470d8..f332cde3b0 100644 --- a/salt/beacons/btmp.py +++ b/salt/beacons/btmp.py @@ -10,8 +10,10 @@ Beacon to fire events at failed login of users # Import python libs from __future__ import absolute_import +import logging import os import struct +import time # Import Salt Libs import salt.utils.files @@ -37,6 +39,15 @@ FIELDS = [ SIZE = struct.calcsize(FMT) LOC_KEY = 'btmp.loc' +log = logging.getLogger(__name__) + +# pylint: disable=import-error +try: + import dateutil.parser as dateutil_parser + _TIME_SUPPORTED = True +except ImportError: + _TIME_SUPPORTED = False + def __virtual__(): if os.path.isfile(BTMP): @@ -44,6 +55,20 @@ def __virtual__(): return False +def _check_time_range(time_range, now): + ''' + Check time range + ''' + if _TIME_SUPPORTED: + _start = int(time.mktime(dateutil_parser.parse(time_range['start']).timetuple())) + _end = int(time.mktime(dateutil_parser.parse(time_range['end']).timetuple())) + + return bool(_start <= now <= _end) + else: + log.error('Dateutil is required.') + return False + + def _get_loc(): ''' return the active file location @@ -60,6 +85,45 @@ def validate(config): if not isinstance(config, list): return False, ('Configuration for btmp beacon must ' 'be a list.') + else: + _config = {} + list(map(_config.update, config)) + + if 'users' in _config: + if not isinstance(_config['users'], dict): + return False, ('User configuration for btmp beacon must ' + 'be a dictionary.') + else: + for user in _config['users']: + if _config['users'][user] and \ + 'time_range' in _config['users'][user]: + _time_range = _config['users'][user]['time_range'] + if not isinstance(_time_range, dict): + return False, ('The time_range parameter for ' + 'btmp beacon must ' + 'be a dictionary.') + else: + if not all(k in _time_range for k in ('start', 'end')): + return False, ('The time_range parameter for ' + 'btmp beacon must contain ' + 'start & end options.') + if 'defaults' in _config: + if not isinstance(_config['defaults'], dict): + return False, ('Defaults configuration for btmp beacon must ' + 'be a dictionary.') + else: + if 'time_range' in _config['defaults']: + _time_range = _config['defaults']['time_range'] + if not isinstance(_time_range, dict): + return False, ('The time_range parameter for ' + 'btmp beacon must ' + 'be a dictionary.') + else: + if not all(k in _time_range for k in ('start', 'end')): + return False, ('The time_range parameter for ' + 'btmp beacon must contain ' + 'start & end options.') + return True, 'Valid beacon configuration' @@ -72,8 +136,40 @@ def beacon(config): beacons: btmp: [] + + beacons: + btmp: + - users: + gareth: + - defaults: + time_range: + start: '8am' + end: '4pm' + + beacons: + btmp: + - users: + gareth: + time_range: + start: '8am' + end: '4pm' + - defaults: + time_range: + start: '8am' + end: '4pm' ''' ret = [] + + users = None + defaults = None + + for config_item in config: + if 'users' in config_item: + users = config_item['users'] + + if 'defaults' in config_item: + defaults = config_item['defaults'] + with salt.utils.files.fopen(BTMP, 'rb') as fp_: loc = __context__.get(LOC_KEY, 0) if loc == 0: @@ -83,15 +179,39 @@ def beacon(config): else: fp_.seek(loc) while True: + now = int(time.time()) raw = fp_.read(SIZE) if len(raw) != SIZE: return ret + else: + log.debug(raw) __context__[LOC_KEY] = fp_.tell() pack = struct.unpack(FMT, raw) + log.debug(pack) event = {} for ind, field in enumerate(FIELDS): + log.debug('{} {}'.format(ind, field)) event[field] = pack[ind] if isinstance(event[field], six.string_types): event[field] = event[field].strip('\x00') - ret.append(event) + + if users: + if event['user'] in users: + _user = users[event['user']] + if isinstance(_user, dict) and 'time_range' in _user: + if _check_time_range(_user['time_range'], now): + ret.append(event) + else: + if defaults and 'time_range' in defaults: + if _check_time_range(defaults['time_range'], + now): + ret.append(event) + else: + ret.append(event) + else: + if defaults and 'time_range' in defaults: + if _check_time_range(defaults['time_range'], now): + ret.append(event) + else: + ret.append(event) return ret diff --git a/salt/beacons/wtmp.py b/salt/beacons/wtmp.py index c10a335e0c..3810a38306 100644 --- a/salt/beacons/wtmp.py +++ b/salt/beacons/wtmp.py @@ -10,8 +10,10 @@ Beacon to fire events at login of users as registered in the wtmp file # Import Python libs from __future__ import absolute_import +import logging import os import struct +import time # Import salt libs import salt.utils.files @@ -37,9 +39,15 @@ FIELDS = [ SIZE = struct.calcsize(FMT) LOC_KEY = 'wtmp.loc' -import logging log = logging.getLogger(__name__) +# pylint: disable=import-error +try: + import dateutil.parser as dateutil_parser + _TIME_SUPPORTED = True +except ImportError: + _TIME_SUPPORTED = False + def __virtual__(): if os.path.isfile(WTMP): @@ -47,6 +55,20 @@ def __virtual__(): return False +def _check_time_range(time_range, now): + ''' + Check time range + ''' + if _TIME_SUPPORTED: + _start = int(time.mktime(dateutil_parser.parse(time_range['start']).timetuple())) + _end = int(time.mktime(dateutil_parser.parse(time_range['end']).timetuple())) + + return bool(_start <= now <= _end) + else: + log.error('Dateutil is required.') + return False + + def _get_loc(): ''' return the active file location @@ -62,6 +84,44 @@ def validate(config): # Configuration for wtmp beacon should be a list of dicts if not isinstance(config, list): return False, ('Configuration for wtmp beacon must be a list.') + else: + _config = {} + list(map(_config.update, config)) + + if 'users' in _config: + if not isinstance(_config['users'], dict): + return False, ('User configuration for btmp beacon must ' + 'be a dictionary.') + else: + for user in _config['users']: + if _config['users'][user] and \ + 'time_range' in _config['users'][user]: + _time_range = _config['users'][user]['time_range'] + if not isinstance(_time_range, dict): + return False, ('The time_range parameter for ' + 'btmp beacon must ' + 'be a dictionary.') + else: + if not all(k in _time_range for k in ('start', 'end')): + return False, ('The time_range parameter for ' + 'btmp beacon must contain ' + 'start & end options.') + if 'defaults' in _config: + if not isinstance(_config['defaults'], dict): + return False, ('Defaults configuration for btmp beacon must ' + 'be a dictionary.') + else: + if 'time_range' in _config['defaults']: + _time_range = _config['defaults']['time_range'] + if not isinstance(_time_range, dict): + return False, ('The time_range parameter for ' + 'btmp beacon must ' + 'be a dictionary.') + else: + if not all(k in _time_range for k in ('start', 'end')): + return False, ('The time_range parameter for ' + 'btmp beacon must contain ' + 'start & end options.') return True, 'Valid beacon configuration' @@ -74,8 +134,40 @@ def beacon(config): beacons: wtmp: [] - ''' + + beacons: + wtmp: + - users: + gareth: + - defaults: + time_range: + start: '8am' + end: '4pm' + + beacons: + wtmp: + - users: + gareth: + time_range: + start: '8am' + end: '4pm' + - defaults: + time_range: + start: '8am' + end: '4pm' +''' ret = [] + + users = None + defaults = None + + for config_item in config: + if 'users' in config_item: + users = config_item['users'] + + if 'defaults' in config_item: + defaults = config_item['defaults'] + with salt.utils.files.fopen(WTMP, 'rb') as fp_: loc = __context__.get(LOC_KEY, 0) if loc == 0: @@ -85,6 +177,7 @@ def beacon(config): else: fp_.seek(loc) while True: + now = int(time.time()) raw = fp_.read(SIZE) if len(raw) != SIZE: return ret @@ -95,5 +188,23 @@ def beacon(config): event[field] = pack[ind] if isinstance(event[field], six.string_types): event[field] = event[field].strip('\x00') - ret.append(event) + if users: + if event['user'] in users: + _user = users[event['user']] + if isinstance(_user, dict) and 'time_range' in _user: + if _check_time_range(_user['time_range'], now): + ret.append(event) + else: + if defaults and 'time_range' in defaults: + if _check_time_range(defaults['time_range'], + now): + ret.append(event) + else: + ret.append(event) + else: + if defaults and 'time_range' in defaults: + if _check_time_range(defaults['time_range'], now): + ret.append(event) + else: + ret.append(event) return ret From 6fe02e3c6c07fd1db9f5fa432c82c444734b4b34 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 22 Sep 2017 11:52:39 -0700 Subject: [PATCH 199/633] Updating btmp & wtmp beacons to work with python3. Adding ability to fire alerts for specific users & specific times. Adding some unit tests for both. --- salt/beacons/btmp.py | 10 +-- salt/beacons/wtmp.py | 7 +- tests/unit/beacons/test_wtmp_beacon.py | 119 +++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 tests/unit/beacons/test_wtmp_beacon.py diff --git a/salt/beacons/btmp.py b/salt/beacons/btmp.py index f332cde3b0..e56e4b5087 100644 --- a/salt/beacons/btmp.py +++ b/salt/beacons/btmp.py @@ -183,17 +183,17 @@ def beacon(config): raw = fp_.read(SIZE) if len(raw) != SIZE: return ret - else: - log.debug(raw) __context__[LOC_KEY] = fp_.tell() pack = struct.unpack(FMT, raw) - log.debug(pack) event = {} for ind, field in enumerate(FIELDS): - log.debug('{} {}'.format(ind, field)) event[field] = pack[ind] if isinstance(event[field], six.string_types): - event[field] = event[field].strip('\x00') + if isinstance(event[field], bytes): + event[field] = event[field].decode() + event[field] = event[field].strip('b\x00') + else: + event[field] = event[field].strip('\x00') if users: if event['user'] in users: diff --git a/salt/beacons/wtmp.py b/salt/beacons/wtmp.py index 3810a38306..b882e5598f 100644 --- a/salt/beacons/wtmp.py +++ b/salt/beacons/wtmp.py @@ -187,7 +187,12 @@ def beacon(config): for ind, field in enumerate(FIELDS): event[field] = pack[ind] if isinstance(event[field], six.string_types): - event[field] = event[field].strip('\x00') + if isinstance(event[field], bytes): + event[field] = event[field].decode() + event[field] = event[field].strip('b\x00') + else: + event[field] = event[field].strip('\x00') + if users: if event['user'] in users: _user = users[event['user']] diff --git a/tests/unit/beacons/test_wtmp_beacon.py b/tests/unit/beacons/test_wtmp_beacon.py new file mode 100644 index 0000000000..b1edd97096 --- /dev/null +++ b/tests/unit/beacons/test_wtmp_beacon.py @@ -0,0 +1,119 @@ +# coding: utf-8 + +# Python libs +from __future__ import absolute_import +import logging +import sys + +# Salt testing libs +from tests.support.unit import skipIf, TestCase +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, mock_open +from tests.support.mixins import LoaderModuleMockMixin + +# Salt libs +import salt.beacons.wtmp as wtmp + +if sys.version_info >= (3,): + raw = bytes('\x07\x00\x00\x00H\x18\x00\x00pts/14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00s/14gareth\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13I\xc5YZf\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'utf-8') + pack = (7, 6216, b'pts/14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b's/14', b'gareth\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 0, 0, 0, 1506101523, 353882, 0, 0, 0, 16777216) +else: + raw = b'\x07\x00\x00\x00H\x18\x00\x00pts/14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00s/14gareth\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13I\xc5YZf\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + pack = (7, 6216, 'pts/14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 's/14', 'gareth\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', '::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 0, 0, 0, 1506101523, 353882, 0, 0, 0, 16777216) + +log = logging.getLogger(__name__) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class WTMPBeaconTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test case for salt.beacons.[s] + ''' + + def setup_loader_modules(self): + return { + wtmp: { + '__context__': {'wtmp.loc': 2}, + '__salt__': {}, + } + } + + def test_non_list_config(self): + config = {} + ret = wtmp.validate(config) + + self.assertEqual(ret, (False, 'Configuration for wtmp beacon must' + ' be a list.')) + + def test_empty_config(self): + config = [{}] + + ret = wtmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + def test_no_match(self): + config = [{'users': {'gareth': {'time': {'end': '5pm', + 'start': '3pm'}}}} + ] + + ret = wtmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + ret = wtmp.beacon(config) + self.assertEqual(ret, []) + + def test_match(self): + with patch('salt.utils.files.fopen', + mock_open(read_data=raw)): + with patch('struct.unpack', + MagicMock(return_value=pack)): + config = [{'users': {'gareth': {}}}] + + ret = wtmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + _expected = [{'PID': 6216, + 'line': 'pts/14', + 'session': 0, + 'time': 0, + 'exit_status': 0, + 'inittab': 's/14', + 'type': 7, + 'addr': 1506101523, + 'hostname': '::1', + 'user': 'gareth'}] + + ret = wtmp.beacon(config) + log.debug('{}'.format(ret)) + self.assertEqual(ret, _expected) + + def test_match_time(self): + with patch('salt.utils.files.fopen', + mock_open(read_data=raw)): + with patch('time.time', + MagicMock(return_value=1506121200)): + with patch('struct.unpack', + MagicMock(return_value=pack)): + config = [{'users': {'gareth': {'time': {'end': '5pm', + 'start': '3pm'}}}} + ] + + ret = wtmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + _expected = [{'PID': 6216, + 'line': 'pts/14', + 'session': 0, + 'time': 0, + 'exit_status': 0, + 'inittab': 's/14', + 'type': 7, + 'addr': 1506101523, + 'hostname': '::1', + 'user': 'gareth'}] + + ret = wtmp.beacon(config) + self.assertEqual(ret, _expected) From ca3f77f81e5aa105bfc5531fb45c95214d501926 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 22 Sep 2017 11:57:34 -0700 Subject: [PATCH 200/633] Adding test_btmp_beacon. --- tests/unit/beacons/test_btmp_beacon.py | 117 +++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/unit/beacons/test_btmp_beacon.py diff --git a/tests/unit/beacons/test_btmp_beacon.py b/tests/unit/beacons/test_btmp_beacon.py new file mode 100644 index 0000000000..708dae9454 --- /dev/null +++ b/tests/unit/beacons/test_btmp_beacon.py @@ -0,0 +1,117 @@ +# coding: utf-8 + +# Python libs +from __future__ import absolute_import +import logging +import sys + +# Salt testing libs +from tests.support.unit import skipIf, TestCase +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, mock_open +from tests.support.mixins import LoaderModuleMockMixin + +# Salt libs +import salt.beacons.btmp as btmp + +if sys.version_info >= (3,): + raw = bytes('\x06\x00\x00\x00Nt\x00\x00ssh:notty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00garet\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xdd\xc7\xc2Y\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'utf-8') + pack = (6, 29774, b'ssh:notty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'\x00\x00\x00\x00', b'garet\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 0, 0, 0, 1505937373, 0, 0, 0, 0, 16777216) +else: + raw = b'\x06\x00\x00\x00Nt\x00\x00ssh:notty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00garet\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xdd\xc7\xc2Y\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + pack = (6, 29774, 'ssh:notty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', '\x00\x00\x00\x00', 'garet\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', '::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 0, 0, 0, 1505937373, 0, 0, 0, 0, 16777216) +log = logging.getLogger(__name__) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class BTMPBeaconTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test case for salt.beacons.[s] + ''' + + def setup_loader_modules(self): + return { + btmp: { + '__context__': {'btmp.loc': 2}, + '__salt__': {}, + } + } + + def test_non_list_config(self): + config = {} + ret = btmp.validate(config) + + self.assertEqual(ret, (False, 'Configuration for btmp beacon must' + ' be a list.')) + + def test_empty_config(self): + config = [{}] + + ret = btmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + def test_no_match(self): + config = [{'users': {'gareth': {'time': {'end': '5pm', + 'start': '3pm'}}}} + ] + + ret = btmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + ret = btmp.beacon(config) + self.assertEqual(ret, []) + + def test_match(self): + with patch('salt.utils.files.fopen', + mock_open(read_data=raw)): + with patch('struct.unpack', + MagicMock(return_value=pack)): + config = [{'users': {'garet': {}}}] + + ret = btmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + _expected = [{'addr': 1505937373, + 'exit_status': 0, + 'inittab': '', + 'hostname': '::1', + 'PID': 29774, + 'session': 0, + 'user': + 'garet', + 'time': 0, + 'line': 'ssh:notty', + 'type': 6}] + ret = btmp.beacon(config) + self.assertEqual(ret, _expected) + + def test_match_time(self): + with patch('salt.utils.files.fopen', + mock_open(read_data=raw)): + with patch('time.time', + MagicMock(return_value=1506121200)): + with patch('struct.unpack', + MagicMock(return_value=pack)): + config = [{'users': {'garet': {'time': {'end': '5pm', + 'start': '3pm'}}}} + ] + + ret = btmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + _expected = [{'addr': 1505937373, + 'exit_status': 0, + 'inittab': '', + 'hostname': '::1', + 'PID': 29774, + 'session': 0, + 'user': + 'garet', + 'time': 0, + 'line': 'ssh:notty', + 'type': 6}] + ret = btmp.beacon(config) + self.assertEqual(ret, _expected) From 2c0bc3596396334f637e19857eda12672c3a8fb2 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 22 Sep 2017 12:48:57 -0700 Subject: [PATCH 201/633] Fixing lint errors. --- salt/beacons/btmp.py | 1 + salt/beacons/wtmp.py | 1 + 2 files changed, 2 insertions(+) diff --git a/salt/beacons/btmp.py b/salt/beacons/btmp.py index e56e4b5087..f539d6032a 100644 --- a/salt/beacons/btmp.py +++ b/salt/beacons/btmp.py @@ -20,6 +20,7 @@ import salt.utils.files # Import 3rd-party libs from salt.ext import six +from salt.ext.six.moves import map __virtualname__ = 'btmp' BTMP = '/var/log/btmp' diff --git a/salt/beacons/wtmp.py b/salt/beacons/wtmp.py index b882e5598f..2ccf1730b4 100644 --- a/salt/beacons/wtmp.py +++ b/salt/beacons/wtmp.py @@ -20,6 +20,7 @@ import salt.utils.files # Import 3rd-party libs from salt.ext import six +from salt.ext.six.moves import map __virtualname__ = 'wtmp' WTMP = '/var/log/wtmp' From fb0b987d0654d836000913ee7974b9d5922197e7 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 22 Sep 2017 14:20:24 -0700 Subject: [PATCH 202/633] Adding some lines to disable the lint errors on import for salt.ext.six.moves. --- salt/beacons/btmp.py | 4 +++- salt/beacons/inotify.py | 2 ++ salt/beacons/wtmp.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/salt/beacons/btmp.py b/salt/beacons/btmp.py index f539d6032a..9c8aca4e0f 100644 --- a/salt/beacons/btmp.py +++ b/salt/beacons/btmp.py @@ -19,8 +19,10 @@ import time import salt.utils.files # Import 3rd-party libs -from salt.ext import six +import salt.ext.six +# pylint: disable=import-error from salt.ext.six.moves import map +# pylint: enable=import-error __virtualname__ = 'btmp' BTMP = '/var/log/btmp' diff --git a/salt/beacons/inotify.py b/salt/beacons/inotify.py index ee1dfa4f78..7e5f4df863 100644 --- a/salt/beacons/inotify.py +++ b/salt/beacons/inotify.py @@ -23,7 +23,9 @@ import re # Import salt libs import salt.ext.six +# pylint: disable=import-error from salt.ext.six.moves import map +# pylint: enable=import-error # Import third party libs try: diff --git a/salt/beacons/wtmp.py b/salt/beacons/wtmp.py index 2ccf1730b4..65a88df28b 100644 --- a/salt/beacons/wtmp.py +++ b/salt/beacons/wtmp.py @@ -20,7 +20,9 @@ import salt.utils.files # Import 3rd-party libs from salt.ext import six +# pylint: disable=import-error from salt.ext.six.moves import map +# pylint: enable=import-error __virtualname__ = 'wtmp' WTMP = '/var/log/wtmp' From db25b6500b2337c9abde32c34b4766ea62d16dc9 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 22 Sep 2017 14:27:55 -0700 Subject: [PATCH 203/633] Fixing one more import. --- salt/beacons/btmp.py | 2 +- salt/beacons/wtmp.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/salt/beacons/btmp.py b/salt/beacons/btmp.py index 9c8aca4e0f..fe7f329025 100644 --- a/salt/beacons/btmp.py +++ b/salt/beacons/btmp.py @@ -191,7 +191,7 @@ def beacon(config): event = {} for ind, field in enumerate(FIELDS): event[field] = pack[ind] - if isinstance(event[field], six.string_types): + if isinstance(event[field], salt.ext.six.string_types): if isinstance(event[field], bytes): event[field] = event[field].decode() event[field] = event[field].strip('b\x00') diff --git a/salt/beacons/wtmp.py b/salt/beacons/wtmp.py index 65a88df28b..4cb3a0f4fc 100644 --- a/salt/beacons/wtmp.py +++ b/salt/beacons/wtmp.py @@ -19,7 +19,7 @@ import time import salt.utils.files # Import 3rd-party libs -from salt.ext import six +import salt.ext.six # pylint: disable=import-error from salt.ext.six.moves import map # pylint: enable=import-error @@ -189,7 +189,7 @@ def beacon(config): event = {} for ind, field in enumerate(FIELDS): event[field] = pack[ind] - if isinstance(event[field], six.string_types): + if isinstance(event[field], salt.ext.six.string_types): if isinstance(event[field], bytes): event[field] = event[field].decode() event[field] = event[field].strip('b\x00') From 6b574ec5dac61addbc7f67d75f73f342b95ca53d Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 22 Sep 2017 17:07:53 -0400 Subject: [PATCH 204/633] Return sorted added/removed/changed/unchanged keys in RecursiveDictDiffer so result is deterministic --- salt/utils/dictdiffer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/salt/utils/dictdiffer.py b/salt/utils/dictdiffer.py index b007742083..abe8bfc1c5 100644 --- a/salt/utils/dictdiffer.py +++ b/salt/utils/dictdiffer.py @@ -267,7 +267,7 @@ class RecursiveDictDiffer(DictDiffer): keys.append('{0}{1}'.format(prefix, key)) return keys - return _added(self._diffs, prefix='') + return sorted(_added(self._diffs, prefix='')) def removed(self): ''' @@ -290,7 +290,7 @@ class RecursiveDictDiffer(DictDiffer): prefix='{0}{1}.'.format(prefix, key))) return keys - return _removed(self._diffs, prefix='') + return sorted(_removed(self._diffs, prefix='')) def changed(self): ''' @@ -338,7 +338,7 @@ class RecursiveDictDiffer(DictDiffer): return keys - return _changed(self._diffs, prefix='') + return sorted(_changed(self._diffs, prefix='')) def unchanged(self): ''' @@ -363,7 +363,7 @@ class RecursiveDictDiffer(DictDiffer): prefix='{0}{1}.'.format(prefix, key))) return keys - return _unchanged(self.current_dict, self._diffs, prefix='') + return sorted(_unchanged(self.current_dict, self._diffs, prefix='')) @property def diffs(self): From 847debab7a5567a47e7a33e02173dcb9176f5f31 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 22 Sep 2017 17:09:01 -0400 Subject: [PATCH 205/633] Fix failing storage and listdiffer tests --- tests/unit/utils/test_dictdiffer.py | 2 +- tests/unit/utils/vmware/test_storage.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/utils/test_dictdiffer.py b/tests/unit/utils/test_dictdiffer.py index 2c6243bbd8..23fa5955eb 100644 --- a/tests/unit/utils/test_dictdiffer.py +++ b/tests/unit/utils/test_dictdiffer.py @@ -49,7 +49,7 @@ class RecursiveDictDifferTestCase(TestCase): def test_changed_without_ignore_unset_values(self): self.recursive_diff.ignore_unset_values = False self.assertEqual(self.recursive_diff.changed(), - ['a.c', 'a.e', 'a.g', 'a.f', 'h', 'i']) + ['a.c', 'a.e', 'a.f', 'a.g', 'h', 'i']) def test_unchanged(self): self.assertEqual(self.recursive_diff.unchanged(), diff --git a/tests/unit/utils/vmware/test_storage.py b/tests/unit/utils/vmware/test_storage.py index 43434225ae..8f9a069149 100644 --- a/tests/unit/utils/vmware/test_storage.py +++ b/tests/unit/utils/vmware/test_storage.py @@ -264,14 +264,14 @@ class GetDatastoresTestCase(TestCase): mock_reference, get_all_datastores=True) - mock_traversal_spec_init.assert_called([ + mock_traversal_spec_init.assert_has_calls([ + call(path='datastore', + skip=False, + type=vim.Datacenter), call(path='childEntity', selectSet=['traversal'], skip=False, - type=vim.Folder), - call(path='datastore', - skip=False, - type=vim.Datacenter)]) + type=vim.Folder)]) def test_unsupported_reference_type(self): class FakeClass(object): @@ -379,7 +379,7 @@ class RenameDatastoreTestCase(TestCase): with self.assertRaises(VMwareApiError) as excinfo: salt.utils.vmware.rename_datastore(self.mock_ds_ref, 'fake_new_name') - self.assertEqual(excinfo.exception.message, 'vim_fault') + self.assertEqual(excinfo.exception.strerror, 'vim_fault') def test_rename_datastore_raise_runtime_fault(self): exc = vmodl.RuntimeFault() @@ -388,7 +388,7 @@ class RenameDatastoreTestCase(TestCase): with self.assertRaises(VMwareRuntimeError) as excinfo: salt.utils.vmware.rename_datastore(self.mock_ds_ref, 'fake_new_name') - self.assertEqual(excinfo.exception.message, 'runtime_fault') + self.assertEqual(excinfo.exception.strerror, 'runtime_fault') def test_rename_datastore(self): salt.utils.vmware.rename_datastore(self.mock_ds_ref, 'fake_new_name') From f73764481b064cfd20ab9a1ce3e84afe8961903c Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 22 Sep 2017 17:03:53 -0500 Subject: [PATCH 206/633] Add missing support for use/use_in requisites to state.sls_id Because requisite resolution doesn't happen until we run call_high, and state.sls_id doesn't run call_high (but rather calls a single low chunk from the compiled low chunks), the use/use_in requisites are ignored. This fixes that oversight by resolving requisites in state.sls_id before we compile the high data. --- salt/modules/state.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/salt/modules/state.py b/salt/modules/state.py index 5d6bf5f009..15ad2f2a4a 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -1303,6 +1303,9 @@ def sls_id(id_, mods, test=None, queue=False, **kwargs): finally: st_.pop_active() errors += st_.state.verify_high(high_) + # Apply requisites to high data + high_, req_in_errors = st_.state.requisite_in(high_) + errors.extend(req_in_errors) if errors: __context__['retcode'] = 1 return errors From c7a652784afe8dfc04f6a47e4abb6c0508ad57e0 Mon Sep 17 00:00:00 2001 From: Damon Atkins Date: Sat, 23 Sep 2017 13:56:50 +1000 Subject: [PATCH 207/633] remove blank line at end of file --- salt/utils/files.py | 1 - 1 file changed, 1 deletion(-) diff --git a/salt/utils/files.py b/salt/utils/files.py index be4077583f..657ff82b04 100644 --- a/salt/utils/files.py +++ b/salt/utils/files.py @@ -328,4 +328,3 @@ def remove(path): except OSError as exc: if exc.errno != errno.ENOENT: raise - From 96c1ef48e62807b71853cb72c2dcc5a8ebef6448 Mon Sep 17 00:00:00 2001 From: Wedge Jarrad Date: Sat, 23 Sep 2017 17:38:51 -0700 Subject: [PATCH 208/633] Ignore retcode on call to grep in selinux.py module Fixes #43711 Returning an exit code of 1 is normal operation of grep when it does not find a match. This will happen every time this function is called by fcontext_policy_present to detirmine whether a selinux policy exists before creating it. Ignoring the retcode will prevent it from emitting an error when this happens. --- salt/modules/selinux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/selinux.py b/salt/modules/selinux.py index 208eee03f5..aecadd7a14 100644 --- a/salt/modules/selinux.py +++ b/salt/modules/selinux.py @@ -463,7 +463,7 @@ def fcontext_get_policy(name, filetype=None, sel_type=None, sel_user=None, sel_l cmd_kwargs['filetype'] = '[[:alpha:] ]+' if filetype is None else filetype_id_to_string(filetype) cmd = 'semanage fcontext -l | egrep ' + \ "'^{filespec}{spacer}{filetype}{spacer}{sel_user}:{sel_role}:{sel_type}:{sel_level}$'".format(**cmd_kwargs) - current_entry_text = __salt__['cmd.shell'](cmd) + current_entry_text = __salt__['cmd.shell'](cmd, ignore_retcode=True) if current_entry_text == '': return None ret = {} From 7ba690afaa1d4d8fe54aac0843ee4eba2e8f641c Mon Sep 17 00:00:00 2001 From: assaf shapira Date: Sun, 24 Sep 2017 12:49:11 +0300 Subject: [PATCH 209/633] added link to citrix SDK download --- salt/cloud/clouds/xen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/cloud/clouds/xen.py b/salt/cloud/clouds/xen.py index 558c7cacb6..d1eeb95ace 100644 --- a/salt/cloud/clouds/xen.py +++ b/salt/cloud/clouds/xen.py @@ -7,6 +7,7 @@ XenServer Cloud Driver The XenServer driver is designed to work with a Citrix XenServer. Requires XenServer SDK +(can be downloaded from https://www.citrix.com/downloads/xenserver/product-software/ ) Place a copy of the XenAPI.py in the Python site-packages folder. From a327ee96148826d2fa355aec0fc4170ae020d055 Mon Sep 17 00:00:00 2001 From: assaf shapira Date: Sun, 24 Sep 2017 17:34:38 +0300 Subject: [PATCH 210/633] fix lint errors --- salt/cloud/clouds/xen.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/salt/cloud/clouds/xen.py b/salt/cloud/clouds/xen.py index d1eeb95ace..759aa5ebd8 100644 --- a/salt/cloud/clouds/xen.py +++ b/salt/cloud/clouds/xen.py @@ -162,8 +162,7 @@ def _get_session(): user, password, api_version, originator) except XenAPI.Failure as ex: ''' - if the server on the url is not the pool master, the pool master's - address will be rturned in the exception message + get the pool master's address from the XenAPI raised exception ''' pool_master_addr = str(ex.__dict__['details'][1]) slash_parts = url.split('/') @@ -193,10 +192,10 @@ def list_nodes(): ret = {} for vm in vms: record = session.xenapi.VM.get_record(vm) - if not(record['is_a_template']) and not(record['is_control_domain']): + if not record['is_a_template'] and not record['is_control_domain']: try: base_template_name = record['other_config']['base_template_name'] - except Exception as KeyError: + except Exception: base_template_name = None log.debug('VM {}, doesnt have base_template_name attribute'.format( record['name_label'])) @@ -316,7 +315,7 @@ def list_nodes_full(session=None): # deal with cases where the VM doesn't have 'base_template_name' attribute try: base_template_name = record['other_config']['base_template_name'] - except Exception as KeyError: + except Exception: base_template_name = None log.debug('VM {}, doesnt have base_template_name attribute'.format( record['name_label'])) @@ -481,7 +480,7 @@ def show_instance(name, session=None, call=None): if not record['is_a_template'] and not record['is_control_domain']: try: base_template_name = record['other_config']['base_template_name'] - except Exception as KeyError: + except Exception: base_template_name = None log.debug('VM {}, doesnt have base_template_name attribute'.format( record['name_label'])) From 78137c0860f9f70e4325e74caa4b5241638d805e Mon Sep 17 00:00:00 2001 From: Sebastien Huber Date: Mon, 25 Sep 2017 10:00:52 +0200 Subject: [PATCH 211/633] Corrected custom port handling This pillar was only able to connect to a Postgres DB running on the default port (5432) This commit extend this to a custom port --- salt/pillar/postgres.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/pillar/postgres.py b/salt/pillar/postgres.py index 58cd0e3298..7b6300989a 100644 --- a/salt/pillar/postgres.py +++ b/salt/pillar/postgres.py @@ -90,7 +90,8 @@ class POSTGRESExtPillar(SqlBaseExtPillar): conn = psycopg2.connect(host=_options['host'], user=_options['user'], password=_options['pass'], - dbname=_options['db']) + dbname=_options['db'], + port=_options['port']) cursor = conn.cursor() try: yield cursor From 2c80ea54f4c363ffc4fbcd2344b9f8568bb78c30 Mon Sep 17 00:00:00 2001 From: assaf shapira Date: Mon, 25 Sep 2017 12:06:48 +0300 Subject: [PATCH 212/633] more lint fixes --- salt/cloud/clouds/xen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cloud/clouds/xen.py b/salt/cloud/clouds/xen.py index 759aa5ebd8..a57bb65fa1 100644 --- a/salt/cloud/clouds/xen.py +++ b/salt/cloud/clouds/xen.py @@ -162,7 +162,7 @@ def _get_session(): user, password, api_version, originator) except XenAPI.Failure as ex: ''' - get the pool master's address from the XenAPI raised exception + get the pool master address from the XenAPI raised exception ''' pool_master_addr = str(ex.__dict__['details'][1]) slash_parts = url.split('/') From 19da1000b4ac9716c3743c8c72640ca74e302512 Mon Sep 17 00:00:00 2001 From: Heghedus Razvan Date: Fri, 22 Sep 2017 15:57:59 +0300 Subject: [PATCH 213/633] test_nilrt_ip: Fix set_static_all test The nameservers needs to be specified only by ip address. Signed-off-by: Heghedus Razvan --- tests/integration/modules/test_nilrt_ip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/modules/test_nilrt_ip.py b/tests/integration/modules/test_nilrt_ip.py index 1412cffb2d..5c2fbc0bfb 100644 --- a/tests/integration/modules/test_nilrt_ip.py +++ b/tests/integration/modules/test_nilrt_ip.py @@ -98,13 +98,13 @@ class Nilrt_ipModuleTest(ModuleCase): def test_static_all(self): interfaces = self.__interfaces() for interface in interfaces: - result = self.run_function('ip.set_static_all', [interface, '192.168.10.4', '255.255.255.0', '192.168.10.1', '8.8.4.4 my.dns.com']) + result = self.run_function('ip.set_static_all', [interface, '192.168.10.4', '255.255.255.0', '192.168.10.1', '8.8.4.4 8.8.8.8']) self.assertTrue(result) info = self.run_function('ip.get_interfaces_details') for interface in info['interfaces']: self.assertIn('8.8.4.4', interface['ipv4']['dns']) - self.assertIn('my.dns.com', interface['ipv4']['dns']) + self.assertIn('8.8.8.8', interface['ipv4']['dns']) self.assertEqual(interface['ipv4']['requestmode'], 'static') self.assertEqual(interface['ipv4']['address'], '192.168.10.4') self.assertEqual(interface['ipv4']['netmask'], '255.255.255.0') From 06e68bfa4fb2aad6f1b50ad334ba6b775ba18dfd Mon Sep 17 00:00:00 2001 From: assaf shapira Date: Mon, 25 Sep 2017 14:52:40 +0300 Subject: [PATCH 214/633] lint errors fixed --- salt/cloud/clouds/xen.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/salt/cloud/clouds/xen.py b/salt/cloud/clouds/xen.py index a57bb65fa1..7359796c20 100644 --- a/salt/cloud/clouds/xen.py +++ b/salt/cloud/clouds/xen.py @@ -161,9 +161,6 @@ def _get_session(): session.xenapi.login_with_password( user, password, api_version, originator) except XenAPI.Failure as ex: - ''' - get the pool master address from the XenAPI raised exception - ''' pool_master_addr = str(ex.__dict__['details'][1]) slash_parts = url.split('/') new_url = '/'.join(slash_parts[:2]) + '/' + pool_master_addr From 3abba0b999c087ad4d6c757e4df1d5b9573e32ea Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Mon, 25 Sep 2017 06:40:31 -0700 Subject: [PATCH 215/633] Update documentation in Pure Storage [purefa] module Add external array authentication methods. Changed version added to be Oxygen --- salt/modules/purefa.py | 60 ++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/salt/modules/purefa.py b/salt/modules/purefa.py index 14beb37bef..aeb4104ee7 100644 --- a/salt/modules/purefa.py +++ b/salt/modules/purefa.py @@ -27,6 +27,20 @@ Installation Prerequisites pip install purestorage +- Configure Pure Storage FlashArray authentication. Use one of the following + three methods. + + 1) From the minion config + .. code-block:: yaml + + pure_tags: + fa: + san_ip: management vip or hostname for the FlashArray + api_token: A valid api token for the FlashArray being managed + + 2) From environment (PUREFA_IP and PUREFA_API) + 3) From the pillar (PUREFA_IP and PUREFA_API) + :maintainer: Simon Dodsley (simon@purestorage.com) :maturity: new :requires: purestorage @@ -195,7 +209,7 @@ def snap_create(name, suffix=None): Will return False is volume selected to snap does not exist. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume to snapshot @@ -231,7 +245,7 @@ def snap_delete(name, suffix=None, eradicate=False): Will return False if selected snapshot does not exist. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume @@ -273,7 +287,7 @@ def snap_eradicate(name, suffix=None): Will retunr False is snapshot is not in a deleted state. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume @@ -306,7 +320,7 @@ def volume_create(name, size=None): Will return False if volume already exists. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume (truncated to 63 characters) @@ -344,7 +358,7 @@ def volume_delete(name, eradicate=False): Will return False if volume doesn't exist is already in a deleted state. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume @@ -383,7 +397,7 @@ def volume_eradicate(name): Will return False is volume is not in a deleted state. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume @@ -413,7 +427,7 @@ def volume_extend(name, size): Will return False if new size is less than or equal to existing size. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume @@ -451,7 +465,7 @@ def snap_volume_create(name, target, overwrite=False): Will return False if target volume already exists and overwrite is not specified, or selected snapshot doesn't exist. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume snapshot @@ -497,7 +511,7 @@ def volume_clone(name, target, overwrite=False): Will return False if source volume doesn't exist, or target volume already exists and overwrite not specified. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume @@ -541,7 +555,7 @@ def volume_attach(name, host): Host and volume must exist or else will return False. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume @@ -574,7 +588,7 @@ def volume_detach(name, host): Will return False if either host or volume do not exist, or if selected volume isn't already connected to the host. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of volume @@ -608,7 +622,7 @@ def host_create(name, iqn=None, wwn=None): Fibre Channel parameters are not in a valid format. See Pure Storage FlashArray documentation. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of host (truncated to 63 characters) @@ -659,7 +673,7 @@ def host_update(name, iqn=None, wwn=None): by another host, or are not in a valid format. See Pure Storage FlashArray documentation. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of host @@ -699,7 +713,7 @@ def host_delete(name): Will return False if the host doesn't exist. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of host @@ -735,7 +749,7 @@ def hg_create(name, host=None, volume=None): Will return False if hostgroup already exists, or if named host or volume do not exist. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of hostgroup (truncated to 63 characters) @@ -791,7 +805,7 @@ def hg_update(name, host=None, volume=None): Will return False is hostgroup doesn't exist, or host or volume do not exist. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of hostgroup @@ -837,7 +851,7 @@ def hg_delete(name): Will return False is hostgroup is already in a deleted state. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of hostgroup @@ -875,7 +889,7 @@ def hg_remove(name, volume=None, host=None): Will return False is hostgroup does not exist, or named host or volume are not in the hostgroup. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of hostgroup @@ -936,7 +950,7 @@ def pg_create(name, hostgroup=None, host=None, volume=None, enabled=True): hostgroups, hosts or volumes * Named type for protection group does not exist - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of protection group @@ -1029,7 +1043,7 @@ def pg_update(name, hostgroup=None, host=None, volume=None): * Incorrect type selected for current protection group type * Specified type does not exist - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of protection group @@ -1119,7 +1133,7 @@ def pg_delete(name, eradicate=False): Will return False if protection group is already in a deleted state. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of protection group @@ -1156,7 +1170,7 @@ def pg_eradicate(name): Will return False if protection group is not in a deleted state. - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of protection group @@ -1188,7 +1202,7 @@ def pg_remove(name, hostgroup=None, host=None, volume=None): * Protection group does not exist * Specified type is not currently associated with the protection group - .. versionadded:: 2017.7.3 + .. versionadded:: Oxygen name : string name of hostgroup From 1c484f6ad568284f0634c549cfb3c330ba6bdcc5 Mon Sep 17 00:00:00 2001 From: nicholasmhughes Date: Mon, 25 Sep 2017 10:39:11 -0400 Subject: [PATCH 216/633] prevent exception when test=True --- salt/modules/ini_manage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/ini_manage.py b/salt/modules/ini_manage.py index 53ff8488e0..a228cfb2fa 100644 --- a/salt/modules/ini_manage.py +++ b/salt/modules/ini_manage.py @@ -368,7 +368,7 @@ class _Ini(_Section): super(_Ini, self).__init__(name, inicontents, separator, commenter) def refresh(self, inicontents=None): - if inicontents is None: + if inicontents is None and __opts__['test'] is False: try: with salt.utils.fopen(self.name) as rfh: inicontents = rfh.read() From cfe37916c39c29cd095676cade576d7143f762b8 Mon Sep 17 00:00:00 2001 From: nicholasmhughes Date: Mon, 25 Sep 2017 10:40:02 -0400 Subject: [PATCH 217/633] handling changes per section --- salt/states/ini_manage.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/salt/states/ini_manage.py b/salt/states/ini_manage.py index 7ea26e05b7..ad126ff695 100644 --- a/salt/states/ini_manage.py +++ b/salt/states/ini_manage.py @@ -93,7 +93,7 @@ def options_present(name, sections=None, separator='=', strict=False): del changes[section_name] else: changes = __salt__['ini.set_option'](name, sections, separator) - except IOError as err: + except (IOError, KeyError) as err: ret['comment'] = "{0}".format(err) ret['result'] = False return ret @@ -102,12 +102,10 @@ def options_present(name, sections=None, separator='=', strict=False): ret['comment'] = 'Errors encountered. {0}'.format(changes['error']) ret['changes'] = {} else: - if changes: - ret['changes'] = changes - ret['comment'] = 'Changes take effect' - else: - ret['changes'] = {} - ret['comment'] = 'No changes take effect' + for name, body in changes.items(): + if body: + ret['comment'] = 'Changes take effect' + ret['changes'].update({name: changes[name]}) return ret From d68c5c4be0e45414156e6befb052a01054731a78 Mon Sep 17 00:00:00 2001 From: nicholasmhughes Date: Mon, 25 Sep 2017 11:11:23 -0400 Subject: [PATCH 218/633] prevent exception when test=True --- salt/modules/ini_manage.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/salt/modules/ini_manage.py b/salt/modules/ini_manage.py index a228cfb2fa..2eb660edd9 100644 --- a/salt/modules/ini_manage.py +++ b/salt/modules/ini_manage.py @@ -368,15 +368,16 @@ class _Ini(_Section): super(_Ini, self).__init__(name, inicontents, separator, commenter) def refresh(self, inicontents=None): - if inicontents is None and __opts__['test'] is False: + if inicontents is None: try: with salt.utils.fopen(self.name) as rfh: inicontents = rfh.read() except (OSError, IOError) as exc: - raise CommandExecutionError( - "Unable to open file '{0}'. " - "Exception: {1}".format(self.name, exc) - ) + if __opts__['test'] is False: + raise CommandExecutionError( + "Unable to open file '{0}'. " + "Exception: {1}".format(self.name, exc) + ) if not inicontents: return # Remove anything left behind from a previous run. From 72d96ed74b706c432ef37ef46551a6c7c31951f9 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 22 Sep 2017 15:37:12 -0600 Subject: [PATCH 219/633] Add an up_to_date state to win_wua --- salt/modules/win_wua.py | 2 +- salt/states/win_wua.py | 187 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 181 insertions(+), 8 deletions(-) diff --git a/salt/modules/win_wua.py b/salt/modules/win_wua.py index 5549b3e2bf..24441d185c 100644 --- a/salt/modules/win_wua.py +++ b/salt/modules/win_wua.py @@ -110,7 +110,7 @@ def available(software=True, Include software updates in the results (default is True) drivers (bool): - Include driver updates in the results (default is False) + Include driver updates in the results (default is True) summary (bool): - True: Return a summary of updates available for each category. diff --git a/salt/states/win_wua.py b/salt/states/win_wua.py index ab43b65654..fef44abe69 100644 --- a/salt/states/win_wua.py +++ b/salt/states/win_wua.py @@ -84,10 +84,12 @@ def installed(name, updates=None): Args: - name (str): The identifier of a single update to install. + name (str): + The identifier of a single update to install. - updates (list): A list of identifiers for updates to be installed. - Overrides ``name``. Default is None. + updates (list): + A list of identifiers for updates to be installed. Overrides + ``name``. Default is None. .. note:: Identifiers can be the GUID, the KB number, or any part of the Title of the Microsoft update. GUIDs and KBs are the preferred method @@ -121,7 +123,7 @@ def installed(name, updates=None): # Install multiple updates install_updates: wua.installed: - - name: + - updates: - KB3194343 - 28cf1b09-2b1a-458c-9bd1-971d1b26b211 ''' @@ -215,10 +217,12 @@ def removed(name, updates=None): Args: - name (str): The identifier of a single update to uninstall. + name (str): + The identifier of a single update to uninstall. - updates (list): A list of identifiers for updates to be removed. - Overrides ``name``. Default is None. + updates (list): + A list of identifiers for updates to be removed. Overrides ``name``. + Default is None. .. note:: Identifiers can be the GUID, the KB number, or any part of the Title of the Microsoft update. GUIDs and KBs are the preferred method @@ -329,3 +333,172 @@ def removed(name, updates=None): ret['comment'] = 'Updates removed successfully' return ret + + +def up_to_date(name, + software=True, + drivers=False, + skip_hidden=False, + skip_mandatory=False, + skip_reboot=True, + categories=None, + severities=None,): + ''' + Ensure Microsoft Updates that match the passed criteria are installed. + Updates will be downloaded if needed. + + This state allows you to update a system without specifying a specific + update to apply. All matching updates will be installed. + + Args: + + name (str): + The name has no functional value and is only used as a tracking + reference + + software (bool): + Include software updates in the results (default is True) + + drivers (bool): + Include driver updates in the results (default is False) + + skip_hidden (bool): + Skip updates that have been hidden. Default is False. + + skip_mandatory (bool): + Skip mandatory updates. Default is False. + + skip_reboot (bool): + Skip updates that require a reboot. Default is True. + + categories (list): + Specify the categories to list. Must be passed as a list. All + categories returned by default. + + Categories include the following: + + * Critical Updates + * Definition Updates + * Drivers (make sure you set drivers=True) + * Feature Packs + * Security Updates + * Update Rollups + * Updates + * Update Rollups + * Windows 7 + * Windows 8.1 + * Windows 8.1 drivers + * Windows 8.1 and later drivers + * Windows Defender + + severities (list): + Specify the severities to include. Must be passed as a list. All + severities returned by default. + + Severities include the following: + + * Critical + * Important + + + Returns: + dict: A dictionary containing the results of the update + + CLI Example: + + .. code-block:: yaml + + # Update the system using the state defaults + update_system: + wua.up_to_date + + # Update the drivers + update_drivers: + wua.up_to_date: + - software: False + - drivers: True + - skip_reboot: False + + # Apply all critical updates + update_critical: + wua.up_to_date: + - severities: + - Critical + ''' + ret = {'name': name, + 'changes': {}, + 'result': True, + 'comment': ''} + + wua = salt.utils.win_update.WindowsUpdateAgent() + + available_updates = wua.available( + skip_hidden=skip_hidden, skip_installed=True, + skip_mandatory=skip_mandatory, skip_reboot=skip_reboot, + software=software, drivers=drivers, categories=categories, + severities=severities) + + # No updates found + if available_updates.count() == 0: + ret['comment'] = 'No updates found' + return ret + + updates = list(available_updates.list().keys()) + + # Search for updates + install_list = wua.search(updates) + + # List of updates to download + download = salt.utils.win_update.Updates() + for item in install_list.updates: + if not salt.utils.is_true(item.IsDownloaded): + download.updates.Add(item) + + # List of updates to install + install = salt.utils.win_update.Updates() + for item in install_list.updates: + if not salt.utils.is_true(item.IsInstalled): + install.updates.Add(item) + + # Return comment of changes if test. + if __opts__['test']: + ret['result'] = None + ret['comment'] = 'Updates will be installed:' + for update in install.updates: + ret['comment'] += '\n' + ret['comment'] += ': '.join( + [update.Identity.UpdateID, update.Title]) + return ret + + # Download updates + wua.download(download) + + # Install updates + wua.install(install) + + # Refresh windows update info + wua.refresh() + + post_info = wua.updates().list() + + # Verify the installation + for item in install.list(): + if not salt.utils.is_true(post_info[item]['Installed']): + ret['changes']['failed'] = { + item: {'Title': post_info[item]['Title'][:40] + '...', + 'KBs': post_info[item]['KBs']} + } + ret['result'] = False + else: + ret['changes']['installed'] = { + item: {'Title': post_info[item]['Title'][:40] + '...', + 'NeedsReboot': post_info[item]['NeedsReboot'], + 'KBs': post_info[item]['KBs']} + } + + if ret['changes'].get('failed', False): + ret['comment'] = 'Updates failed' + else: + ret['comment'] = 'Updates installed successfully' + + return ret From be554c898b679aaad66acf036854af4d7c97c58e Mon Sep 17 00:00:00 2001 From: twangboy Date: Mon, 25 Sep 2017 09:21:41 -0600 Subject: [PATCH 220/633] Rename new state to `uptodate` --- salt/states/win_wua.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/salt/states/win_wua.py b/salt/states/win_wua.py index fef44abe69..798853d5ca 100644 --- a/salt/states/win_wua.py +++ b/salt/states/win_wua.py @@ -335,14 +335,14 @@ def removed(name, updates=None): return ret -def up_to_date(name, - software=True, - drivers=False, - skip_hidden=False, - skip_mandatory=False, - skip_reboot=True, - categories=None, - severities=None,): +def uptodate(name, + software=True, + drivers=False, + skip_hidden=False, + skip_mandatory=False, + skip_reboot=True, + categories=None, + severities=None,): ''' Ensure Microsoft Updates that match the passed criteria are installed. Updates will be downloaded if needed. From 85b0a8c401844afe66211b5317e40fc2e9ca00af Mon Sep 17 00:00:00 2001 From: Jochen Breuer Date: Mon, 25 Sep 2017 17:29:27 +0200 Subject: [PATCH 221/633] Improved delete_deployment test for kubernetes module This is a follow up of this PR: https://github.com/saltstack/salt/pull/43235 With the fix in PR 43235, we are polling the status of the deletion via show_deployment. This is now also reflected in the tests with this change. --- tests/unit/modules/test_kubernetes.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/unit/modules/test_kubernetes.py b/tests/unit/modules/test_kubernetes.py index 46ac760158..4e8f6cd4b5 100644 --- a/tests/unit/modules/test_kubernetes.py +++ b/tests/unit/modules/test_kubernetes.py @@ -97,19 +97,20 @@ class KubernetesTestCase(TestCase, LoaderModuleMockMixin): def test_delete_deployments(self): ''' - Tests deployment creation. + Tests deployment deletion :return: ''' with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib: - with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}): - mock_kubernetes_lib.client.V1DeleteOptions = Mock(return_value="") - mock_kubernetes_lib.client.ExtensionsV1beta1Api.return_value = Mock( - **{"delete_namespaced_deployment.return_value.to_dict.return_value": {'code': 200}} - ) - self.assertEqual(kubernetes.delete_deployment("test"), {'code': 200}) - self.assertTrue( - kubernetes.kubernetes.client.ExtensionsV1beta1Api(). - delete_namespaced_deployment().to_dict.called) + with patch('salt.modules.kubernetes.show_deployment', Mock(return_value=None)): + with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}): + mock_kubernetes_lib.client.V1DeleteOptions = Mock(return_value="") + mock_kubernetes_lib.client.ExtensionsV1beta1Api.return_value = Mock( + **{"delete_namespaced_deployment.return_value.to_dict.return_value": {'code': ''}} + ) + self.assertEqual(kubernetes.delete_deployment("test"), {'code': 200}) + self.assertTrue( + kubernetes.kubernetes.client.ExtensionsV1beta1Api(). + delete_namespaced_deployment().to_dict.called) def test_create_deployments(self): ''' From c5cf5e92c1290e3740425ca3b4ed63076826e9df Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 29 Jun 2017 16:39:13 -0600 Subject: [PATCH 222/633] Fix many tests --- salt/modules/file.py | 36 ++-- tests/unit/modules/test_file.py | 333 +++++++++++++++++--------------- 2 files changed, 198 insertions(+), 171 deletions(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index 21a60dda51..9f8263cab0 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -2179,14 +2179,14 @@ def replace(path, if not_found_content is None: not_found_content = repl if prepend_if_not_found: - new_file.insert(0, not_found_content + b'\n') + new_file.insert(0, not_found_content + os.linesep) else: # append_if_not_found # Make sure we have a newline at the end of the file if 0 != len(new_file): - if not new_file[-1].endswith(b'\n'): - new_file[-1] += b'\n' - new_file.append(not_found_content + b'\n') + if not new_file[-1].endswith(os.linesep): + new_file[-1] += os.linesep + new_file.append(not_found_content + os.linesep) has_changes = True if not dry_run: try: @@ -2197,7 +2197,7 @@ def replace(path, raise CommandExecutionError("Exception: {0}".format(exc)) # write new content in the file while avoiding partial reads try: - fh_ = salt.utils.atomicfile.atomic_open(path, 'w') + fh_ = salt.utils.atomicfile.atomic_open(path, 'wb') for line in new_file: fh_.write(salt.utils.to_str(line)) finally: @@ -2369,7 +2369,7 @@ def blockreplace(path, try: fi_file = fileinput.input(path, inplace=False, backup=False, - bufsize=1, mode='r') + bufsize=1, mode='rb') for line in fi_file: result = line @@ -2386,12 +2386,12 @@ def blockreplace(path, # Check for multi-line '\n' terminated content as split will # introduce an unwanted additional new line. - if content and content[-1] == '\n': + if content and content[-1] == os.linesep: content = content[:-1] # push new block content in file - for cline in content.split('\n'): - new_file.append(cline + '\n') + for cline in content.split(os.linesep): + new_file.append(cline + os.linesep) done = True @@ -2419,25 +2419,25 @@ def blockreplace(path, if not done: if prepend_if_not_found: # add the markers and content at the beginning of file - new_file.insert(0, marker_end + '\n') + new_file.insert(0, marker_end + os.linesep) if append_newline is True: - new_file.insert(0, content + '\n') + new_file.insert(0, content + os.linesep) else: new_file.insert(0, content) - new_file.insert(0, marker_start + '\n') + new_file.insert(0, marker_start + os.linesep) done = True elif append_if_not_found: # Make sure we have a newline at the end of the file if 0 != len(new_file): - if not new_file[-1].endswith('\n'): - new_file[-1] += '\n' + if not new_file[-1].endswith(os.linesep): + new_file[-1] += os.linesep # add the markers and content at the end of file - new_file.append(marker_start + '\n') + new_file.append(marker_start + os.linesep) if append_newline is True: - new_file.append(content + '\n') + new_file.append(content + os.linesep) else: new_file.append(content) - new_file.append(marker_end + '\n') + new_file.append(marker_end + os.linesep) done = True else: raise CommandExecutionError( @@ -2468,7 +2468,7 @@ def blockreplace(path, # write new content in the file while avoiding partial reads try: - fh_ = salt.utils.atomicfile.atomic_open(path, 'w') + fh_ = salt.utils.atomicfile.atomic_open(path, 'wb') for line in new_file: fh_.write(line) finally: diff --git a/tests/unit/modules/test_file.py b/tests/unit/modules/test_file.py index 1c7dbe13eb..0a7ef22742 100644 --- a/tests/unit/modules/test_file.py +++ b/tests/unit/modules/test_file.py @@ -10,7 +10,7 @@ import textwrap # Import Salt Testing libs from tests.support.mixins import LoaderModuleMockMixin from tests.support.paths import TMP -from tests.support.unit import TestCase +from tests.support.unit import TestCase, skipIf from tests.support.mock import MagicMock, patch # Import Salt libs @@ -89,45 +89,57 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): 'repl': 'baz=\\g', 'append_if_not_found': True, } - base = 'foo=1\nbar=2' - expected = '{base}\n{repl}\n'.format(base=base, **args) + base = os.linesep.join(['foo=1', 'bar=2']) + # File ending with a newline, no match - with tempfile.NamedTemporaryFile(mode='w+') as tfile: - tfile.write(base + '\n') + with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: + tfile.write(base + os.linesep) tfile.flush() - filemod.replace(tfile.name, **args) - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), expected) + filemod.replace(tfile.name, **args) + expected = os.linesep.join([base, 'baz=\\g']) + os.linesep + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), expected) + os.remove(tfile.name) + # File not ending with a newline, no match - with tempfile.NamedTemporaryFile('w+') as tfile: + with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: tfile.write(base) tfile.flush() - filemod.replace(tfile.name, **args) - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), expected) + filemod.replace(tfile.name, **args) + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), expected) + os.remove(tfile.name) + # A newline should not be added in empty files - with tempfile.NamedTemporaryFile('w+') as tfile: - filemod.replace(tfile.name, **args) - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), args['repl'] + '\n') + tfile = tempfile.NamedTemporaryFile('w+b', delete=False) + tfile.close() + filemod.replace(tfile.name, **args) + expected = args['repl'] + os.linesep + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), expected) + os.remove(tfile.name) + # Using not_found_content, rather than repl - with tempfile.NamedTemporaryFile('w+') as tfile: - args['not_found_content'] = 'baz=3' - expected = '{base}\n{not_found_content}\n'.format(base=base, **args) + with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: tfile.write(base) tfile.flush() - filemod.replace(tfile.name, **args) - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), expected) + args['not_found_content'] = 'baz=3' + expected = os.linesep.join([base, 'baz=3']) + os.linesep + filemod.replace(tfile.name, **args) + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), expected) + os.remove(tfile.name) + # not appending if matches - with tempfile.NamedTemporaryFile('w+') as tfile: + with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: base = 'foo=1\n#baz=42\nbar=2\n' - expected = 'foo=1\nbaz=42\nbar=2\n' + base = os.linesep.join(['foo=1', 'baz=42', 'bar=2']) tfile.write(base) tfile.flush() - filemod.replace(tfile.name, **args) - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), expected) + expected = base + filemod.replace(tfile.name, **args) + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), expected) def test_backup(self): fext = '.bak' @@ -246,23 +258,24 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): del self.tfile def test_replace_multiline(self): - new_multiline_content = ( - "Who's that then?\nWell, how'd you become king," - "then?\nWe found them. I'm not a witch.\nWe shall" - "say 'Ni' again to you, if you do not appease us." - ) + new_multiline_content = os.linesep.join([ + "Who's that then?", + "Well, how'd you become king, then?", + "We found them. I'm not a witch.", + "We shall say 'Ni' again to you, if you do not appease us." + ]) filemod.blockreplace(self.tfile.name, '#-- START BLOCK 1', '#-- END BLOCK 1', new_multiline_content, backup=False) - with salt.utils.fopen(self.tfile.name, 'r') as fp: + with salt.utils.fopen(self.tfile.name, 'rb') as fp: filecontent = fp.read() - self.assertIn('#-- START BLOCK 1' - + "\n" + new_multiline_content - + "\n" - + '#-- END BLOCK 1', filecontent) + self.assertIn( + os.linesep.join([ + '#-- START BLOCK 1', new_multiline_content, '#-- END BLOCK 1']), + filecontent) self.assertNotIn('old content part 1', filecontent) self.assertNotIn('old content part 2', filecontent) @@ -291,10 +304,12 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): backup=False, append_if_not_found=True) - with salt.utils.fopen(self.tfile.name, 'r') as fp: - self.assertIn('#-- START BLOCK 2' - + "\n" + new_content - + '#-- END BLOCK 2', fp.read()) + with salt.utils.fopen(self.tfile.name, 'rb') as fp: + self.assertIn( + os.linesep.join([ + '#-- START BLOCK 2', + '{0}#-- END BLOCK 2'.format(new_content)]), + fp.read()) def test_replace_append_newline_at_eof(self): ''' @@ -308,27 +323,33 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): 'content': 'baz', 'append_if_not_found': True, } - block = '{marker_start}\n{content}{marker_end}\n'.format(**args) - expected = base + '\n' + block + block = os.linesep.join(['#start', 'baz#stop']) + os.linesep # File ending with a newline - with tempfile.NamedTemporaryFile(mode='w+') as tfile: - tfile.write(base + '\n') + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: + tfile.write(base + os.linesep) tfile.flush() - filemod.blockreplace(tfile.name, **args) - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), expected) + filemod.blockreplace(tfile.name, **args) + expected = os.linesep.join([base, block]) + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), expected) + os.remove(tfile.name) + # File not ending with a newline - with tempfile.NamedTemporaryFile(mode='w+') as tfile: + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: tfile.write(base) tfile.flush() - filemod.blockreplace(tfile.name, **args) - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), expected) + filemod.blockreplace(tfile.name, **args) + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), expected) + os.remove(tfile.name) + # A newline should not be added in empty files - with tempfile.NamedTemporaryFile(mode='w+') as tfile: - filemod.blockreplace(tfile.name, **args) - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), block) + tfile = tempfile.NamedTemporaryFile(mode='w+b', delete=False) + tfile.close() + filemod.blockreplace(tfile.name, **args) + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), block) + os.remove(tfile.name) def test_replace_prepend(self): new_content = "Well, I didn't vote for you." @@ -343,10 +364,11 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): prepend_if_not_found=False, backup=False ) - with salt.utils.fopen(self.tfile.name, 'r') as fp: + with salt.utils.fopen(self.tfile.name, 'rb') as fp: self.assertNotIn( - '#-- START BLOCK 2' + "\n" - + new_content + '#-- END BLOCK 2', + os.linesep.join([ + '#-- START BLOCK 2', + '{0}#-- END BLOCK 2'.format(new_content)]), fp.read()) filemod.blockreplace(self.tfile.name, @@ -355,12 +377,12 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): backup=False, prepend_if_not_found=True) - with salt.utils.fopen(self.tfile.name, 'r') as fp: + with salt.utils.fopen(self.tfile.name, 'rb') as fp: self.assertTrue( fp.read().startswith( - '#-- START BLOCK 2' - + "\n" + new_content - + '#-- END BLOCK 2')) + os.linesep.join([ + '#-- START BLOCK 2', + '{0}#-- END BLOCK 2'.format(new_content)]))) def test_replace_partial_marked_lines(self): filemod.blockreplace(self.tfile.name, @@ -477,6 +499,7 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): } } + @skipIf(salt.utils.is_windows(), 'SED is not available on Windows') def test_sed_limit_escaped(self): with tempfile.NamedTemporaryFile(mode='w+') as tfile: tfile.write(SED_CONTENT) @@ -501,31 +524,34 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): newlines at end of file. ''' # File ending with a newline - with tempfile.NamedTemporaryFile(mode='w+') as tfile: - tfile.write('foo\n') + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: + tfile.write('foo' + os.linesep) tfile.flush() - filemod.append(tfile.name, 'bar') - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), 'foo\nbar\n') + filemod.append(tfile.name, 'bar') + expected = os.linesep.join(['foo', 'bar']) + os.linesep + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), expected) + # File not ending with a newline - with tempfile.NamedTemporaryFile(mode='w+') as tfile: + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: tfile.write('foo') tfile.flush() + filemod.append(tfile.name, 'bar') + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), expected) + + # A newline should be added in empty files + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: filemod.append(tfile.name, 'bar') - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), 'foo\nbar\n') - # A newline should not be added in empty files - with tempfile.NamedTemporaryFile(mode='w+') as tfile: - filemod.append(tfile.name, 'bar') - with salt.utils.fopen(tfile.name) as tfile2: - self.assertEqual(tfile2.read(), 'bar\n') + with salt.utils.fopen(tfile.name) as tfile2: + self.assertEqual(tfile2.read(), 'bar' + os.linesep) def test_extract_hash(self): ''' Check various hash file formats. ''' # With file name - with tempfile.NamedTemporaryFile(mode='w+') as tfile: + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: tfile.write( 'rc.conf ef6e82e4006dee563d98ada2a2a80a27\n' 'ead48423703509d37c4a90e6a0d53e143b6fc268 example.tar.gz\n' @@ -534,94 +560,94 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): ) tfile.flush() - result = filemod.extract_hash(tfile.name, '', '/rc.conf') - self.assertEqual(result, { - 'hsum': 'ef6e82e4006dee563d98ada2a2a80a27', - 'hash_type': 'md5' - }) + result = filemod.extract_hash(tfile.name, '', '/rc.conf') + self.assertEqual(result, { + 'hsum': 'ef6e82e4006dee563d98ada2a2a80a27', + 'hash_type': 'md5' + }) - result = filemod.extract_hash(tfile.name, '', '/example.tar.gz') - self.assertEqual(result, { + result = filemod.extract_hash(tfile.name, '', '/example.tar.gz') + self.assertEqual(result, { + 'hsum': 'ead48423703509d37c4a90e6a0d53e143b6fc268', + 'hash_type': 'sha1' + }) + + # All the checksums in this test file are sha1 sums. We run this + # loop three times. The first pass tests auto-detection of hash + # type by length of the hash. The second tests matching a specific + # type. The third tests a failed attempt to match a specific type, + # since sha256 was requested but sha1 is what is in the file. + for hash_type in ('', 'sha1', 'sha256'): + # Test the source_hash_name argument. Even though there are + # matches in the source_hash file for both the file_name and + # source params, they should be ignored in favor of the + # source_hash_name. + file_name = '/example.tar.gz' + source = 'https://mydomain.tld/foo.tar.bz2?key1=val1&key2=val2' + source_hash_name = './subdir/example.tar.gz' + result = filemod.extract_hash( + tfile.name, + hash_type, + file_name, + source, + source_hash_name) + expected = { + 'hsum': 'fe05bcdcdc4928012781a5f1a2a77cbb5398e106', + 'hash_type': 'sha1' + } if hash_type != 'sha256' else None + self.assertEqual(result, expected) + + # Test both a file_name and source but no source_hash_name. + # Even though there are matches for both file_name and + # source_hash_name, file_name should be preferred. + file_name = '/example.tar.gz' + source = 'https://mydomain.tld/foo.tar.bz2?key1=val1&key2=val2' + source_hash_name = None + result = filemod.extract_hash( + tfile.name, + hash_type, + file_name, + source, + source_hash_name) + expected = { 'hsum': 'ead48423703509d37c4a90e6a0d53e143b6fc268', 'hash_type': 'sha1' - }) + } if hash_type != 'sha256' else None + self.assertEqual(result, expected) - # All the checksums in this test file are sha1 sums. We run this - # loop three times. The first pass tests auto-detection of hash - # type by length of the hash. The second tests matching a specific - # type. The third tests a failed attempt to match a specific type, - # since sha256 was requested but sha1 is what is in the file. - for hash_type in ('', 'sha1', 'sha256'): - # Test the source_hash_name argument. Even though there are - # matches in the source_hash file for both the file_name and - # source params, they should be ignored in favor of the - # source_hash_name. - file_name = '/example.tar.gz' - source = 'https://mydomain.tld/foo.tar.bz2?key1=val1&key2=val2' - source_hash_name = './subdir/example.tar.gz' - result = filemod.extract_hash( - tfile.name, - hash_type, - file_name, - source, - source_hash_name) - expected = { - 'hsum': 'fe05bcdcdc4928012781a5f1a2a77cbb5398e106', - 'hash_type': 'sha1' - } if hash_type != 'sha256' else None - self.assertEqual(result, expected) - - # Test both a file_name and source but no source_hash_name. - # Even though there are matches for both file_name and - # source_hash_name, file_name should be preferred. - file_name = '/example.tar.gz' - source = 'https://mydomain.tld/foo.tar.bz2?key1=val1&key2=val2' - source_hash_name = None - result = filemod.extract_hash( - tfile.name, - hash_type, - file_name, - source, - source_hash_name) - expected = { - 'hsum': 'ead48423703509d37c4a90e6a0d53e143b6fc268', - 'hash_type': 'sha1' - } if hash_type != 'sha256' else None - self.assertEqual(result, expected) - - # Test both a file_name and source but no source_hash_name. - # Since there is no match for the file_name, the source is - # matched. - file_name = '/somefile.tar.gz' - source = 'https://mydomain.tld/foo.tar.bz2?key1=val1&key2=val2' - source_hash_name = None - result = filemod.extract_hash( - tfile.name, - hash_type, - file_name, - source, - source_hash_name) - expected = { - 'hsum': 'ad782ecdac770fc6eb9a62e44f90873fb97fb26b', - 'hash_type': 'sha1' - } if hash_type != 'sha256' else None - self.assertEqual(result, expected) + # Test both a file_name and source but no source_hash_name. + # Since there is no match for the file_name, the source is + # matched. + file_name = '/somefile.tar.gz' + source = 'https://mydomain.tld/foo.tar.bz2?key1=val1&key2=val2' + source_hash_name = None + result = filemod.extract_hash( + tfile.name, + hash_type, + file_name, + source, + source_hash_name) + expected = { + 'hsum': 'ad782ecdac770fc6eb9a62e44f90873fb97fb26b', + 'hash_type': 'sha1' + } if hash_type != 'sha256' else None + self.assertEqual(result, expected) # Hash only, no file name (Maven repo checksum format) # Since there is no name match, the first checksum in the file will # always be returned, never the second. - with tempfile.NamedTemporaryFile(mode='w+') as tfile: + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: tfile.write('ead48423703509d37c4a90e6a0d53e143b6fc268\n' 'ad782ecdac770fc6eb9a62e44f90873fb97fb26b\n') tfile.flush() - for hash_type in ('', 'sha1', 'sha256'): - result = filemod.extract_hash(tfile.name, hash_type, '/testfile') - expected = { - 'hsum': 'ead48423703509d37c4a90e6a0d53e143b6fc268', - 'hash_type': 'sha1' - } if hash_type != 'sha256' else None - self.assertEqual(result, expected) + for hash_type in ('', 'sha1', 'sha256'): + result = filemod.extract_hash(tfile.name, hash_type, '/testfile') + expected = { + 'hsum': 'ead48423703509d37c4a90e6a0d53e143b6fc268', + 'hash_type': 'sha1' + } if hash_type != 'sha256' else None + self.assertEqual(result, expected) def test_user_to_uid_int(self): ''' @@ -774,6 +800,7 @@ class FileBasicsTestCase(TestCase, LoaderModuleMockMixin): self.addCleanup(os.remove, self.myfile) self.addCleanup(delattr, self, 'myfile') + @skipIf(salt.utils.is_windows(), 'os.symlink is not available on Windows') def test_symlink_already_in_desired_state(self): os.symlink(self.tfile.name, self.directory + '/a_link') self.addCleanup(os.remove, self.directory + '/a_link') From 9fe83a34a55fc799ee005d958b7ff2bcd07f271d Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 29 Jun 2017 16:57:01 -0600 Subject: [PATCH 223/633] Remove old variable declaration --- tests/unit/modules/test_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/modules/test_file.py b/tests/unit/modules/test_file.py index 0a7ef22742..f987c5f8a1 100644 --- a/tests/unit/modules/test_file.py +++ b/tests/unit/modules/test_file.py @@ -132,7 +132,6 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): # not appending if matches with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: - base = 'foo=1\n#baz=42\nbar=2\n' base = os.linesep.join(['foo=1', 'baz=42', 'bar=2']) tfile.write(base) tfile.flush() From 543610570cbf9b6647e954e1ca5ddd98766f509f Mon Sep 17 00:00:00 2001 From: twangboy Date: Mon, 31 Jul 2017 17:48:48 -0600 Subject: [PATCH 224/633] Fix bytestring issues, fix errored tests --- salt/modules/file.py | 62 +++++++++++++++++++++++---------- tests/unit/modules/test_file.py | 4 +++ 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index 9f8263cab0..8e0d4edbf1 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -2179,14 +2179,14 @@ def replace(path, if not_found_content is None: not_found_content = repl if prepend_if_not_found: - new_file.insert(0, not_found_content + os.linesep) + new_file.insert(0, not_found_content + salt.utils.to_bytes(os.linesep)) else: # append_if_not_found # Make sure we have a newline at the end of the file if 0 != len(new_file): - if not new_file[-1].endswith(os.linesep): - new_file[-1] += os.linesep - new_file.append(not_found_content + os.linesep) + if not new_file[-1].endswith(salt.utils.to_bytes(os.linesep)): + new_file[-1] += salt.utils.to_bytes(os.linesep) + new_file.append(not_found_content + salt.utils.to_bytes(os.linesep)) has_changes = True if not dry_run: try: @@ -2386,12 +2386,12 @@ def blockreplace(path, # Check for multi-line '\n' terminated content as split will # introduce an unwanted additional new line. - if content and content[-1] == os.linesep: + if content and content[-1] == salt.utils.to_bytes(os.linesep): content = content[:-1] # push new block content in file - for cline in content.split(os.linesep): - new_file.append(cline + os.linesep) + for cline in content.split(salt.utils.to_bytes(os.linesep)): + new_file.append(cline + salt.utils.to_bytes(os.linesep)) done = True @@ -2419,25 +2419,25 @@ def blockreplace(path, if not done: if prepend_if_not_found: # add the markers and content at the beginning of file - new_file.insert(0, marker_end + os.linesep) + new_file.insert(0, marker_end + salt.utils.to_bytes(os.linesep)) if append_newline is True: - new_file.insert(0, content + os.linesep) + new_file.insert(0, content + salt.utils.to_bytes(os.linesep)) else: new_file.insert(0, content) - new_file.insert(0, marker_start + os.linesep) + new_file.insert(0, marker_start + salt.utils.to_bytes(os.linesep)) done = True elif append_if_not_found: # Make sure we have a newline at the end of the file if 0 != len(new_file): - if not new_file[-1].endswith(os.linesep): + if not new_file[-1].endswith(salt.utils.to_bytes(os.linesep)): new_file[-1] += os.linesep # add the markers and content at the end of file - new_file.append(marker_start + os.linesep) + new_file.append(marker_start + salt.utils.to_bytes(os.linesep)) if append_newline is True: - new_file.append(content + os.linesep) + new_file.append(content + salt.utils.to_bytes(os.linesep)) else: new_file.append(content) - new_file.append(marker_end + os.linesep) + new_file.append(marker_end + salt.utils.to_bytes(os.linesep)) done = True else: raise CommandExecutionError( @@ -3585,6 +3585,7 @@ def source_list(source, source_hash, saltenv): if contextkey in __context__: return __context__[contextkey] + # get the master file list if isinstance(source, list): mfiles = [(f, saltenv) for f in __salt__['cp.list_master'](saltenv)] @@ -3609,6 +3610,14 @@ def source_list(source, source_hash, saltenv): single_src = next(iter(single)) single_hash = single[single_src] if single[single_src] else source_hash urlparsed_single_src = _urlparse(single_src) + # Fix this for Windows + if salt.utils.is_windows(): + # urlparse doesn't handle a local Windows path without the + # protocol indicator (file://). The scheme will be the + # drive letter instead of the protocol. So, we'll add the + # protocol and re-parse + if urlparsed_single_src.scheme.lower() in string.ascii_lowercase: + urlparsed_single_src = _urlparse('file://' + single_src) proto = urlparsed_single_src.scheme if proto == 'salt': path, senv = salt.utils.url.parse(single_src) @@ -3620,10 +3629,15 @@ def source_list(source, source_hash, saltenv): elif proto.startswith('http') or proto == 'ftp': ret = (single_src, single_hash) break - elif proto == 'file' and os.path.exists(urlparsed_single_src.path): + elif proto == 'file' and ( + os.path.exists(urlparsed_single_src.netloc) or + os.path.exists(urlparsed_single_src.path) or + os.path.exists(os.path.join( + urlparsed_single_src.netloc, + urlparsed_single_src.path))): ret = (single_src, single_hash) break - elif single_src.startswith('/') and os.path.exists(single_src): + elif single_src.startswith(os.linesep) and os.path.exists(single_src): ret = (single_src, single_hash) break elif isinstance(single, six.string_types): @@ -3634,14 +3648,26 @@ def source_list(source, source_hash, saltenv): ret = (single, source_hash) break urlparsed_src = _urlparse(single) + if salt.utils.is_windows(): + # urlparse doesn't handle a local Windows path without the + # protocol indicator (file://). The scheme will be the + # drive letter instead of the protocol. So, we'll add the + # protocol and re-parse + if urlparsed_src.scheme.lower() in string.ascii_lowercase: + urlparsed_src = _urlparse('file://' + single) proto = urlparsed_src.scheme - if proto == 'file' and os.path.exists(urlparsed_src.path): + if proto == 'file' and ( + os.path.exists(urlparsed_src.netloc) or + os.path.exists(urlparsed_src.path) or + os.path.exists(os.path.join( + urlparsed_src.netloc, + urlparsed_src.path))): ret = (single, source_hash) break elif proto.startswith('http') or proto == 'ftp': ret = (single, source_hash) break - elif single.startswith('/') and os.path.exists(single): + elif single.startswith(os.linesep) and os.path.exists(single): ret = (single, source_hash) break if ret is None: diff --git a/tests/unit/modules/test_file.py b/tests/unit/modules/test_file.py index f987c5f8a1..65f20d0cdc 100644 --- a/tests/unit/modules/test_file.py +++ b/tests/unit/modules/test_file.py @@ -862,6 +862,10 @@ class FileBasicsTestCase(TestCase, LoaderModuleMockMixin): def test_source_list_for_list_returns_local_file_slash_from_dict(self): with patch.dict(filemod.__salt__, {'cp.list_master': MagicMock(return_value=[]), 'cp.list_master_dirs': MagicMock(return_value=[])}): + print('*' * 68) + print(self.myfile) + print(os.path.exists(self.myfile)) + print('*' * 68) ret = filemod.source_list( [{self.myfile: ''}], 'filehash', 'base') self.assertEqual(list(ret), [self.myfile, 'filehash']) From 716e99c4530a3fbd49c41836db5f54be951fe92d Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 23 Aug 2017 17:11:18 -0600 Subject: [PATCH 225/633] Fix py3 bytestring problems --- salt/modules/file.py | 27 +++++++++--------- tests/unit/modules/test_file.py | 49 ++++++++++++++++----------------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index 8e0d4edbf1..c5f27d2ec5 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -2179,7 +2179,7 @@ def replace(path, if not_found_content is None: not_found_content = repl if prepend_if_not_found: - new_file.insert(0, not_found_content + salt.utils.to_bytes(os.linesep)) + new_file.insert(0, not_found_content + os.linesep) else: # append_if_not_found # Make sure we have a newline at the end of the file @@ -2199,7 +2199,7 @@ def replace(path, try: fh_ = salt.utils.atomicfile.atomic_open(path, 'wb') for line in new_file: - fh_.write(salt.utils.to_str(line)) + fh_.write(salt.utils.to_bytes(line)) finally: fh_.close() @@ -2372,6 +2372,7 @@ def blockreplace(path, bufsize=1, mode='rb') for line in fi_file: + line = salt.utils.to_str(line) result = line if marker_start in line: @@ -2386,12 +2387,12 @@ def blockreplace(path, # Check for multi-line '\n' terminated content as split will # introduce an unwanted additional new line. - if content and content[-1] == salt.utils.to_bytes(os.linesep): + if content and content[-1] == os.linesep: content = content[:-1] # push new block content in file - for cline in content.split(salt.utils.to_bytes(os.linesep)): - new_file.append(cline + salt.utils.to_bytes(os.linesep)) + for cline in content.split(os.linesep): + new_file.append(cline + os.linesep) done = True @@ -2419,25 +2420,25 @@ def blockreplace(path, if not done: if prepend_if_not_found: # add the markers and content at the beginning of file - new_file.insert(0, marker_end + salt.utils.to_bytes(os.linesep)) + new_file.insert(0, marker_end + os.linesep) if append_newline is True: - new_file.insert(0, content + salt.utils.to_bytes(os.linesep)) + new_file.insert(0, content + os.linesep) else: new_file.insert(0, content) - new_file.insert(0, marker_start + salt.utils.to_bytes(os.linesep)) + new_file.insert(0, marker_start + os.linesep) done = True elif append_if_not_found: # Make sure we have a newline at the end of the file if 0 != len(new_file): - if not new_file[-1].endswith(salt.utils.to_bytes(os.linesep)): + if not new_file[-1].endswith(os.linesep): new_file[-1] += os.linesep # add the markers and content at the end of file - new_file.append(marker_start + salt.utils.to_bytes(os.linesep)) + new_file.append(marker_start + os.linesep) if append_newline is True: - new_file.append(content + salt.utils.to_bytes(os.linesep)) + new_file.append(content + os.linesep) else: new_file.append(content) - new_file.append(marker_end + salt.utils.to_bytes(os.linesep)) + new_file.append(marker_end + os.linesep) done = True else: raise CommandExecutionError( @@ -2470,7 +2471,7 @@ def blockreplace(path, try: fh_ = salt.utils.atomicfile.atomic_open(path, 'wb') for line in new_file: - fh_.write(line) + fh_.write(salt.utils.to_bytes(line)) finally: fh_.close() diff --git a/tests/unit/modules/test_file.py b/tests/unit/modules/test_file.py index 65f20d0cdc..2b84429487 100644 --- a/tests/unit/modules/test_file.py +++ b/tests/unit/modules/test_file.py @@ -93,7 +93,7 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): # File ending with a newline, no match with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: - tfile.write(base + os.linesep) + tfile.write(salt.utils.to_bytes(base + os.linesep)) tfile.flush() filemod.replace(tfile.name, **args) expected = os.linesep.join([base, 'baz=\\g']) + os.linesep @@ -103,7 +103,7 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): # File not ending with a newline, no match with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: - tfile.write(base) + tfile.write(salt.utils.to_bytes(base)) tfile.flush() filemod.replace(tfile.name, **args) with salt.utils.fopen(tfile.name) as tfile2: @@ -121,7 +121,7 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): # Using not_found_content, rather than repl with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: - tfile.write(base) + tfile.write(salt.utils.to_bytes(base)) tfile.flush() args['not_found_content'] = 'baz=3' expected = os.linesep.join([base, 'baz=3']) + os.linesep @@ -133,7 +133,7 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): # not appending if matches with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: base = os.linesep.join(['foo=1', 'baz=42', 'bar=2']) - tfile.write(base) + tfile.write(salt.utils.to_bytes(base)) tfile.flush() expected = base filemod.replace(tfile.name, **args) @@ -271,12 +271,12 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): with salt.utils.fopen(self.tfile.name, 'rb') as fp: filecontent = fp.read() - self.assertIn( + self.assertIn(salt.utils.to_bytes( os.linesep.join([ - '#-- START BLOCK 1', new_multiline_content, '#-- END BLOCK 1']), + '#-- START BLOCK 1', new_multiline_content, '#-- END BLOCK 1'])), filecontent) - self.assertNotIn('old content part 1', filecontent) - self.assertNotIn('old content part 2', filecontent) + self.assertNotIn(b'old content part 1', filecontent) + self.assertNotIn(b'old content part 2', filecontent) def test_replace_append(self): new_content = "Well, I didn't vote for you." @@ -304,10 +304,10 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): append_if_not_found=True) with salt.utils.fopen(self.tfile.name, 'rb') as fp: - self.assertIn( + self.assertIn(salt.utils.to_bytes( os.linesep.join([ '#-- START BLOCK 2', - '{0}#-- END BLOCK 2'.format(new_content)]), + '{0}#-- END BLOCK 2'.format(new_content)])), fp.read()) def test_replace_append_newline_at_eof(self): @@ -325,7 +325,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): block = os.linesep.join(['#start', 'baz#stop']) + os.linesep # File ending with a newline with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write(base + os.linesep) + tfile.write(salt.utils.to_bytes(base + os.linesep)) tfile.flush() filemod.blockreplace(tfile.name, **args) expected = os.linesep.join([base, block]) @@ -335,7 +335,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): # File not ending with a newline with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write(base) + tfile.write(salt.utils.to_bytes(base)) tfile.flush() filemod.blockreplace(tfile.name, **args) with salt.utils.fopen(tfile.name) as tfile2: @@ -364,10 +364,10 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): backup=False ) with salt.utils.fopen(self.tfile.name, 'rb') as fp: - self.assertNotIn( + self.assertNotIn(salt.utils.to_bytes( os.linesep.join([ '#-- START BLOCK 2', - '{0}#-- END BLOCK 2'.format(new_content)]), + '{0}#-- END BLOCK 2'.format(new_content)])), fp.read()) filemod.blockreplace(self.tfile.name, @@ -378,10 +378,10 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): with salt.utils.fopen(self.tfile.name, 'rb') as fp: self.assertTrue( - fp.read().startswith( + fp.read().startswith(salt.utils.to_bytes( os.linesep.join([ '#-- START BLOCK 2', - '{0}#-- END BLOCK 2'.format(new_content)]))) + '{0}#-- END BLOCK 2'.format(new_content)])))) def test_replace_partial_marked_lines(self): filemod.blockreplace(self.tfile.name, @@ -524,7 +524,7 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): ''' # File ending with a newline with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write('foo' + os.linesep) + tfile.write(salt.utils.to_bytes('foo' + os.linesep)) tfile.flush() filemod.append(tfile.name, 'bar') expected = os.linesep.join(['foo', 'bar']) + os.linesep @@ -533,7 +533,7 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): # File not ending with a newline with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write('foo') + tfile.write(salt.utils.to_bytes('foo')) tfile.flush() filemod.append(tfile.name, 'bar') with salt.utils.fopen(tfile.name) as tfile2: @@ -551,12 +551,12 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): ''' # With file name with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write( + tfile.write(salt.utils.to_bytes( 'rc.conf ef6e82e4006dee563d98ada2a2a80a27\n' 'ead48423703509d37c4a90e6a0d53e143b6fc268 example.tar.gz\n' 'fe05bcdcdc4928012781a5f1a2a77cbb5398e106 ./subdir/example.tar.gz\n' 'ad782ecdac770fc6eb9a62e44f90873fb97fb26b foo.tar.bz2\n' - ) + )) tfile.flush() result = filemod.extract_hash(tfile.name, '', '/rc.conf') @@ -636,8 +636,9 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): # Since there is no name match, the first checksum in the file will # always be returned, never the second. with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write('ead48423703509d37c4a90e6a0d53e143b6fc268\n' - 'ad782ecdac770fc6eb9a62e44f90873fb97fb26b\n') + tfile.write(salt.utils.to_bytes( + 'ead48423703509d37c4a90e6a0d53e143b6fc268\n' + 'ad782ecdac770fc6eb9a62e44f90873fb97fb26b\n')) tfile.flush() for hash_type in ('', 'sha1', 'sha256'): @@ -862,10 +863,6 @@ class FileBasicsTestCase(TestCase, LoaderModuleMockMixin): def test_source_list_for_list_returns_local_file_slash_from_dict(self): with patch.dict(filemod.__salt__, {'cp.list_master': MagicMock(return_value=[]), 'cp.list_master_dirs': MagicMock(return_value=[])}): - print('*' * 68) - print(self.myfile) - print(os.path.exists(self.myfile)) - print('*' * 68) ret = filemod.source_list( [{self.myfile: ''}], 'filehash', 'base') self.assertEqual(list(ret), [self.myfile, 'filehash']) From d5f27901e324ad6904ea4da1ddc9cd12bf72e543 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 23 Aug 2017 17:16:49 -0600 Subject: [PATCH 226/633] Fix additional bytestring issue --- salt/modules/file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index c5f27d2ec5..f6fd3e5a5d 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -2179,7 +2179,7 @@ def replace(path, if not_found_content is None: not_found_content = repl if prepend_if_not_found: - new_file.insert(0, not_found_content + os.linesep) + new_file.insert(0, not_found_content + salt.utils.to_bytes(os.linesep)) else: # append_if_not_found # Make sure we have a newline at the end of the file From e20aa5c39b7d7315664094bc832858c3f866a77a Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 24 Aug 2017 15:39:30 -0600 Subject: [PATCH 227/633] Fix line, use os.sep instead of os.linesep --- salt/modules/file.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index f6fd3e5a5d..ae704d75a5 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -3586,7 +3586,6 @@ def source_list(source, source_hash, saltenv): if contextkey in __context__: return __context__[contextkey] - # get the master file list if isinstance(source, list): mfiles = [(f, saltenv) for f in __salt__['cp.list_master'](saltenv)] @@ -3638,7 +3637,7 @@ def source_list(source, source_hash, saltenv): urlparsed_single_src.path))): ret = (single_src, single_hash) break - elif single_src.startswith(os.linesep) and os.path.exists(single_src): + elif single_src.startswith(os.sep) and os.path.exists(single_src): ret = (single_src, single_hash) break elif isinstance(single, six.string_types): @@ -3668,7 +3667,7 @@ def source_list(source, source_hash, saltenv): elif proto.startswith('http') or proto == 'ftp': ret = (single, source_hash) break - elif single.startswith(os.linesep) and os.path.exists(single): + elif single.startswith(os.sep) and os.path.exists(single): ret = (single, source_hash) break if ret is None: From b55172d5dc802ad501924c82ac46aff0819c08ef Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 24 Aug 2017 17:06:10 -0600 Subject: [PATCH 228/633] Split by Windows and Linux style line endings --- salt/modules/file.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index ae704d75a5..4751476a2a 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -2385,14 +2385,22 @@ def blockreplace(path, # end of block detected in_block = False - # Check for multi-line '\n' terminated content as split will - # introduce an unwanted additional new line. - if content and content[-1] == os.linesep: - content = content[:-1] + # Separate the content into lines. Account for Windows + # style line endings using os.linesep, then by linux + # style line endings + split_content = [] + for linesep_line in content.split(os.linesep): + for content_line in linesep_line.split('\n'): + split_content.append(content_line) + + # Trim any trailing new lines to avoid unwanted + # additional new lines + while not split_content[-1]: + split_content.pop() # push new block content in file - for cline in content.split(os.linesep): - new_file.append(cline + os.linesep) + for content_line in split_content: + new_file.append(content_line + os.linesep) done = True From 352fe69e3568558c6de274cd46a3efc20fecde44 Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 29 Aug 2017 17:47:01 -0600 Subject: [PATCH 229/633] Clarify the purpose of the for loop --- salt/modules/file.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index 4751476a2a..6e903a5669 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -2385,9 +2385,11 @@ def blockreplace(path, # end of block detected in_block = False - # Separate the content into lines. Account for Windows - # style line endings using os.linesep, then by linux - # style line endings + # Handle situations where there may be multiple types + # of line endings in the same file. Separate the content + # into lines. Account for Windows-style line endings + # using os.linesep, then by linux-style line endings + # using '\n' split_content = [] for linesep_line in content.split(os.linesep): for content_line in linesep_line.split('\n'): From 056f3bb4c09c7b8a9cfb9f12edaa4d1d43b1184e Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 14 Sep 2017 08:42:19 -0600 Subject: [PATCH 230/633] Use with to open temp file --- tests/unit/modules/test_file.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/modules/test_file.py b/tests/unit/modules/test_file.py index 2b84429487..713a96576a 100644 --- a/tests/unit/modules/test_file.py +++ b/tests/unit/modules/test_file.py @@ -111,8 +111,8 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): os.remove(tfile.name) # A newline should not be added in empty files - tfile = tempfile.NamedTemporaryFile('w+b', delete=False) - tfile.close() + with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: + pass filemod.replace(tfile.name, **args) expected = args['repl'] + os.linesep with salt.utils.fopen(tfile.name) as tfile2: @@ -343,8 +343,8 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): os.remove(tfile.name) # A newline should not be added in empty files - tfile = tempfile.NamedTemporaryFile(mode='w+b', delete=False) - tfile.close() + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: + pass filemod.blockreplace(tfile.name, **args) with salt.utils.fopen(tfile.name) as tfile2: self.assertEqual(tfile2.read(), block) From 048e16883f54b27e43f892dc447a369c15c0c2e4 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 28 Jun 2017 17:20:34 -0600 Subject: [PATCH 231/633] Use uppercase KEY --- tests/unit/modules/test_environ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/test_environ.py b/tests/unit/modules/test_environ.py index 9442942041..889f2e5c80 100644 --- a/tests/unit/modules/test_environ.py +++ b/tests/unit/modules/test_environ.py @@ -83,7 +83,7 @@ class EnvironTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(os.environ, mock_environ): mock_setval = MagicMock(return_value=None) with patch.object(environ, 'setval', mock_setval): - self.assertEqual(environ.setenv({}, False, True, False)['key'], + self.assertEqual(environ.setenv({}, False, True, False)['KEY'], None) def test_get(self): From d73ef44cf676ccc3a021c0dbab47321792c84360 Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 29 Jun 2017 10:54:25 -0600 Subject: [PATCH 232/633] Mock with uppercase KEY --- tests/unit/modules/test_environ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/test_environ.py b/tests/unit/modules/test_environ.py index 889f2e5c80..085887bfe4 100644 --- a/tests/unit/modules/test_environ.py +++ b/tests/unit/modules/test_environ.py @@ -70,7 +70,7 @@ class EnvironTestCase(TestCase, LoaderModuleMockMixin): Set multiple salt process environment variables from a dict. Returns a dict. ''' - mock_environ = {'key': 'value'} + mock_environ = {'KEY': 'value'} with patch.dict(os.environ, mock_environ): self.assertFalse(environ.setenv('environ')) From 68e1bd99ebe8ff83ebe1e456af91d284e776c820 Mon Sep 17 00:00:00 2001 From: Nicole Thomas Date: Mon, 25 Sep 2017 12:10:23 -0400 Subject: [PATCH 233/633] Revert "Extend openscap module command parsing." --- salt/modules/openscap.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/salt/modules/openscap.py b/salt/modules/openscap.py index 0dfb911f4a..2061550012 100644 --- a/salt/modules/openscap.py +++ b/salt/modules/openscap.py @@ -26,7 +26,7 @@ _XCCDF_MAP = { 'cmd_pattern': ( "oscap xccdf eval " "--oval-results --results results.xml --report report.html " - "--profile {0} {1} {2}" + "--profile {0} {1}" ) } } @@ -73,7 +73,6 @@ def xccdf(params): ''' params = shlex.split(params) policy = params[-1] - del params[-1] success = True error = None @@ -90,7 +89,7 @@ def xccdf(params): error = str(err) if success: - cmd = _XCCDF_MAP[action]['cmd_pattern'].format(args.profile, " ".join(argv), policy) + cmd = _XCCDF_MAP[action]['cmd_pattern'].format(args.profile, policy) tempdir = tempfile.mkdtemp() proc = Popen( shlex.split(cmd), stdout=PIPE, stderr=PIPE, cwd=tempdir) From 8d6ab66658dc8ddc1f571afe39bc4af08aa40f8e Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Wed, 20 Sep 2017 12:54:48 -0700 Subject: [PATCH 234/633] Add new core grains to display minion storage initiators Support for Linux and Windows platforms to display both the iSCSI IQN and Fibre Channel HBA WWPNs. With the integration of storage modules to allow configuration of 3rd party external storage arrays, these values are needed to enable the full auotmation of storage provisioning to minions. Support for Windows, Linux and AIX (iSCSI only) --- salt/grains/core.py | 118 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/salt/grains/core.py b/salt/grains/core.py index 57142ded3f..c613c27d64 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -16,6 +16,7 @@ import os import json import socket import sys +import glob import re import platform import logging @@ -65,6 +66,7 @@ __salt__ = { 'cmd.run_all': salt.modules.cmdmod._run_all_quiet, 'smbios.records': salt.modules.smbios.records, 'smbios.get': salt.modules.smbios.get, + 'cmd.run_ps': salt.modules.cmdmod.powershell, } log = logging.getLogger(__name__) @@ -2472,3 +2474,119 @@ def default_gateway(): except Exception as exc: pass return grains + + +def fc_wwn(): + ''' + Return list of fiber channel HBA WWNs + ''' + grains = {} + grains['fc_wwn'] = False + if salt.utils.platform.is_linux(): + grains['fc_wwn'] = _linux_wwns() + elif salt.utils.platform.is_windows(): + grains['fc_wwn'] = _windows_wwns() + return grains + + +def iscsi_iqn(): + ''' + Return iSCSI IQN + ''' + grains = {} + grains['iscsi_iqn'] = False + if salt.utils.platform.is_linux(): + grains['iscsi_iqn'] = _linux_iqn() + elif salt.utils.platform.is_windows(): + grains['iscsi_iqn'] = _windows_iqn() + elif salt.utils.platform.is_aix(): + grains['iscsi_iqn'] = _aix_iqn() + return grains + + +def _linux_iqn(): + ''' + Return iSCSI IQN from a Linux host. + ''' + ret = [] + + initiator = '/etc/iscsi/initiatorname.iscsi' + + if os.path.isfile(initiator): + with salt.utils.files.fopen(initiator, 'r') as _iscsi: + for line in _iscsi: + if line.find('InitiatorName') != -1: + iqn = line.split('=') + ret.extend([iqn[1]]) + return ret + + +def _aix_iqn(): + ''' + Return iSCSI IQN from an AIX host. + ''' + ret = [] + + aixcmd = 'lsattr -E -l iscsi0 | grep initiator_name' + + aixret = __salt__['cmd.run'](aixcmd) + if aixret[0].isalpha(): + iqn = aixret.split() + ret.extend([iqn[1]]) + return ret + + +def _linux_wwns(): + ''' + Return Fibre Channel port WWNs from a Linux host. + ''' + ret = [] + + for fcfile in glob.glob('/sys/class/fc_host/*/port_name'): + with salt.utils.files.fopen(fcfile, 'r') as _wwn: + for line in _wwn: + ret.extend([line[2:]]) + return ret + + +def _windows_iqn(): + ''' + Return iSCSI IQN from a Windows host. + ''' + ret = [] + + wmic = salt.utils.path.which('wmic') + + if not wmic: + return ret + + namespace = r'\\root\WMI' + mspath = 'MSiSCSIInitiator_MethodClass' + get = 'iSCSINodeName' + + cmdret = __salt__['cmd.run_all']( + '{0} /namespace:{1} path {2} get {3} /format:table'.format( + wmic, namespace, mspath, get)) + + for line in cmdret['stdout'].splitlines(): + if line[0].isalpha(): + continue + ret.extend([line]) + + return ret + + +def _windows_wwns(): + ''' + Return Fibre Channel port WWNs from a Windows host. + ''' + ps_cmd = r'Get-WmiObject -class MSFC_FibrePortHBAAttributes -namespace "root\WMI" | Select -Expandproperty Attributes | %{($_.PortWWN | % {"{0:x2}" -f $_}) -join ""}' + + ret = [] + + cmdret = __salt__['cmd.run_ps'](ps_cmd) + + for line in cmdret: + ret.append(line) + + return ret From 03c673bb004cef6737df9ff0b3996daf7f2bd19b Mon Sep 17 00:00:00 2001 From: Ronald van Zantvoort Date: Thu, 14 Sep 2017 13:46:54 +0200 Subject: [PATCH 235/633] highstate output: allow '_id' mode for each output mode --- salt/output/highstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/output/highstate.py b/salt/output/highstate.py index c003cfc32c..944fac8777 100644 --- a/salt/output/highstate.py +++ b/salt/output/highstate.py @@ -246,7 +246,7 @@ def _format_host(host, data): state_output = __opts__.get('state_output', 'full').lower() comps = [sdecode(comp) for comp in tname.split('_|-')] - if state_output == 'mixed_id': + if state_output.endswith('_id'): # Swap in the ID for the name. Refs #35137 comps[2] = comps[1] From bfbca748e2fc829a51e1820985b97464c31889bf Mon Sep 17 00:00:00 2001 From: Ronald van Zantvoort Date: Thu, 14 Sep 2017 13:58:39 +0200 Subject: [PATCH 236/633] highstate output: Document additional output modes --- conf/master | 11 ++++++----- conf/minion | 9 ++++++--- conf/proxy | 9 ++++++--- conf/suse/master | 11 ++++++----- doc/ref/configuration/master.rst | 13 ++++++++----- doc/ref/configuration/minion.rst | 12 ++++++++---- doc/topics/releases/oxygen.rst | 6 ++++++ salt/output/highstate.py | 24 ++++++++++++------------ 8 files changed, 58 insertions(+), 37 deletions(-) diff --git a/conf/master b/conf/master index 08accd85cb..e39b0b5e3e 100644 --- a/conf/master +++ b/conf/master @@ -589,11 +589,12 @@ # all data that has a result of True and no changes will be suppressed. #state_verbose: True -# The state_output setting changes if the output is the full multi line -# output for each changed state if set to 'full', but if set to 'terse' -# the output will be shortened to a single line. If set to 'mixed', the output -# will be terse unless a state failed, in which case that output will be full. -# If set to 'changes', the output will be full unless the state didn't change. +# The state_output setting controls which results will be output full multi line +# full, terse - each state will be full/terse +# mixed - only states with errors will be full +# changes - states with changes and errors will be full +# full_id, mixed_id, changes_id and terse_id are also allowed; +# when set, the state ID will be used as name in the output #state_output: full # The state_output_diff setting changes whether or not the output from diff --git a/conf/minion b/conf/minion index fa5caf317b..ffa6b7273f 100644 --- a/conf/minion +++ b/conf/minion @@ -635,9 +635,12 @@ # all data that has a result of True and no changes will be suppressed. #state_verbose: True -# The state_output setting changes if the output is the full multi line -# output for each changed state if set to 'full', but if set to 'terse' -# the output will be shortened to a single line. +# The state_output setting controls which results will be output full multi line +# full, terse - each state will be full/terse +# mixed - only states with errors will be full +# changes - states with changes and errors will be full +# full_id, mixed_id, changes_id and terse_id are also allowed; +# when set, the state ID will be used as name in the output #state_output: full # The state_output_diff setting changes whether or not the output from diff --git a/conf/proxy b/conf/proxy index f81dc32b5c..908dd25ba8 100644 --- a/conf/proxy +++ b/conf/proxy @@ -498,9 +498,12 @@ # all data that has a result of True and no changes will be suppressed. #state_verbose: True -# The state_output setting changes if the output is the full multi line -# output for each changed state if set to 'full', but if set to 'terse' -# the output will be shortened to a single line. +# The state_output setting controls which results will be output full multi line +# full, terse - each state will be full/terse +# mixed - only states with errors will be full +# changes - states with changes and errors will be full +# full_id, mixed_id, changes_id and terse_id are also allowed; +# when set, the state ID will be used as name in the output #state_output: full # The state_output_diff setting changes whether or not the output from diff --git a/conf/suse/master b/conf/suse/master index aeaa1d8859..cdba8f7dac 100644 --- a/conf/suse/master +++ b/conf/suse/master @@ -560,11 +560,12 @@ syndic_user: salt # all data that has a result of True and no changes will be suppressed. #state_verbose: True -# The state_output setting changes if the output is the full multi line -# output for each changed state if set to 'full', but if set to 'terse' -# the output will be shortened to a single line. If set to 'mixed', the output -# will be terse unless a state failed, in which case that output will be full. -# If set to 'changes', the output will be full unless the state didn't change. +# The state_output setting controls which results will be output full multi line +# full, terse - each state will be full/terse +# mixed - only states with errors will be full +# changes - states with changes and errors will be full +# full_id, mixed_id, changes_id and terse_id are also allowed; +# when set, the state ID will be used as name in the output #state_output: full # The state_output_diff setting changes whether or not the output from diff --git a/doc/ref/configuration/master.rst b/doc/ref/configuration/master.rst index 0c6ad6f919..8dc4f83ca1 100644 --- a/doc/ref/configuration/master.rst +++ b/doc/ref/configuration/master.rst @@ -2011,11 +2011,14 @@ output for states that failed or states that have changes. Default: ``full`` -The state_output setting changes if the output is the full multi line -output for each changed state if set to 'full', but if set to 'terse' -the output will be shortened to a single line. If set to 'mixed', the output -will be terse unless a state failed, in which case that output will be full. -If set to 'changes', the output will be full unless the state didn't change. +The state_output setting controls which results will be output full multi line: + +* ``full``, ``terse`` - each state will be full/terse +* ``mixed`` - only states with errors will be full +* ``changes`` - states with changes and errors will be full + +``full_id``, ``mixed_id``, ``changes_id`` and ``terse_id`` are also allowed; +when set, the state ID will be used as name in the output. .. code-block:: yaml diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index 3438bfca03..4a440526ad 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -1664,15 +1664,19 @@ output for states that failed or states that have changes. Default: ``full`` -The state_output setting changes if the output is the full multi line -output for each changed state if set to 'full', but if set to 'terse' -the output will be shortened to a single line. +The state_output setting controls which results will be output full multi line: + +* ``full``, ``terse`` - each state will be full/terse +* ``mixed`` - only states with errors will be full +* ``changes`` - states with changes and errors will be full + +``full_id``, ``mixed_id``, ``changes_id`` and ``terse_id`` are also allowed; +when set, the state ID will be used as name in the output. .. code-block:: yaml state_output: full - .. conf_minion:: state_output_diff ``state_output_diff`` diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 4c651bfce9..5c414a7143 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -88,6 +88,12 @@ environments (i.e. ``saltenvs``) have been added: ignore all tags and use branches only, and also to keep SHAs from being made available as saltenvs. +Additional output modes +------------------ + +The ``state_output`` parameter now supports ``full_id``, ``changes_id`` and ``terse_id``. +Just like ``mixed_id``, these use the state ID as name in the highstate output + Salt Cloud Features ------------------- diff --git a/salt/output/highstate.py b/salt/output/highstate.py index 944fac8777..7f7620557c 100644 --- a/salt/output/highstate.py +++ b/salt/output/highstate.py @@ -16,30 +16,30 @@ state_verbose: instruct the highstate outputter to omit displaying anything in green, this means that nothing with a result of True and no changes will not be printed state_output: - The highstate outputter has six output modes, ``full``, ``terse``, - ``mixed``, ``mixed_id``, ``changes`` and ``filter``. - + The highstate outputter has six output modes, + ``full``, ``terse``, ``mixed``, ``changes`` and ``filter`` * The default is set to ``full``, which will display many lines of detailed information for each executed chunk. * If ``terse`` is used, then the output is greatly simplified and shown in only one line. * If ``mixed`` is used, then terse output will be used unless a state failed, in which case full output will be used. - * If ``mixed_id`` is used, then the mixed form will be used, but the value for ``name`` - will be drawn from the state ID. This is useful for cases where the name - value might be very long and hard to read. * If ``changes`` is used, then terse output will be used if there was no error and no changes, otherwise full output will be used. * If ``filter`` is used, then either or both of two different filters can be used: ``exclude`` or ``terse``. - * for ``exclude``, state.highstate expects a list of states to be excluded - (or ``None``) - followed by ``True`` for terse output or ``False`` for regular output. - Because of parsing nuances, if only one of these is used, it must still - contain a comma. For instance: `exclude=True,`. - * for ``terse``, state.highstate expects simply ``True`` or ``False``. + * for ``exclude``, state.highstate expects a list of states to be excluded (or ``None``) + followed by ``True`` for terse output or ``False`` for regular output. + Because of parsing nuances, if only one of these is used, it must still + contain a comma. For instance: `exclude=True,`. + * for ``terse``, state.highstate expects simply ``True`` or ``False``. These can be set as such from the command line, or in the Salt config as `state_output_exclude` or `state_output_terse`, respectively. + The output modes have one modifier: + ``full_id``, ``terse_id``, ``mixed_id``, ``changes_id`` and ``filter_id`` + If ``_id`` is used, then the corresponding form will be used, but the value for ``name`` + will be drawn from the state ID. This is useful for cases where the name + value might be very long and hard to read. state_tabular: If `state_output` uses the terse output, set this to `True` for an aligned output format. If you wish to use a custom format, this can be set to a From a2234e45e2d81c76f1e30b8f5ae5344c208c9b57 Mon Sep 17 00:00:00 2001 From: Ronald van Zantvoort Date: Mon, 25 Sep 2017 18:37:48 +0200 Subject: [PATCH 237/633] Update release note docs regarding _id highstate output modes --- doc/topics/releases/oxygen.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 5c414a7143..5bd9ec8a80 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -92,7 +92,8 @@ Additional output modes ------------------ The ``state_output`` parameter now supports ``full_id``, ``changes_id`` and ``terse_id``. -Just like ``mixed_id``, these use the state ID as name in the highstate output +Just like ``mixed_id``, these use the state ID as name in the highstate output. +For more information on these output modes, see the docs for the :mod:`Highstate Outputter `. Salt Cloud Features ------------------- From 846af152b27a334e9cf7f9f85d9271aab234cb77 Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 25 Sep 2017 12:54:32 -0400 Subject: [PATCH 238/633] Update mocked values in some master/masterapi unit tests The addition of checking for the `auth_list` in PR #43467 requires that the mocked return of `get_auth_list` actually contains something in the list. These mock calls need to be updated so we can check for the SaltInvocationErrors. --- tests/unit/daemons/test_masterapi.py | 12 ++++++------ tests/unit/test_master.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/unit/daemons/test_masterapi.py b/tests/unit/daemons/test_masterapi.py index 29ea37ecd4..d2f5931227 100644 --- a/tests/unit/daemons/test_masterapi.py +++ b/tests/unit/daemons/test_masterapi.py @@ -63,7 +63,7 @@ class LocalFuncsTestCase(TestCase): u'message': u'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.local_funcs.runner(load) self.assertDictEqual(mock_ret, ret) @@ -93,7 +93,7 @@ class LocalFuncsTestCase(TestCase): self.assertDictEqual(mock_ret, ret) - def test_runner_eauth_salt_invocation_errpr(self): + def test_runner_eauth_salt_invocation_error(self): ''' Asserts that an EauthAuthenticationError is returned when the user authenticates, but the command is malformed. @@ -102,7 +102,7 @@ class LocalFuncsTestCase(TestCase): mock_ret = {u'error': {u'name': u'SaltInvocationError', u'message': u'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.local_funcs.runner(load) self.assertDictEqual(mock_ret, ret) @@ -146,7 +146,7 @@ class LocalFuncsTestCase(TestCase): u'message': u'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.local_funcs.wheel(load) self.assertDictEqual(mock_ret, ret) @@ -176,7 +176,7 @@ class LocalFuncsTestCase(TestCase): self.assertDictEqual(mock_ret, ret) - def test_wheel_eauth_salt_invocation_errpr(self): + def test_wheel_eauth_salt_invocation_error(self): ''' Asserts that an EauthAuthenticationError is returned when the user authenticates, but the command is malformed. @@ -185,7 +185,7 @@ class LocalFuncsTestCase(TestCase): mock_ret = {u'error': {u'name': u'SaltInvocationError', u'message': u'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.local_funcs.wheel(load) self.assertDictEqual(mock_ret, ret) diff --git a/tests/unit/test_master.py b/tests/unit/test_master.py index c663d2c45c..b2dc733198 100644 --- a/tests/unit/test_master.py +++ b/tests/unit/test_master.py @@ -93,7 +93,7 @@ class ClearFuncsTestCase(TestCase): self.assertDictEqual(mock_ret, ret) - def test_runner_eauth_salt_invocation_errpr(self): + def test_runner_eauth_salt_invocation_error(self): ''' Asserts that an EauthAuthenticationError is returned when the user authenticates, but the command is malformed. @@ -102,7 +102,7 @@ class ClearFuncsTestCase(TestCase): mock_ret = {u'error': {u'name': u'SaltInvocationError', u'message': u'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.clear_funcs.runner(clear_load) self.assertDictEqual(mock_ret, ret) @@ -155,7 +155,7 @@ class ClearFuncsTestCase(TestCase): u'message': u'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.clear_funcs.wheel(clear_load) self.assertDictEqual(mock_ret, ret) @@ -185,7 +185,7 @@ class ClearFuncsTestCase(TestCase): self.assertDictEqual(mock_ret, ret) - def test_wheel_eauth_salt_invocation_errpr(self): + def test_wheel_eauth_salt_invocation_error(self): ''' Asserts that an EauthAuthenticationError is returned when the user authenticates, but the command is malformed. @@ -194,7 +194,7 @@ class ClearFuncsTestCase(TestCase): mock_ret = {u'error': {u'name': u'SaltInvocationError', u'message': u'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.clear_funcs.wheel(clear_load) self.assertDictEqual(mock_ret, ret) From cdb028b794f82c898e25bdef7a28068772087704 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 13:09:38 -0400 Subject: [PATCH 239/633] Added key sorting to have deterministing string repr of RecursiveDictDiffer objects --- salt/utils/dictdiffer.py | 2 +- tests/unit/utils/test_dictdiffer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/utils/dictdiffer.py b/salt/utils/dictdiffer.py index abe8bfc1c5..6dc7799a57 100644 --- a/salt/utils/dictdiffer.py +++ b/salt/utils/dictdiffer.py @@ -217,7 +217,7 @@ class RecursiveDictDiffer(DictDiffer): Each inner difference is tabulated two space deeper ''' changes_strings = [] - for p in diff_dict.keys(): + for p in sorted(diff_dict.keys()): if sorted(diff_dict[p].keys()) == ['new', 'old']: # Some string formatting old_value = diff_dict[p]['old'] diff --git a/tests/unit/utils/test_dictdiffer.py b/tests/unit/utils/test_dictdiffer.py index 23fa5955eb..c2706d72a3 100644 --- a/tests/unit/utils/test_dictdiffer.py +++ b/tests/unit/utils/test_dictdiffer.py @@ -89,7 +89,7 @@ class RecursiveDictDifferTestCase(TestCase): 'a:\n' ' c from 2 to 4\n' ' e from \'old_value\' to \'new_value\'\n' - ' g from nothing to \'new_key\'\n' ' f from \'old_key\' to nothing\n' + ' g from nothing to \'new_key\'\n' 'h from nothing to \'new_key\'\n' 'i from nothing to None') From 3c26d4e3be2bb5261fc69f277d9efe80df3429aa Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 13:11:08 -0400 Subject: [PATCH 240/633] Updated all list_differ tests to compare dicts so the key order is not assumed --- tests/unit/utils/test_listdiffer.py | 57 +++++++++++++++-------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/tests/unit/utils/test_listdiffer.py b/tests/unit/utils/test_listdiffer.py index ae8288c81c..2df44278e3 100644 --- a/tests/unit/utils/test_listdiffer.py +++ b/tests/unit/utils/test_listdiffer.py @@ -32,34 +32,43 @@ class ListDictDifferTestCase(TestCase): continue def test_added(self): - self.assertEqual(self.list_diff.added, - [{'key': 5, 'value': 'foo5', 'int_value': 105}]) + self.assertEqual(len(self.list_diff.added), 1) + self.assertDictEqual(self.list_diff.added[0], + {'key': 5, 'value': 'foo5', 'int_value': 105}) def test_removed(self): - self.assertEqual(self.list_diff.removed, - [{'key': 3, 'value': 'foo3', 'int_value': 103}]) + self.assertEqual(len(self.list_diff.removed), 1) + self.assertDictEqual(self.list_diff.removed[0], + {'key': 3, 'value': 'foo3', 'int_value': 103}) def test_diffs(self): - self.assertEqual(self.list_diff.diffs, - [{2: {'int_value': {'new': 112, 'old': 102}}}, - # Added items - {5: {'int_value': {'new': 105, 'old': NONE}, - 'key': {'new': 5, 'old': NONE}, - 'value': {'new': 'foo5', 'old': NONE}}}, - # Removed items - {3: {'int_value': {'new': NONE, 'old': 103}, - 'key': {'new': NONE, 'old': 3}, - 'value': {'new': NONE, 'old': 'foo3'}}}]) + self.assertEqual(len(self.list_diff.diffs), 3) + self.assertDictEqual(self.list_diff.diffs[0], + {2: {'int_value': {'new': 112, 'old': 102}}}) + self.assertDictEqual(self.list_diff.diffs[1], + # Added items + {5: {'int_value': {'new': 105, 'old': NONE}, + 'key': {'new': 5, 'old': NONE}, + 'value': {'new': 'foo5', 'old': NONE}}}) + self.assertDictEqual(self.list_diff.diffs[2], + # Removed items + {3: {'int_value': {'new': NONE, 'old': 103}, + 'key': {'new': NONE, 'old': 3}, + 'value': {'new': NONE, 'old': 'foo3'}}}) def test_new_values(self): - self.assertEqual(self.list_diff.new_values, - [{'key': 2, 'int_value': 112}, - {'key': 5, 'value': 'foo5', 'int_value': 105}]) + self.assertEqual(len(self.list_diff.new_values), 2) + self.assertDictEqual(self.list_diff.new_values[0], + {'key': 2, 'int_value': 112}) + self.assertDictEqual(self.list_diff.new_values[1], + {'key': 5, 'value': 'foo5', 'int_value': 105}) def test_old_values(self): - self.assertEqual(self.list_diff.old_values, - [{'key': 2, 'int_value': 102}, - {'key': 3, 'value': 'foo3', 'int_value': 103}]) + self.assertEqual(len(self.list_diff.old_values), 2) + self.assertDictEqual(self.list_diff.old_values[0], + {'key': 2, 'int_value': 102}) + self.assertDictEqual(self.list_diff.old_values[1], + {'key': 3, 'value': 'foo3', 'int_value': 103}) def test_changed_all(self): self.assertEqual(self.list_diff.changed(selection='all'), @@ -78,11 +87,3 @@ class ListDictDifferTestCase(TestCase): '\twill be removed\n' '\tidentified by key 5:\n' '\twill be added\n') - - def test_changes_str2(self): - self.assertEqual(self.list_diff.changes_str2, - ' key=2 (updated):\n' - ' int_value from 102 to 112\n' - ' key=3 (removed)\n' - ' key=5 (added): {\'int_value\': 105, \'key\': 5, ' - '\'value\': \'foo5\'}') From babad12d836ba5ad9c9f50daaeb067e3128cd148 Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 25 Sep 2017 15:23:01 -0400 Subject: [PATCH 241/633] Revise "Contributing" docs: merge-forwards/release branches explained! Fixes #43650 The merge-forward process needs a more prominent position and explanation in the contributing documentation. This change attempts to explain this process a little more fully and incorporates some changes to how we are handling "main" release branches, "dot" release branches, and "develop" in a more complete context with merge-forwards. --- doc/topics/development/contributing.rst | 358 ++++++++++++++---------- 1 file changed, 206 insertions(+), 152 deletions(-) diff --git a/doc/topics/development/contributing.rst b/doc/topics/development/contributing.rst index 7941397682..09286295f3 100644 --- a/doc/topics/development/contributing.rst +++ b/doc/topics/development/contributing.rst @@ -31,7 +31,7 @@ documentation for more information. .. _github-pull-request: Sending a GitHub pull request -============================= +----------------------------- Sending pull requests on GitHub is the preferred method for receiving contributions. The workflow advice below mirrors `GitHub's own guide `_ and is well worth reading. .. code-block:: bash git fetch upstream - git checkout -b fix-broken-thing upstream/2016.3 + git checkout -b fix-broken-thing upstream/2016.11 If you're working on a feature, create your branch from the develop branch. @@ -130,7 +130,7 @@ Fork a Repo Guide_>`_ and is well worth reading. .. code-block:: bash git fetch upstream - git rebase upstream/2016.3 fix-broken-thing + git rebase upstream/2016.11 fix-broken-thing git push -u origin fix-broken-thing or @@ -170,9 +170,9 @@ Fork a Repo Guide_>`_ and is well worth reading. https://github.com/my-account/salt/pull/new/fix-broken-thing #. If your branch is a fix for a release branch, choose that as the base - branch (e.g. ``2016.3``), + branch (e.g. ``2016.11``), - https://github.com/my-account/salt/compare/saltstack:2016.3...fix-broken-thing + https://github.com/my-account/salt/compare/saltstack:2016.11...fix-broken-thing If your branch is a feature, choose ``develop`` as the base branch, @@ -205,80 +205,206 @@ Fork a Repo Guide_>`_ and is well worth reading. .. _which-salt-branch: -Which Salt branch? -================== - -GitHub will open pull requests against Salt's main branch, ``develop``, by -default. Ideally, features should go into ``develop`` and bug fixes and -documentation changes should go into the oldest supported release branch -affected by the bug or documentation update. See -:ref:`Sending a GitHub pull request `. - -If you have a bug fix or doc change and have already forked your working -branch from ``develop`` and do not know how to rebase your commits against -another branch, then submit it to ``develop`` anyway and we'll be sure to -back-port it to the correct place. - -The current release branch --------------------------- - -The current release branch is the most recent stable release. Pull requests -containing bug fixes should be made against the release branch. - -The branch name will be a date-based name such as ``2016.3``. - -Bug fixes are made on this branch so that minor releases can be cut from this -branch without introducing surprises and new features. This approach maximizes -stability. - -The Salt development team will "merge-forward" any fixes made on the release -branch to the ``develop`` branch once the pull request has been accepted. This -keeps the fix in isolation on the release branch and also keeps the ``develop`` -branch up-to-date. - -.. note:: Closing GitHub issues from commits - - This "merge-forward" strategy requires that `the magic keywords to close a - GitHub issue `_ appear in the commit - message text directly. Only including the text in a pull request will not - close the issue. - - GitHub will close the referenced issue once the *commit* containing the - magic text is merged into the default branch (``develop``). Any magic text - input only into the pull request description will not be seen at the - Git-level when those commits are merged-forward. In other words, only the - commits are merged-forward and not the pull request. - -The ``develop`` branch +Salt's Branch Topology ---------------------- +There are three different kinds of branches in use: develop, main release +branches, and dot release branches. + +- All feature work should go into the ``develop`` branch. +- Bug fixes and documentation changes should go into the oldest supported + **main** release branch affected by the the bug or documentation change. + Main release branches are named after a year and month, such as + ``2016.11`` and ``2017.7``. +- Hot fixes, as determined by SaltStack's release team, should be submitted + against **dot** release branches. Dot release branches are named after a + year, month, and version. Examples include ``2016.11.8`` and ``2017.7.2``. + +.. note:: + + GitHub will open pull requests against Salt's main branch, ``develop``, + byndefault. Be sure to check which branch is selected when creating the + pull request. + +The Develop Branch +================== + The ``develop`` branch is unstable and bleeding-edge. Pull requests containing feature additions or non-bug-fix changes should be made against the ``develop`` branch. -The Salt development team will back-port bug fixes made to ``develop`` to the -current release branch if the contributor cannot create the pull request -against that branch. +.. note:: -Release Branches ----------------- + If you have a bug fix or documentation change and have already forked your + working branch from ``develop`` and do not know how to rebase your commits + against another branch, then submit it to ``develop`` anyway. SaltStack's + development team will be happy to back-port it to the correct branch. -For each release, a branch will be created when the SaltStack release team is -ready to tag. The release branch is created from the parent branch and will be -the same name as the tag minus the ``v``. For example, the ``2017.7.1`` release -branch was created from the ``2017.7`` parent branch and the ``v2017.7.1`` -release was tagged at the ``HEAD`` of the ``2017.7.1`` branch. This branching -strategy will allow for more stability when there is a need for a re-tag during -the testing phase of the release process. + **Please make sure you let the maintainers know that the pull request needs + to be back-ported.** -Once the release branch is created, the fixes required for a given release, as -determined by the SaltStack release team, will be added to this branch. All -commits in this branch will be merged forward into the parent branch as well. +Main Release Branches +===================== + +The current release branch is the most recent stable release. Pull requests +containing bug fixes or documentation changes should be made against the main +release branch that is affected. + +The branch name will be a date-based name such as ``2016.11``. + +Bug fixes are made on this branch so that dot release branches can be cut from +the main release branch without introducing surprises and new features. This +approach maximizes stability. + +Dot Release Branches +==================== + +Prior to tagging an official release, a branch will be created when the SaltStack +release team is ready to tag. The dot release branch is created from a main release +branch. The dot release branch will be the same name as the tag minus the ``v``. +For example, the ``2017.7.1`` dot release branch was created from the ``2017.7`` +main release branch. The ``v2017.7.1`` release was tagged at the ``HEAD`` of the +``2017.7.1`` branch. + +This branching strategy will allow for more stability when there is a need for +a re-tag during the testing phase of the release process and further increases +stability. + +Once the dot release branch is created, the fixes required for a given release, +as determined by the SaltStack release team, will be added to this branch. All +commits in this branch will be merged forward into the main release branch as +well. + +Merge Forward Process +===================== + +The Salt repository follows a "Merge Forward" policy. The merge-forward +behavior means that changes submitted to older main release branches will +automatically be "merged-forward" into the newer branches. + +For example, a pull request is merged into ``2016.11``. Then, the entire +``2016.11`` branch is merged-forward into the ``2017.7`` branch, and the +``2017.7`` branch is merged-forward into the ``develop`` branch. + +This process makes is easy for contributors to make only one pull-request +against an older branch, but allows the change to propagate to all **main** +release branches. + +The merge-forward work-flow applies to all main release branches and the +operation runs continuously. + +Merge-Forwards for Dot Release Branches +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The merge-forward policy applies to dot release branches as well, but has a +slightly different behavior. If a change is submitted to a **dot** release +branch, the dot release branch will be merged into its parent **main** +release branch. + +For example, a pull request is merged into the ``2017.7.2`` release branch. +Then, the entire ``2017.7.2`` branch is merged-forward into the ``2017.7`` +branch. From there, the merge forward process continues as normal. + +The only way in which dot release branches differ from main release branches +in regard to merge-forwards, is that once a dot release branch is created +from the main release branch, the dot release branch does not receive merge +forwards. + +.. note:: + + The merge forward process for dot release branches is one-way: + dot release branch --> main release branch. + +Closing GitHub issues from commits +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This "merge-forward" strategy requires that `the magic keywords to close a +GitHub issue `_ appear in the commit +message text directly. Only including the text in a pull request will not +close the issue. + +GitHub will close the referenced issue once the *commit* containing the +magic text is merged into the default branch (``develop``). Any magic text +input only into the pull request description will not be seen at the +Git-level when those commits are merged-forward. In other words, only the +commits are merged-forward and not the pull request text. + +.. _backporting-pull-requests: + +Backporting Pull Requests +========================= + +If a bug is fixed on ``develop`` and the bug is also present on a +currently-supported release branch, it will need to be back-ported to an +applicable branch. + +.. note:: Most Salt contributors can skip these instructions + + These instructions do not need to be read in order to contribute to the + Salt project! The SaltStack team will back-port fixes on behalf of + contributors in order to keep the contribution process easy. + + These instructions are intended for frequent Salt contributors, advanced + Git users, SaltStack employees, or independent souls who wish to back-port + changes themselves. + +It is often easiest to fix a bug on the oldest supported release branch and +then merge that branch forward into ``develop`` (as described earlier in this +document). When that is not possible the fix must be back-ported, or copied, +into any other affected branches. + +These steps assume a pull request ``#1234`` has been merged into ``develop``. +And ``upstream`` is the name of the remote pointing to the main Salt repo. + +#. Identify the oldest supported release branch that is affected by the bug. + +#. Create a new branch for the back-port by reusing the same branch from the + original pull request. + + Name the branch ``bp-`` and use the number of the original pull + request. + + .. code-block:: bash + + git fetch upstream refs/pull/1234/head:bp-1234 + git checkout bp-1234 + +#. Find the parent commit of the original pull request. + + The parent commit of the original pull request must be known in order to + rebase onto a release branch. The easiest way to find this is on GitHub. + + Open the original pull request on GitHub and find the first commit in the + list of commits. Select and copy the SHA for that commit. The parent of + that commit can be specified by appending ``~1`` to the end. + +#. Rebase the new branch on top of the release branch. + + * ```` is the branch identified in step #1. + + * ```` is the SHA identified in step #3 -- don't forget to add + ``~1`` to the end! + + .. code-block:: bash + + git rebase --onto bp-1234 + + Note, release branches prior to ``2016.11`` will not be able to make use of + rebase and must use cherry-picking instead. + +#. Push the back-port branch to GitHub and open a new pull request. + + Opening a pull request for the back-port allows for the test suite and + normal code-review process. + + .. code-block:: bash + + git push -u origin bp-1234 Keeping Salt Forks in Sync -========================== +-------------------------- -Salt is advancing quickly. It is therefore critical to pull upstream changes +Salt advances quickly. It is therefore critical to pull upstream changes from upstream into your fork on a regular basis. Nothing is worse than putting hard work into a pull request only to see bunches of merge conflicts because it has diverged too far from upstream. @@ -340,92 +466,31 @@ the name of the main `saltstack/salt`_ repository. the current release branch. Posting patches to the mailing list -=================================== +----------------------------------- Patches will also be accepted by email. Format patches using `git format-patch`_ and send them to the `salt-users`_ mailing list. The contributor will then get credit for the patch, and the Salt community will have an archive of the patch and a place for discussion. -.. _backporting-pull-requests: - -Backporting Pull Requests -========================= - -If a bug is fixed on ``develop`` and the bug is also present on a -currently-supported release branch it will need to be back-ported to all -applicable branches. - -.. note:: Most Salt contributors can skip these instructions - - These instructions do not need to be read in order to contribute to the - Salt project! The SaltStack team will back-port fixes on behalf of - contributors in order to keep the contribution process easy. - - These instructions are intended for frequent Salt contributors, advanced - Git users, SaltStack employees, or independent souls who wish to back-port - changes themselves. - -It is often easiest to fix a bug on the oldest supported release branch and -then merge that branch forward into ``develop`` (as described earlier in this -document). When that is not possible the fix must be back-ported, or copied, -into any other affected branches. - -These steps assume a pull request ``#1234`` has been merged into ``develop``. -And ``upstream`` is the name of the remote pointing to the main Salt repo. - -1. Identify the oldest supported release branch that is affected by the bug. - -2. Create a new branch for the back-port by reusing the same branch from the - original pull request. - - Name the branch ``bp-`` and use the number of the original pull - request. - - .. code-block:: bash - - git fetch upstream refs/pull/1234/head:bp-1234 - git checkout bp-1234 - -3. Find the parent commit of the original pull request. - - The parent commit of the original pull request must be known in order to - rebase onto a release branch. The easiest way to find this is on GitHub. - - Open the original pull request on GitHub and find the first commit in the - list of commits. Select and copy the SHA for that commit. The parent of - that commit can be specified by appending ``~1`` to the end. - -4. Rebase the new branch on top of the release branch. - - * ```` is the branch identified in step #1. - - * ```` is the SHA identified in step #3 -- don't forget to add - ``~1`` to the end! - - .. code-block:: bash - - git rebase --onto bp-1234 - - Note, release branches prior to ``2016.3`` will not be able to make use of - rebase and must use cherry-picking instead. - -5. Push the back-port branch to GitHub and open a new pull request. - - Opening a pull request for the back-port allows for the test suite and - normal code-review process. - - .. code-block:: bash - - git push -u origin bp-1234 - Issue and Pull Request Labeling System -====================================== +-------------------------------------- SaltStack uses several labeling schemes to help facilitate code contributions and bug resolution. See the :ref:`Labels and Milestones ` documentation for more information. +Mentionbot +---------- + +SaltStack runs a mention-bot which notifies contributors who might be able +to help review incoming pull-requests based on their past contribution to +files which are being changed. + +If you do not wish to receive these notifications, please add your GitHub +handle to the blacklist line in the `.mention-bot` file located in the +root of the Salt repository. + .. _`saltstack/salt`: https://github.com/saltstack/salt .. _`GitHub Fork a Repo Guide`: https://help.github.com/articles/fork-a-repo .. _`GitHub issue tracker`: https://github.com/saltstack/salt/issues @@ -434,14 +499,3 @@ and bug resolution. See the :ref:`Labels and Milestones .. _`Closing issues via commit message`: https://help.github.com/articles/closing-issues-via-commit-messages .. _`git format-patch`: https://www.kernel.org/pub/software/scm/git/docs/git-format-patch.html .. _salt-users: https://groups.google.com/forum/#!forum/salt-users - -Mentionbot -========== - -SaltStack runs a mention-bot which notifies contributors who might be able -to help review incoming pull-requests based on their past contribution to -files which are being changed. - -If you do not wish to receive these notifications, please add your GitHub -handle to the blacklist line in the `.mention-bot` file located in the -root of the Salt repository. From f98a555f9819eb40ce881286ced8c478dfe68e18 Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 25 Sep 2017 15:37:38 -0400 Subject: [PATCH 242/633] Missed updating one of the master unit test mocks --- tests/unit/test_master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_master.py b/tests/unit/test_master.py index b2dc733198..b12fcb6a93 100644 --- a/tests/unit/test_master.py +++ b/tests/unit/test_master.py @@ -63,7 +63,7 @@ class ClearFuncsTestCase(TestCase): u'message': u'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.clear_funcs.runner(clear_load) self.assertDictEqual(mock_ret, ret) From 5f90812b122bf8b78ddbf94e99cccdbd4cc19203 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 12 Sep 2017 09:23:06 -0500 Subject: [PATCH 243/633] Fix missing PER_REMOTE_ONLY in cache.clear_git_lock runner This allows for the runner to clear the git lock if any of the per-remote-only params was present in the configuration. This also adds PER_REMOTE_ONLY attributes to the winrepo runner and git_pillar external pillar, and fixes all refs to GitBase's init_remotes function to include PER_REMOTE_ONLY arguments. This will insulate us against future headaches if either git_pillar or winrepo gain extra per-remote-only params, as we will only need to make the change to the attribute in their respective modules. --- salt/daemons/masterapi.py | 3 ++- salt/master.py | 16 +++++++++++----- salt/modules/win_repo.py | 3 ++- salt/pillar/__init__.py | 7 +++++-- salt/pillar/git_pillar.py | 8 +++++++- salt/runners/cache.py | 23 +++++++++++++++-------- salt/runners/git_pillar.py | 3 ++- salt/runners/winrepo.py | 9 ++++++++- 8 files changed, 52 insertions(+), 20 deletions(-) diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index 7e2edaa069..7c3c2c843d 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -90,7 +90,8 @@ def init_git_pillar(opts): pillar = salt.utils.gitfs.GitPillar(opts) pillar.init_remotes( opts_dict['git'], - git_pillar.PER_REMOTE_OVERRIDES + git_pillar.PER_REMOTE_OVERRIDES, + git_pillar.PER_REMOTE_ONLY ) ret.append(pillar) return ret diff --git a/salt/master.py b/salt/master.py index 740912e35c..9ad1946011 100644 --- a/salt/master.py +++ b/salt/master.py @@ -61,7 +61,6 @@ import salt.search import salt.key import salt.acl import salt.engines -import salt.fileserver import salt.daemons.masterapi import salt.defaults.exitcodes import salt.transport.server @@ -182,7 +181,8 @@ class Maintenance(SignalHandlingMultiprocessingProcess): in the parent process, then once the fork happens you'll start getting errors like "WARNING: Mixing fork() and threads detected; memory leaked." ''' - # Init fileserver manager + # Avoid circular import + import salt.fileserver self.fileserver = salt.fileserver.Fileserver(self.opts) # Load Runners ropts = dict(self.opts) @@ -463,6 +463,8 @@ class Master(SMaster): 'Cannot change to root directory ({0})'.format(err) ) + # Avoid circular import + import salt.fileserver fileserver = salt.fileserver.Fileserver(self.opts) if not fileserver.servers: errors.append( @@ -496,13 +498,15 @@ class Master(SMaster): if non_legacy_git_pillars: try: new_opts = copy.deepcopy(self.opts) - from salt.pillar.git_pillar \ - import PER_REMOTE_OVERRIDES as overrides + import salt.pillar.git_pillar for repo in non_legacy_git_pillars: new_opts['ext_pillar'] = [repo] try: git_pillar = salt.utils.gitfs.GitPillar(new_opts) - git_pillar.init_remotes(repo['git'], overrides) + git_pillar.init_remotes( + repo['git'], + salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, + salt.pillar.git_pillar.PER_REMOTE_ONLY) except FileserverConfigError as exc: critical_errors.append(exc.strerror) finally: @@ -972,6 +976,8 @@ class AESFuncs(object): ''' Set the local file objects from the file server interface ''' + # Avoid circular import + import salt.fileserver self.fs_ = salt.fileserver.Fileserver(self.opts) self._serve_file = self.fs_.serve_file self._file_find = self.fs_._find_file diff --git a/salt/modules/win_repo.py b/salt/modules/win_repo.py index 543d29914a..4b4975239d 100644 --- a/salt/modules/win_repo.py +++ b/salt/modules/win_repo.py @@ -25,7 +25,8 @@ from salt.exceptions import CommandExecutionError, SaltRenderError from salt.runners.winrepo import ( genrepo as _genrepo, update_git_repos as _update_git_repos, - PER_REMOTE_OVERRIDES + PER_REMOTE_OVERRIDES, + PER_REMOTE_ONLY ) from salt.ext import six try: diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py index a2d1dcb901..2c3bed7abf 100644 --- a/salt/pillar/__init__.py +++ b/salt/pillar/__init__.py @@ -763,9 +763,12 @@ class Pillar(object): and self.opts.get('__role') != 'minion': # Avoid circular import import salt.utils.gitfs - from salt.pillar.git_pillar import PER_REMOTE_OVERRIDES + import salt.pillar.git_pillar git_pillar = salt.utils.gitfs.GitPillar(self.opts) - git_pillar.init_remotes(self.ext['git'], PER_REMOTE_OVERRIDES) + git_pillar.init_remotes( + self.ext['git'], + salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, + salt.pillar.git_pillar.PER_REMOTE_ONLY) git_pillar.fetch_remotes() except TypeError: # Handle malformed ext_pillar diff --git a/salt/pillar/git_pillar.py b/salt/pillar/git_pillar.py index b3e687c866..66592d9896 100644 --- a/salt/pillar/git_pillar.py +++ b/salt/pillar/git_pillar.py @@ -328,6 +328,12 @@ except ImportError: PER_REMOTE_OVERRIDES = ('env', 'root', 'ssl_verify') +# Fall back to default per-remote-only. This isn't technically needed since +# salt.utils.gitfs.GitBase.init_remotes() will default to +# salt.utils.gitfs.PER_REMOTE_ONLY for this value, so this is mainly for +# runners and other modules that import salt.pillar.git_pillar. +PER_REMOTE_ONLY = salt.utils.gitfs.PER_REMOTE_ONLY + # Set up logging log = logging.getLogger(__name__) @@ -380,7 +386,7 @@ def ext_pillar(minion_id, repo, pillar_dirs): opts['pillar_roots'] = {} opts['__git_pillar'] = True pillar = salt.utils.gitfs.GitPillar(opts) - pillar.init_remotes(repo, PER_REMOTE_OVERRIDES) + pillar.init_remotes(repo, PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) if __opts__.get('__role') == 'minion': # If masterless, fetch the remotes. We'll need to remove this once # we make the minion daemon able to run standalone. diff --git a/salt/runners/cache.py b/salt/runners/cache.py index f19c94eff0..879f4ed030 100644 --- a/salt/runners/cache.py +++ b/salt/runners/cache.py @@ -11,12 +11,11 @@ import salt.log import salt.utils import salt.utils.master import salt.payload +import salt.fileserver.gitfs +import salt.pillar.git_pillar +import salt.runners.winrepo from salt.exceptions import SaltInvocationError from salt.fileserver import clear_lock as _clear_lock -from salt.fileserver.gitfs import PER_REMOTE_OVERRIDES as __GITFS_OVERRIDES -from salt.pillar.git_pillar \ - import PER_REMOTE_OVERRIDES as __GIT_PILLAR_OVERRIDES -from salt.runners.winrepo import PER_REMOTE_OVERRIDES as __WINREPO_OVERRIDES log = logging.getLogger(__name__) @@ -213,8 +212,10 @@ def clear_git_lock(role, remote=None, **kwargs): if role == 'gitfs': git_objects = [salt.utils.gitfs.GitFS(__opts__)] - git_objects[0].init_remotes(__opts__['gitfs_remotes'], - __GITFS_OVERRIDES) + git_objects[0].init_remotes( + __opts__['gitfs_remotes'], + salt.fileserver.gitfs.PER_REMOTE_OVERRIDES, + salt.fileserver.gitfs.PER_REMOTE_ONLY) elif role == 'git_pillar': git_objects = [] for ext_pillar in __opts__['ext_pillar']: @@ -223,7 +224,10 @@ def clear_git_lock(role, remote=None, **kwargs): if not isinstance(ext_pillar['git'], list): continue obj = salt.utils.gitfs.GitPillar(__opts__) - obj.init_remotes(ext_pillar['git'], __GIT_PILLAR_OVERRIDES) + obj.init_remotes( + ext_pillar['git'], + salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, + salt.pillar.git_pillar.PER_REMOTE_ONLY) git_objects.append(obj) elif role == 'winrepo': if 'win_repo' in __opts__: @@ -252,7 +256,10 @@ def clear_git_lock(role, remote=None, **kwargs): (__opts__['winrepo_remotes_ng'], __opts__['winrepo_dir_ng']) ): obj = salt.utils.gitfs.WinRepo(__opts__, base_dir) - obj.init_remotes(remotes, __WINREPO_OVERRIDES) + obj.init_remotes( + remotes, + salt.runners.winrepo.PER_REMOTE_OVERRIDES, + salt.runners.winrepo.PER_REMOTE_ONLY) git_objects.append(obj) else: raise SaltInvocationError('Invalid role \'{0}\''.format(role)) diff --git a/salt/runners/git_pillar.py b/salt/runners/git_pillar.py index 8dfc4412eb..0e8e97beb3 100644 --- a/salt/runners/git_pillar.py +++ b/salt/runners/git_pillar.py @@ -86,7 +86,8 @@ def update(branch=None, repo=None): else: pillar = salt.utils.gitfs.GitPillar(__opts__) pillar.init_remotes(pillar_conf, - salt.pillar.git_pillar.PER_REMOTE_OVERRIDES) + salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, + salt.pillar.git_pillar.PER_REMOTE_ONLY) for remote in pillar.remotes: # Skip this remote if it doesn't match the search criteria if branch is not None: diff --git a/salt/runners/winrepo.py b/salt/runners/winrepo.py index 56dc6fb9c0..5127b0c51e 100644 --- a/salt/runners/winrepo.py +++ b/salt/runners/winrepo.py @@ -31,6 +31,12 @@ log = logging.getLogger(__name__) # Global parameters which can be overridden on a per-remote basis PER_REMOTE_OVERRIDES = ('ssl_verify',) +# Fall back to default per-remote-only. This isn't technically needed since +# salt.utils.gitfs.GitBase.init_remotes() will default to +# salt.utils.gitfs.PER_REMOTE_ONLY for this value, so this is mainly for +# runners and other modules that import salt.runners.winrepo. +PER_REMOTE_ONLY = salt.utils.gitfs.PER_REMOTE_ONLY + def genrepo(opts=None, fire_event=True): ''' @@ -260,7 +266,8 @@ def update_git_repos(opts=None, clean=False, masterless=False): # New winrepo code utilizing salt.utils.gitfs try: winrepo = salt.utils.gitfs.WinRepo(opts, base_dir) - winrepo.init_remotes(remotes, PER_REMOTE_OVERRIDES) + winrepo.init_remotes( + remotes, PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) winrepo.fetch_remotes() # Since we're not running update(), we need to manually call # clear_old_remotes() to remove directories from remotes that From 5b3be6e8afb74cc3e886b77f73692c0e61551819 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 25 Sep 2017 16:06:33 -0500 Subject: [PATCH 244/633] Fix failing unit test --- salt/modules/state.py | 5 ++++- tests/unit/modules/state_test.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/salt/modules/state.py b/salt/modules/state.py index 15ad2f2a4a..e05e41983f 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -1305,7 +1305,10 @@ def sls_id(id_, mods, test=None, queue=False, **kwargs): errors += st_.state.verify_high(high_) # Apply requisites to high data high_, req_in_errors = st_.state.requisite_in(high_) - errors.extend(req_in_errors) + if req_in_errors: + # This if statement should not be necessary if there were no errors, + # but it is required to get the unit tests to pass. + errors.extend(req_in_errors) if errors: __context__['retcode'] = 1 return errors diff --git a/tests/unit/modules/state_test.py b/tests/unit/modules/state_test.py index 3f27bd94c6..1d3ebdba51 100644 --- a/tests/unit/modules/state_test.py +++ b/tests/unit/modules/state_test.py @@ -132,6 +132,9 @@ class MockState(object): data = data return True + def requisite_in(self, data): # pylint: disable=unused-argument + return data, [] + class HighState(object): ''' Mock HighState class From dc1b36b7e239fd84ad0241b9a7ddd34b338340a6 Mon Sep 17 00:00:00 2001 From: twangboy Date: Mon, 25 Sep 2017 15:06:44 -0600 Subject: [PATCH 245/633] Change expected return for Windows --- tests/unit/beacons/test_status.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/unit/beacons/test_status.py b/tests/unit/beacons/test_status.py index fca7576344..4ab3d83a77 100644 --- a/tests/unit/beacons/test_status.py +++ b/tests/unit/beacons/test_status.py @@ -12,6 +12,7 @@ # Python libs from __future__ import absolute_import +import sys # Salt libs import salt.config @@ -45,14 +46,32 @@ class StatusBeaconTestCase(TestCase, LoaderModuleMockMixin): def test_empty_config(self, *args, **kwargs): config = {} ret = status.beacon(config) - self.assertEqual(sorted(list(ret[0]['data'])), sorted(['loadavg', 'meminfo', 'cpustats', 'vmstats', 'time'])) + + if sys.platform.startswith('win'): + expected = [] + else: + expected = sorted(['loadavg', 'meminfo', 'cpustats', 'vmstats', 'time']) + + self.assertEqual(sorted(list(ret[0]['data'])), expected) def test_deprecated_dict_config(self): config = {'time': ['all']} ret = status.beacon(config) - self.assertEqual(list(ret[0]['data']), ['time']) + + if sys.platform.startswith('win'): + expected = [] + else: + expected = ['time'] + + self.assertEqual(list(ret[0]['data']), expected) def test_list_config(self): config = [{'time': ['all']}] ret = status.beacon(config) - self.assertEqual(list(ret[0]['data']), ['time']) + + if sys.platform.startswith('win'): + expected = [] + else: + expected = ['time'] + + self.assertEqual(list(ret[0]['data']), expected) From 922e60fa673d656462b9ff2b11f17f878a834e40 Mon Sep 17 00:00:00 2001 From: twangboy Date: Mon, 25 Sep 2017 15:25:31 -0600 Subject: [PATCH 246/633] Add os agnostic paths --- tests/unit/modules/test_poudriere.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/modules/test_poudriere.py b/tests/unit/modules/test_poudriere.py index 52e8f322e3..9a181b59c5 100644 --- a/tests/unit/modules/test_poudriere.py +++ b/tests/unit/modules/test_poudriere.py @@ -50,10 +50,12 @@ class PoudriereTestCase(TestCase, LoaderModuleMockMixin): ''' Test if it make jail ``jname`` pkgng aware. ''' - ret1 = 'Could not create or find required directory /tmp/salt' - ret2 = 'Looks like file /tmp/salt/salt-make.conf could not be created' - ret3 = {'changes': 'Created /tmp/salt/salt-make.conf'} - mock = MagicMock(return_value='/tmp/salt') + temp_dir = os.path.join('tmp', 'salt') + conf_file = os.path.join('tmp', 'salt', 'salt-make.conf') + ret1 = 'Could not create or find required directory {0}'.format(temp_dir) + ret2 = 'Looks like file {0} could not be created'.format(conf_file) + ret3 = {'changes': 'Created {0}'.format(conf_file)} + mock = MagicMock(return_value=temp_dir) mock_true = MagicMock(return_value=True) with patch.dict(poudriere.__salt__, {'config.option': mock, 'file.write': mock_true}): From c369e337e4d8b0a6eee894e075cc1ebe688fbcff Mon Sep 17 00:00:00 2001 From: Eric Radman Date: Mon, 25 Sep 2017 16:47:20 -0400 Subject: [PATCH 247/633] Skip ZFS module check on OpenBSD Avoids the following error when running `salt-call` on OpenBSD: [ERROR ] Command '/usr/sbin/rcctl get zfs-fuse' failed with return code: 2 [ERROR ] output: rcctl: service zfs-fuse does not exist --- salt/modules/zfs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/salt/modules/zfs.py b/salt/modules/zfs.py index dc42400796..fd8b291f82 100644 --- a/salt/modules/zfs.py +++ b/salt/modules/zfs.py @@ -77,6 +77,9 @@ def __virtual__(): ) == 0: return 'zfs' + if __grains__['kernel'] == 'OpenBSD': + return False + _zfs_fuse = lambda f: __salt__['service.' + f]('zfs-fuse') if _zfs_fuse('available') and (_zfs_fuse('status') or _zfs_fuse('start')): return 'zfs' From e5ebd28ee12fc06465441936e895db03e667b98f Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 12:34:25 -0400 Subject: [PATCH 248/633] Added get_new_service_instance_stub that creates a new service instance stub --- salt/utils/vmware.py | 52 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index d54dbced04..cbfb741dc0 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- ''' +import sys +import ssl Connection library for VMware .. versionadded:: 2015.8.2 @@ -79,6 +81,8 @@ import atexit import errno import logging import time +import sys +import ssl # Import Salt Libs import salt.exceptions @@ -92,8 +96,9 @@ import salt.utils.stringutils from salt.ext import six from salt.ext.six.moves.http_client import BadStatusLine # pylint: disable=E0611 try: - from pyVim.connect import GetSi, SmartConnect, Disconnect, GetStub - from pyVmomi import vim, vmodl + from pyVim.connect import GetSi, SmartConnect, Disconnect, GetStub, \ + SoapStubAdapter + from pyVmomi import vim, vmodl, VmomiSupport HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False @@ -405,6 +410,49 @@ def get_service_instance(host, username=None, password=None, protocol=None, return service_instance +def get_new_service_instance_stub(service_instance, path, ns=None, + version=None): + ''' + Returns a stub that points to a different path, + created from an existing connection. + + service_instance + The Service Instance. + + path + Path of the new stub. + + ns + Namespace of the new stub. + Default value is None + + version + Version of the new stub. + Default value is None. + ''' + #For python 2.7.9 and later, the defaul SSL conext has more strict + #connection handshaking rule. We may need turn of the hostname checking + #and client side cert verification + context = None + if sys.version_info[:3] > (2,7,8): + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + stub = service_instance._stub + hostname = stub.host.split(':')[0] + session_cookie = stub.cookie.split('"')[1] + VmomiSupport.GetRequestContext()['vcSessionCookie'] = session_cookie + new_stub = SoapStubAdapter(host=hostname, + ns=ns, + path=path, + version=version, + poolSize=0, + sslContext=context) + new_stub.cookie = stub.cookie + return new_stub + + def get_service_instance_from_managed_object(mo_ref, name=''): ''' Retrieves the service instance from a managed object. From dd54f8ab15fc5038d356b49cb8b945e04344bff2 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 13:16:58 -0400 Subject: [PATCH 249/633] Added tests for salt.utils.vmware.get_new_service_instance_stub --- tests/unit/utils/vmware/test_connection.py | 93 +++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/tests/unit/utils/vmware/test_connection.py b/tests/unit/utils/vmware/test_connection.py index 4a95e9b67f..dd357d4870 100644 --- a/tests/unit/utils/vmware/test_connection.py +++ b/tests/unit/utils/vmware/test_connection.py @@ -13,6 +13,7 @@ import ssl import sys # Import Salt testing libraries +from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import TestCase, skipIf from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, call, \ PropertyMock @@ -24,7 +25,7 @@ import salt.utils.vmware from salt.ext import six try: - from pyVmomi import vim, vmodl + from pyVmomi import vim, vmodl, VmomiSupport HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False @@ -852,6 +853,96 @@ class IsConnectionToAVCenterTestCase(TestCase): excinfo.exception.strerror) +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetNewServiceInstanceStub(TestCase, LoaderModuleMockMixin): + '''Tests for salt.utils.vmware.get_new_service_instance_stub''' + def setup_loader_modules(self): + return {salt.utils.vmware: { + '__virtual__': MagicMock(return_value='vmware'), + 'sys': MagicMock(), + 'ssl': MagicMock()}} + + def setUp(self): + self.mock_stub = MagicMock( + host='fake_host:1000', + cookie='ignore"fake_cookie') + self.mock_si = MagicMock( + _stub=self.mock_stub) + self.mock_ret = MagicMock() + self.mock_new_stub = MagicMock() + self.context_dict = {} + patches = (('salt.utils.vmware.VmomiSupport.GetRequestContext', + MagicMock( + return_value=self.context_dict)), + ('salt.utils.vmware.SoapStubAdapter', + MagicMock(return_value=self.mock_new_stub))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + type(salt.utils.vmware.sys).version_info = \ + PropertyMock(return_value=(2, 7, 9)) + self.mock_context = MagicMock() + self.mock_create_default_context = \ + MagicMock(return_value=self.mock_context) + salt.utils.vmware.ssl.create_default_context = \ + self.mock_create_default_context + + def tearDown(self): + for attr in ('mock_stub', 'mock_si', 'mock_ret', 'mock_new_stub', + 'context_dict', 'mock_context', + 'mock_create_default_context'): + delattr(self, attr) + + def test_ssl_default_context_loaded(self): + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path') + self.mock_create_default_context.assert_called_once_with() + self.assertFalse(self.mock_context.check_hostname) + self.assertEqual(self.mock_context.verify_mode, + salt.utils.vmware.ssl.CERT_NONE) + + def test_ssl_default_context_not_loaded(self): + type(salt.utils.vmware.sys).version_info = \ + PropertyMock(return_value=(2, 7, 8)) + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path') + self.assertEqual(self.mock_create_default_context.call_count, 0) + + def test_session_cookie_in_context(self): + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path') + self.assertEqual(self.context_dict['vcSessionCookie'], 'fake_cookie') + + def test_get_new_stub(self): + mock_get_new_stub = MagicMock() + with patch('salt.utils.vmware.SoapStubAdapter', mock_get_new_stub): + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path', 'fake_ns', 'fake_version') + mock_get_new_stub.assert_called_once_with( + host='fake_host', ns='fake_ns', path='fake_path', + version='fake_version', poolSize=0, sslContext=self.mock_context) + + def test_get_new_stub_2_7_8_python(self): + type(salt.utils.vmware.sys).version_info = \ + PropertyMock(return_value=(2, 7, 8)) + mock_get_new_stub = MagicMock() + with patch('salt.utils.vmware.SoapStubAdapter', mock_get_new_stub): + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path', 'fake_ns', 'fake_version') + mock_get_new_stub.assert_called_once_with( + host='fake_host', ns='fake_ns', path='fake_path', + version='fake_version', poolSize=0, sslContext=None) + + def test_new_stub_returned(self): + ret = salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path') + self.assertEqual(self.mock_new_stub.cookie, 'ignore"fake_cookie') + self.assertEqual(ret, self.mock_new_stub) + + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class GetServiceInstanceFromManagedObjectTestCase(TestCase): From 3e8ed5934d97e33a2dd5f1d19841b4e15cb87b16 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 13:33:12 -0400 Subject: [PATCH 250/633] Added initial sysdoc and imports of salt.utils.pbm --- salt/utils/pbm.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 salt/utils/pbm.py diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py new file mode 100644 index 0000000000..9d9e7bb989 --- /dev/null +++ b/salt/utils/pbm.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +''' +Library for VMware Storage Policy management (via the pbm endpoint) + +This library is used to manage the various policies available in VMware + +:codeauthor: Alexandru Bleotu + +Dependencies +~~~~~~~~~~~~ + +- pyVmomi Python Module + +pyVmomi +------- + +PyVmomi can be installed via pip: + +.. code-block:: bash + + pip install pyVmomi + +.. note:: + + versions of Python. If using version 6.0 of pyVmomi, Python 2.6, + Python 2.7.9, or newer must be present. This is due to an upstream dependency + in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the + version of Python is not in the supported range, you will need to install an + earlier version of pyVmomi. See `Issue #29537`_ for more information. + +.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 + +Based on the note above, to install an earlier version of pyVmomi than the +version currently listed in PyPi, run the following: + +.. code-block:: bash + + pip install pyVmomi==5.5.0.2014.1.1 +''' + +# Import Python Libs +from __future__ import absolute_import +import logging + +# Import Salt Libs +import salt.utils.vmware +from salt.exceptions import VMwareApiError, VMwareRuntimeError, \ + VMwareObjectRetrievalError + + +try: + from pyVmomi import pbm, vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +# Get Logging Started +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only load if PyVmomi is installed. + ''' + if HAS_PYVMOMI: + return True + else: + return False, 'Missing dependency: The salt.utils.pbm module ' \ + 'requires the pyvmomi library' From e77b912f2cb65b1901f2a297386b50f55a826dc8 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 13:34:33 -0400 Subject: [PATCH 251/633] Added salt.utils.pbm.get_profile_manager --- salt/utils/pbm.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index 9d9e7bb989..aec5341112 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -68,3 +68,28 @@ def __virtual__(): else: return False, 'Missing dependency: The salt.utils.pbm module ' \ 'requires the pyvmomi library' + + +def get_profile_manager(service_instance): + ''' + Returns a profile manager + + service_instance + Service instance to the host or vCenter + ''' + stub = salt.utils.vmware.get_new_service_instance_stub( + service_instance, ns='pbm/2.0', path='/pbm/sdk') + pbm_si = pbm.ServiceInstance('ServiceInstance', stub) + try: + profile_manager = pbm_si.RetrieveContent().profileManager + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + return profile_manager From 6b2ddffb4c7a0585ddbce6d4c9fad8c7c150fc97 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 13:35:04 -0400 Subject: [PATCH 252/633] Added tests for salt.utils.pbm.get_profile_manager --- tests/unit/utils/test_pbm.py | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/unit/utils/test_pbm.py diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py new file mode 100644 index 0000000000..11256c9b32 --- /dev/null +++ b/tests/unit/utils/test_pbm.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu ` + + Tests functions in salt.utils.vsan +''' + +# Import python libraries +from __future__ import absolute_import +import logging + +# Import Salt testing libraries +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, \ + PropertyMock + +# Import Salt libraries +from salt.exceptions import VMwareApiError, VMwareRuntimeError +import salt.utils.pbm + +try: + from pyVmomi import vim, vmodl, pbm + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +# Get Logging Started +log = logging.getLogger(__name__) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetProfileManagerTestCase(TestCase): + '''Tests for salt.utils.pbm.get_profile_manager''' + def setUp(self): + self.mock_si = MagicMock() + self.mock_stub = MagicMock() + self.mock_prof_mgr = MagicMock() + self.mock_content = MagicMock() + self.mock_pbm_si = MagicMock( + RetrieveContent=MagicMock(return_value=self.mock_content)) + type(self.mock_content).profileManager = \ + PropertyMock(return_value=self.mock_prof_mgr) + patches = ( + ('salt.utils.vmware.get_new_service_instance_stub', + MagicMock(return_value=self.mock_stub)), + ('salt.utils.pbm.pbm.ServiceInstance', + MagicMock(return_value=self.mock_pbm_si))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_stub', 'mock_content', + 'mock_pbm_si', 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_new_service_stub(self): + mock_get_new_service_stub = MagicMock() + with patch('salt.utils.vmware.get_new_service_instance_stub', + mock_get_new_service_stub): + salt.utils.pbm.get_profile_manager(self.mock_si) + mock_get_new_service_stub.assert_called_once_with( + self.mock_si, ns='pbm/2.0', path='/pbm/sdk') + + def test_pbm_si(self): + mock_get_pbm_si = MagicMock() + with patch('salt.utils.pbm.pbm.ServiceInstance', + mock_get_pbm_si): + salt.utils.pbm.get_profile_manager(self.mock_si) + mock_get_pbm_si.assert_called_once_with('ServiceInstance', + self.mock_stub) + + def test_return_profile_manager(self): + ret = salt.utils.pbm.get_profile_manager(self.mock_si) + self.assertEqual(ret, self.mock_prof_mgr) + + def test_profile_manager_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_content).profileManager = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_profile_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_profile_manager_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_content).profileManager = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_profile_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_profile_manager_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_content).profileManager = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_profile_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') From c790107d17ba097dcdc71cfc21b6d6cf126665bd Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 13:42:21 -0400 Subject: [PATCH 253/633] Added salt.utils.pbm.get_placement_solver --- salt/utils/pbm.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index aec5341112..eb2cf26887 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -93,3 +93,28 @@ def get_profile_manager(service_instance): log.exception(exc) raise VMwareRuntimeError(exc.msg) return profile_manager + + +def get_placement_solver(service_instance): + ''' + Returns a placement solver + + service_instance + Service instance to the host or vCenter + ''' + stub = salt.utils.vmware.get_new_service_instance_stub( + service_instance, ns='pbm/2.0', path='/pbm/sdk') + pbm_si = pbm.ServiceInstance('ServiceInstance', stub) + try: + profile_manager = pbm_si.RetrieveContent().placementSolver + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + return profile_manager From 68f48d123ae51a66f61cefe844d39ce1c34af697 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 13:42:56 -0400 Subject: [PATCH 254/633] Added tests for salt.utils.pbm.get_placement_solver --- tests/unit/utils/test_pbm.py | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index 11256c9b32..8bdcbaa075 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -103,3 +103,78 @@ class GetProfileManagerTestCase(TestCase): with self.assertRaises(VMwareRuntimeError) as excinfo: salt.utils.pbm.get_profile_manager(self.mock_si) self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetPlacementSolverTestCase(TestCase): + '''Tests for salt.utils.pbm.get_placement_solver''' + def setUp(self): + self.mock_si = MagicMock() + self.mock_stub = MagicMock() + self.mock_prof_mgr = MagicMock() + self.mock_content = MagicMock() + self.mock_pbm_si = MagicMock( + RetrieveContent=MagicMock(return_value=self.mock_content)) + type(self.mock_content).placementSolver = \ + PropertyMock(return_value=self.mock_prof_mgr) + patches = ( + ('salt.utils.vmware.get_new_service_instance_stub', + MagicMock(return_value=self.mock_stub)), + ('salt.utils.pbm.pbm.ServiceInstance', + MagicMock(return_value=self.mock_pbm_si))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_stub', 'mock_content', + 'mock_pbm_si', 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_new_service_stub(self): + mock_get_new_service_stub = MagicMock() + with patch('salt.utils.vmware.get_new_service_instance_stub', + mock_get_new_service_stub): + salt.utils.pbm.get_placement_solver(self.mock_si) + mock_get_new_service_stub.assert_called_once_with( + self.mock_si, ns='pbm/2.0', path='/pbm/sdk') + + def test_pbm_si(self): + mock_get_pbm_si = MagicMock() + with patch('salt.utils.pbm.pbm.ServiceInstance', + mock_get_pbm_si): + salt.utils.pbm.get_placement_solver(self.mock_si) + mock_get_pbm_si.assert_called_once_with('ServiceInstance', + self.mock_stub) + + def test_return_profile_manager(self): + ret = salt.utils.pbm.get_placement_solver(self.mock_si) + self.assertEqual(ret, self.mock_prof_mgr) + + def test_placement_solver_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_content).placementSolver = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_placement_solver(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_placement_solver_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_content).placementSolver = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_placement_solver(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_placement_solver_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_content).placementSolver = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_placement_solver(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') From eac509bab8bf1a6d8f9102b59f6e1daa3618984b Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 18:39:47 -0400 Subject: [PATCH 255/633] Added salt.utils.pbm.get_capability_definitions --- salt/utils/pbm.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index eb2cf26887..5ca85ce4d9 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -118,3 +118,30 @@ def get_placement_solver(service_instance): log.exception(exc) raise VMwareRuntimeError(exc.msg) return profile_manager + + +def get_capability_definitions(profile_manager): + ''' + Returns a list of all capability definitions. + + profile_manager + Reference to the profile manager. + ''' + res_type = pbm.profile.ResourceType( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + try: + cap_categories = profile_manager.FetchCapabilityMetadata(res_type) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + cap_definitions = [] + for cat in cap_categories: + cap_definitions.extend(cat.capabilityMetadata) + return cap_definitions From e980407c54dea63ea35e642bb9ef2ba2d3bdc6a9 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 18:40:41 -0400 Subject: [PATCH 256/633] Added tests for salt.utils.pbm.get_capability_definitions --- tests/unit/utils/test_pbm.py | 71 ++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index 8bdcbaa075..d59ce1afdd 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -178,3 +178,74 @@ class GetPlacementSolverTestCase(TestCase): with self.assertRaises(VMwareRuntimeError) as excinfo: salt.utils.pbm.get_placement_solver(self.mock_si) self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetCapabilityDefinitionsTestCase(TestCase): + '''Tests for salt.utils.pbm.get_capability_definitions''' + def setUp(self): + self.mock_res_type = MagicMock() + self.mock_cap_cats =[MagicMock(capabilityMetadata=['fake_cap_meta1', + 'fake_cap_meta2']), + MagicMock(capabilityMetadata=['fake_cap_meta3'])] + self.mock_prof_mgr = MagicMock( + FetchCapabilityMetadata=MagicMock(return_value=self.mock_cap_cats)) + patches = ( + ('salt.utils.pbm.pbm.profile.ResourceType', + MagicMock(return_value=self.mock_res_type)),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_res_type', 'mock_cap_cats', 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_res_type(self): + mock_get_res_type = MagicMock() + with patch('salt.utils.pbm.pbm.profile.ResourceType', + mock_get_res_type): + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + mock_get_res_type.assert_called_once_with( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + + def test_fetch_capabilities(self): + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.mock_prof_mgr.FetchCapabilityMetadata.assert_callend_once_with( + self.mock_res_type) + + def test_fetch_capabilities_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.FetchCapabilityMetadata = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_fetch_capabilities_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.FetchCapabilityMetadata = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_fetch_capabilities_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.FetchCapabilityMetadata = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_return_cap_definitions(self): + ret = salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.assertEqual(ret, ['fake_cap_meta1', 'fake_cap_meta2', + 'fake_cap_meta3']) From f42de9c66b9e8df28dc10b1d32a197990ec1a849 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 18:41:41 -0400 Subject: [PATCH 257/633] Added salt.utils.pbm.get_policies_by_id --- salt/utils/pbm.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index 5ca85ce4d9..bf589b06c0 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -145,3 +145,27 @@ def get_capability_definitions(profile_manager): for cat in cap_categories: cap_definitions.extend(cat.capabilityMetadata) return cap_definitions + + +def get_policies_by_id(profile_manager, policy_ids): + ''' + Returns a list of policies with the specified ids. + + profile_manager + Reference to the profile manager. + + policy_ids + List of policy ids to retrieve. + ''' + try: + return profile_manager.RetrieveContent(policy_ids) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) From d8e0cbde9ac679c7beb64ea91b892e717dc31523 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 18:42:29 -0400 Subject: [PATCH 258/633] Added tests for salt.utils.pbm.get_policies_by_id --- tests/unit/utils/test_pbm.py | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index d59ce1afdd..100a7313bf 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -249,3 +249,53 @@ class GetCapabilityDefinitionsTestCase(TestCase): ret = salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) self.assertEqual(ret, ['fake_cap_meta1', 'fake_cap_meta2', 'fake_cap_meta3']) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetPoliciesById(TestCase): + '''Tests for salt.utils.pbm.get_policies_by_id''' + def setUp(self): + self.policy_ids = MagicMock() + self.mock_policies = MagicMock() + self.mock_prof_mgr = MagicMock( + RetrieveContent=MagicMock(return_value=self.mock_policies)) + + def tearDown(self): + for attr in ('policy_ids', 'mock_policies', 'mock_prof_mgr'): + delattr(self, attr) + + def test_retrieve_policies(self): + salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.mock_prof_mgr.RetrieveContent.assert_callend_once_with( + self.policy_ids) + + def test_retrieve_policies_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.RetrieveContent = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_retrieve_policies_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.RetrieveContent = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_retrieve_policies_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.RetrieveContent = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_return_policies(self): + ret = salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.assertEqual(ret, self.mock_policies) From df16bdb686446867c2f1e79c43bbfdc069232b0e Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 18:43:22 -0400 Subject: [PATCH 259/633] Added salt.utils.pbm.get_storage_policies --- salt/utils/pbm.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index bf589b06c0..8bab714435 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -169,3 +169,42 @@ def get_policies_by_id(profile_manager, policy_ids): except vmodl.RuntimeFault as exc: log.exception(exc) raise VMwareRuntimeError(exc.msg) + + +def get_storage_policies(profile_manager, policy_names=[], + get_all_policies=False): + ''' + Returns a list of the storage policies, filtered by name. + + profile_manager + Reference to the profile manager. + + policy_names + List of policy names to filter by. + + get_all_policies + Flag specifying to return all policies, regardless of the specified + filter. + ''' + res_type = pbm.profile.ResourceType( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + try: + policy_ids = profile_manager.QueryProfile(res_type) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + log.trace('policy_ids = {0}'.format(policy_ids)) + # More policies are returned so we need to filter again + policies = [p for p in get_policies_by_id(profile_manager, policy_ids) + if p.resourceType.resourceType == + pbm.profile.ResourceTypeEnum.STORAGE] + if get_all_policies: + return policies + return [p for p in policies if p.name in policy_names] From 75764567c44002a3e71a8ca375a37b6c3dad3a09 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 18:44:07 -0400 Subject: [PATCH 260/633] Added tests for salt.utils.pbm.get_storage_policies --- tests/unit/utils/test_pbm.py | 90 ++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index 100a7313bf..829f6c293a 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -299,3 +299,93 @@ class GetPoliciesById(TestCase): def test_return_policies(self): ret = salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) self.assertEqual(ret, self.mock_policies) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetStoragePoliciesTestCase(TestCase): + '''Tests for salt.utils.pbm.get_storage_policies''' + def setUp(self): + self.mock_res_type = MagicMock() + self.mock_policy_ids = MagicMock() + self.mock_prof_mgr = MagicMock( + QueryProfile=MagicMock(return_value=self.mock_policy_ids)) + # Policies + self.mock_policies=[] + for i in range(4): + mock_obj = MagicMock(resourceType=MagicMock( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE)) + mock_obj.name = 'fake_policy{0}'.format(i) + self.mock_policies.append(mock_obj) + patches = ( + ('salt.utils.pbm.pbm.profile.ResourceType', + MagicMock(return_value=self.mock_res_type)), + ('salt.utils.pbm.get_policies_by_id', + MagicMock(return_value=self.mock_policies))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_res_type', 'mock_policy_ids', 'mock_policies', + 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_res_type(self): + mock_get_res_type = MagicMock() + with patch('salt.utils.pbm.pbm.profile.ResourceType', + mock_get_res_type): + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + mock_get_res_type.assert_called_once_with( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + + def test_retrieve_policy_ids(self): + mock_retrieve_policy_ids = MagicMock(return_value=self.mock_policy_ids) + self.mock_prof_mgr.QueryProfile = mock_retrieve_policy_ids + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + mock_retrieve_policy_ids.asser_called_once_with(self.mock_res_type) + + def test_retrieve_policy_ids_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.QueryProfile = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_retrieve_policy_ids_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.QueryProfile = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_retrieve_policy_ids_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.QueryProfile = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_get_policies_by_id(self): + mock_get_policies_by_id = MagicMock(return_value=self.mock_policies) + with patch('salt.utils.pbm.get_policies_by_id', + mock_get_policies_by_id): + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + mock_get_policies_by_id.assert_called_once_with( + self.mock_prof_mgr, self.mock_policy_ids) + + def test_return_all_policies(self): + ret = salt.utils.pbm.get_storage_policies(self.mock_prof_mgr, + get_all_policies=True) + self.assertEqual(ret, self.mock_policies) + + def test_return_filtered_policies(self): + ret = salt.utils.pbm.get_storage_policies( + self.mock_prof_mgr, policy_names=['fake_policy1', 'fake_policy3']) + self.assertEqual(ret, [self.mock_policies[1], self.mock_policies[3]]) From d3744c80030a1d54c2978fa73fa7ed80eac76f35 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 19:44:48 -0400 Subject: [PATCH 261/633] Added salt.utils.pbm.create_storage_policy --- salt/utils/pbm.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index 8bab714435..eb45c96da2 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -208,3 +208,27 @@ def get_storage_policies(profile_manager, policy_names=[], if get_all_policies: return policies return [p for p in policies if p.name in policy_names] + + +def create_storage_policy(profile_manager, policy_spec): + ''' + Creates a storage policy. + + profile_manager + Reference to the profile manager. + + policy_spec + Policy update spec. + ''' + try: + profile_manager.Create(policy_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) From c80df65776c9caaafa7c8e900bf9b5b9705dfa10 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 19:46:47 -0400 Subject: [PATCH 262/633] Fixed tests for salt.utils.pbm.get_policies_by_id --- tests/unit/utils/test_pbm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index 829f6c293a..538448720d 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -253,7 +253,7 @@ class GetCapabilityDefinitionsTestCase(TestCase): @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') -class GetPoliciesById(TestCase): +class GetPoliciesByIdTestCase(TestCase): '''Tests for salt.utils.pbm.get_policies_by_id''' def setUp(self): self.policy_ids = MagicMock() @@ -344,7 +344,7 @@ class GetStoragePoliciesTestCase(TestCase): mock_retrieve_policy_ids = MagicMock(return_value=self.mock_policy_ids) self.mock_prof_mgr.QueryProfile = mock_retrieve_policy_ids salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) - mock_retrieve_policy_ids.asser_called_once_with(self.mock_res_type) + mock_retrieve_policy_ids.assert_called_once_with(self.mock_res_type) def test_retrieve_policy_ids_raises_no_permissions(self): exc = vim.fault.NoPermission() From d43e3421350fb5ef81bb68780004dceda4d26ac8 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 19:47:36 -0400 Subject: [PATCH 263/633] Added tests for salt.utils.pbm.create_storage_policy --- tests/unit/utils/test_pbm.py | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index 538448720d..789d0c56d4 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -389,3 +389,51 @@ class GetStoragePoliciesTestCase(TestCase): ret = salt.utils.pbm.get_storage_policies( self.mock_prof_mgr, policy_names=['fake_policy1', 'fake_policy3']) self.assertEqual(ret, [self.mock_policies[1], self.mock_policies[3]]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class CreateStoragePolicyTestCase(TestCase): + '''Tests for salt.utils.pbm.create_storage_policy''' + def setUp(self): + self.mock_policy_spec = MagicMock() + self.mock_prof_mgr = MagicMock() + + def tearDown(self): + for attr in ('mock_policy_spec', 'mock_prof_mgr'): + delattr(self, attr) + + def test_create_policy(self): + salt.utils.pbm.create_storage_policy(self.mock_prof_mgr, + self.mock_policy_spec) + self.mock_prof_mgr.Create.assert_called_once_with( + self.mock_policy_spec) + + def test_create_policy_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.Create = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.create_storage_policy(self.mock_prof_mgr, + self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_create_policy_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.Create = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.create_storage_policy(self.mock_prof_mgr, + self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_create_policy_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.Create = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.create_storage_policy(self.mock_prof_mgr, + self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') From 9c05f7c7341ee1b1de218c299d04be800e4e10d3 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 19:49:24 -0400 Subject: [PATCH 264/633] Added salt.utils.pbm.update_storage_policy --- salt/utils/pbm.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index eb45c96da2..57d2f598d4 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -232,3 +232,30 @@ def create_storage_policy(profile_manager, policy_spec): except vmodl.RuntimeFault as exc: log.exception(exc) raise VMwareRuntimeError(exc.msg) + + +def update_storage_policy(profile_manager, policy, policy_spec): + ''' + Updates a storage policy. + + profile_manager + Reference to the profile manager. + + policy + Reference to the policy to be updated. + + policy_spec + Policy update spec. + ''' + try: + profile_manager.Update(policy.profileId, policy_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) From 79419702d934e53a0f621da3bb47328a9928ca62 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 19:50:14 -0400 Subject: [PATCH 265/633] Added tests for salt.utils.pbm.update_storage_policy --- tests/unit/utils/test_pbm.py | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index 789d0c56d4..f398f5a4ea 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -437,3 +437,52 @@ class CreateStoragePolicyTestCase(TestCase): salt.utils.pbm.create_storage_policy(self.mock_prof_mgr, self.mock_policy_spec) self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class UpdateStoragePolicyTestCase(TestCase): + '''Tests for salt.utils.pbm.update_storage_policy''' + def setUp(self): + self.mock_policy_spec = MagicMock() + self.mock_policy = MagicMock() + self.mock_prof_mgr = MagicMock() + + def tearDown(self): + for attr in ('mock_policy_spec', 'mock_policy', 'mock_prof_mgr'): + delattr(self, attr) + + def test_create_policy(self): + salt.utils.pbm.update_storage_policy( + self.mock_prof_mgr, self.mock_policy, self.mock_policy_spec) + self.mock_prof_mgr.Update.assert_called_once_with( + self.mock_policy.profileId, self.mock_policy_spec) + + def test_create_policy_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.Update = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.update_storage_policy( + self.mock_prof_mgr, self.mock_policy, self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_create_policy_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.Update = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.update_storage_policy( + self.mock_prof_mgr, self.mock_policy, self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_create_policy_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.Update = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.update_storage_policy( + self.mock_prof_mgr, self.mock_policy, self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') From 61c226c086e370e00546adf77f8e3c1039d7772c Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 19:51:07 -0400 Subject: [PATCH 266/633] Added salt.utils.pbm.get_default_storage_policy_of_datastore --- salt/utils/pbm.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index 57d2f598d4..cb6474be85 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -259,3 +259,36 @@ def update_storage_policy(profile_manager, policy, policy_spec): except vmodl.RuntimeFault as exc: log.exception(exc) raise VMwareRuntimeError(exc.msg) + + +def get_default_storage_policy_of_datastore(profile_manager, datastore): + ''' + Returns the default storage policy reference assigned to a datastore. + + profile_manager + Reference to the profile manager. + + datastore + Reference to the datastore. + ''' + # Retrieve all datastores visible + hub = pbm.placement.PlacementHub( + hubId=datastore._moId, hubType='Datastore') + log.trace('placement_hub = {0}'.format(hub)) + try: + policy_id = profile_manager.QueryDefaultRequirementProfile(hub) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + policy_refs = get_policies_by_id(profile_manager, [policy_id]) + if not policy_refs: + raise VMwareObjectRetrievalError('Storage policy with id \'{0}\' was ' + 'not found'.format(policy_id)) + return policy_refs[0] From 5dbbac182d86bbd51cb22dcad89ae495a5730f57 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 19:51:38 -0400 Subject: [PATCH 267/633] Added tests for salt.utils.pbm.get_default_storage_policy_of_datastore --- tests/unit/utils/test_pbm.py | 106 ++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index f398f5a4ea..b8803c475f 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -16,7 +16,8 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, \ PropertyMock # Import Salt libraries -from salt.exceptions import VMwareApiError, VMwareRuntimeError +from salt.exceptions import VMwareApiError, VMwareRuntimeError, \ + VMwareObjectRetrievalError import salt.utils.pbm try: @@ -486,3 +487,106 @@ class UpdateStoragePolicyTestCase(TestCase): salt.utils.pbm.update_storage_policy( self.mock_prof_mgr, self.mock_policy, self.mock_policy_spec) self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetDefaultStoragePolicyOfDatastoreTestCase(TestCase): + '''Tests for salt.utils.pbm.get_default_storage_policy_of_datastore''' + def setUp(self): + self.mock_ds = MagicMock(_moId='fake_ds_moid') + self.mock_hub = MagicMock() + self.mock_policy_id = 'fake_policy_id' + self.mock_prof_mgr = MagicMock( + QueryDefaultRequirementProfile=MagicMock( + return_value=self.mock_policy_id)) + self.mock_policy_refs = [MagicMock()] + patches = ( + ('salt.utils.pbm.pbm.placement.PlacementHub', + MagicMock(return_value=self.mock_hub)), + ('salt.utils.pbm.get_policies_by_id', + MagicMock(return_value=self.mock_policy_refs))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_ds', 'mock_hub', 'mock_policy_id', 'mock_prof_mgr', + 'mock_policy_refs'): + delattr(self, attr) + + def test_get_placement_hub(self): + mock_get_placement_hub = MagicMock() + with patch('salt.utils.pbm.pbm.placement.PlacementHub', + mock_get_placement_hub): + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + mock_get_placement_hub.assert_called_once_with( + hubId='fake_ds_moid', hubType='Datastore') + + def test_query_default_requirement_profile(self): + mock_query_prof = MagicMock(return_value=self.mock_policy_id) + self.mock_prof_mgr.QueryDefaultRequirementProfile = \ + mock_query_prof + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + mock_query_prof.assert_called_once_with(self.mock_hub) + + def test_query_default_requirement_profile_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.QueryDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_query_default_requirement_profile_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.QueryDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_query_default_requirement_profile_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.QueryDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_get_policies_by_id(self): + mock_get_policies_by_id = MagicMock() + with patch('salt.utils.pbm.get_policies_by_id', + mock_get_policies_by_id): + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + mock_get_policies_by_id.assert_called_once_with( + self.mock_prof_mgr, [self.mock_policy_id]) + + def test_no_policy_refs(self): + mock_get_policies_by_id = MagicMock() + with path('salt.utils.pbm.get_policies_by_id', + MagicMock(return_value=None)): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, + 'Storage policy with id \'fake_policy_id\' was not ' + 'found') + + def test_no_policy_refs(self): + mock_get_policies_by_id = MagicMock() + ret = salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(ret, self.mock_policy_refs[0]) From 20fca4be441df1e0794f312a5e752c20534f6e0a Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 20:05:05 -0400 Subject: [PATCH 268/633] Added salt.utils.pbm.assign_default_storage_policy_to_datastore --- salt/utils/pbm.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index cb6474be85..17b25aceca 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -292,3 +292,35 @@ def get_default_storage_policy_of_datastore(profile_manager, datastore): raise VMwareObjectRetrievalError('Storage policy with id \'{0}\' was ' 'not found'.format(policy_id)) return policy_refs[0] + + +def assign_default_storage_policy_to_datastore(profile_manager, policy, + datastore): + ''' + Assigns a storage policy as the default policy to a datastore. + + profile_manager + Reference to the profile manager. + + policy + Reference to the policy to assigned. + + datastore + Reference to the datastore. + ''' + placement_hub = pbm.placement.PlacementHub( + hubId=datastore._moId, hubType='Datastore') + log.trace('placement_hub = {0}'.format(placement_hub)) + try: + profile_manager.AssignDefaultRequirementProfile(policy.profileId, + [placement_hub]) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) From a3047ad3071c4c64d0e18035207c2bbf8d188519 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 19 Sep 2017 20:05:43 -0400 Subject: [PATCH 269/633] Added tests for salt.utils.pbm.assign_default_storage_policy_to_datastore --- tests/unit/utils/test_pbm.py | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index b8803c475f..4e08229e26 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -590,3 +590,75 @@ class GetDefaultStoragePolicyOfDatastoreTestCase(TestCase): ret = salt.utils.pbm.get_default_storage_policy_of_datastore( self.mock_prof_mgr, self.mock_ds) self.assertEqual(ret, self.mock_policy_refs[0]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class AssignDefaultStoragePolicyToDatastoreTestCase(TestCase): + '''Tests for salt.utils.pbm.assign_default_storage_policy_to_datastore''' + def setUp(self): + self.mock_ds = MagicMock(_moId='fake_ds_moid') + self.mock_policy = MagicMock() + self.mock_hub = MagicMock() + self.mock_prof_mgr = MagicMock() + patches = ( + ('salt.utils.pbm.pbm.placement.PlacementHub', + MagicMock(return_value=self.mock_hub)),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_ds', 'mock_hub', 'mock_policy', 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_placement_hub(self): + mock_get_placement_hub = MagicMock() + with patch('salt.utils.pbm.pbm.placement.PlacementHub', + mock_get_placement_hub): + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + mock_get_placement_hub.assert_called_once_with( + hubId='fake_ds_moid', hubType='Datastore') + + def test_assign_default_requirement_profile(self): + mock_assign_prof = MagicMock() + self.mock_prof_mgr.AssignDefaultRequirementProfile = \ + mock_assign_prof + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + mock_assign_prof.assert_called_once_with( + self.mock_policy.profileId, [self.mock_hub]) + + def test_assign_default_requirement_profile_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.AssignDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_assign_default_requirement_profile_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.AssignDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_assign_default_requirement_profile_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.AssignDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') From 6da3ff5d933aa4631f7d8d5d9faaad582d30ebdb Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 10:03:36 -0400 Subject: [PATCH 270/633] Added salt.modules.vsphere._get_policy_dict that transforms a policy VMware object into a dict representation --- salt/modules/vsphere.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index bde7c9c98e..84f9a7ace6 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -177,6 +177,7 @@ import salt.utils.http import salt.utils.path import salt.utils.vmware import salt.utils.vsan +import salt.utils.pbm from salt.exceptions import CommandExecutionError, VMwareSaltError, \ ArgumentValueError, InvalidConfigError, VMwareObjectRetrievalError, \ VMwareApiError, InvalidEntityError @@ -193,7 +194,7 @@ except ImportError: HAS_JSONSCHEMA = False try: - from pyVmomi import vim, vmodl, VmomiSupport + from pyVmomi import vim, vmodl, pbm, VmomiSupport HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False @@ -4608,6 +4609,43 @@ def remove_dvportgroup(portgroup, dvs, service_instance=None): return True +def _get_policy_dict(policy): + '''Returns a dictionary representation of a policy''' + profile_dict = {'name': policy.name, + 'description': policy.description, + 'resource_type': policy.resourceType.resourceType} + subprofile_dicts = [] + if isinstance(policy, pbm.profile.CapabilityBasedProfile) and \ + isinstance(policy.constraints, + pbm.profile.SubProfileCapabilityConstraints): + + for subprofile in policy.constraints.subProfiles: + subprofile_dict = {'name': subprofile.name, + 'force_provision': subprofile.forceProvision} + cap_dicts = [] + for cap in subprofile.capability: + cap_dict = {'namespace': cap.id.namespace, + 'id': cap.id.id} + # We assume there is one constraint with one value set + val = cap.constraint[0].propertyInstance[0].value + if isinstance(val, pbm.capability.types.Range): + val_dict = {'type': 'range', + 'min': val.min, + 'max': val.max} + elif isinstance(val, pbm.capability.types.DiscreteSet): + val_dict = {'type': 'set', + 'values': val.values} + else: + val_dict = {'type': 'scalar', + 'value': val} + cap_dict['setting'] = val_dict + cap_dicts.append(cap_dict) + subprofile_dict['capabilities'] = cap_dicts + subprofile_dicts.append(subprofile_dict) + profile_dict['subprofiles'] = subprofile_dicts + return profile_dict + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 6bb0111b327134d908e2061471d7732b362b3926 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 10:04:49 -0400 Subject: [PATCH 271/633] Added salt.modules.vsphere.list_storage_policies that retrieves dict representations of storage policies, filtered by name --- salt/modules/vsphere.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 84f9a7ace6..59181fd634 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4646,6 +4646,36 @@ def _get_policy_dict(policy): return profile_dict +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def list_storage_policies(policy_names=None, service_instance=None): + ''' + Returns a list of storage policies. + + policy_names + Names of policies to list. If None, all policies are listed. + Default is None. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.list_storage_policies + + salt '*' vsphere.list_storage_policy policy_names=[policy_name] + ''' + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + if not policy_names: + policies = salt.utils.pbm.get_storage_policies(profile_manager, + get_all_policies=True) + else: + policies = salt.utils.pbm.get_storage_policies(profile_manager, + policy_names) + return [_get_policy_dict(p) for p in policies] + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From f9f84fde9ab8f45b183570665a315b31bd30d3e5 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 10:06:42 -0400 Subject: [PATCH 272/633] Added salt.modules.vsphere.list_default_vsan_policy that retrieves dict representation of the default storage policies --- salt/modules/vsphere.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 59181fd634..96b2ac037e 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4676,6 +4676,33 @@ def list_storage_policies(policy_names=None, service_instance=None): return [_get_policy_dict(p) for p in policies] +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def list_default_vsan_policy(service_instance=None): + ''' + Returns the default vsan storage policy. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.list_storage_policies + + salt '*' vsphere.list_storage_policy policy_names=[policy_name] + ''' + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + policies = salt.utils.pbm.get_storage_policies(profile_manager, + get_all_policies=True) + def_policies = [p for p in policies + if p.systemCreatedProfileType == 'VsanDefaultProfile'] + if not def_policies: + raise excs.VMwareObjectRetrievalError('Default VSAN policy was not ' + 'retrieved') + return _get_policy_dict(def_policies[0]) + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 8275e5710681c3dcc585089014bd5887279ed728 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 10:10:48 -0400 Subject: [PATCH 273/633] Added salt.modules.vsphere._get_capability_definition_dict that transforms a VMware capability definition into a dict representation --- salt/modules/vsphere.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 96b2ac037e..9655fd39fa 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4703,6 +4703,17 @@ def list_default_vsan_policy(service_instance=None): return _get_policy_dict(def_policies[0]) +def _get_capability_definition_dict(cap_metadata): + # We assume each capability definition has one property with the same id + # as the capability so we display its type as belonging to the capability + # The object model permits multiple properties + return {'namespace': cap_metadata.id.namespace, + 'id': cap_metadata.id.id, + 'mandatory': cap_metadata.mandatory, + 'description': cap_metadata.summary.summary, + 'type': cap_metadata.propertyMetadata[0].type.typeName} + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From c88c207011821c7c2aad8d80a9eecb8be4befc4c Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 10:12:58 -0400 Subject: [PATCH 274/633] Added salt.modules.vsphere.list_capability_definitions that returns dict representations of VMware capability definition --- salt/modules/vsphere.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 9655fd39fa..f92c3b6339 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4714,6 +4714,26 @@ def _get_capability_definition_dict(cap_metadata): 'type': cap_metadata.propertyMetadata[0].type.typeName} +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def list_capability_definitions(service_instance=None): + ''' + Returns a list of the metadata of all capabilities in the vCenter. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.list_capabilities + ''' + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + ret_list = [_get_capability_definition_dict(c) for c in + salt.utils.pbm.get_capability_definitions(profile_manager)] + return ret_list + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From ee2af6fc9c129539e39412c4b9ff79d6287d822c Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 12:26:14 -0400 Subject: [PATCH 275/633] Added salt.modules.vsphere._apply_policy_config that applies a storage dict representations values to a object --- salt/modules/vsphere.py | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index f92c3b6339..2a0000b8c5 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4734,6 +4734,55 @@ def list_capability_definitions(service_instance=None): return ret_list +def _apply_policy_config(policy_spec, policy_dict): + '''Applies a policy dictionary to a policy spec''' + log.trace('policy_dict = {0}'.format(policy_dict)) + if policy_dict.get('name'): + policy_spec.name = policy_dict['name'] + if policy_dict.get('description'): + policy_spec.description = policy_dict['description'] + if policy_dict.get('subprofiles'): + # Incremental changes to subprofiles and capabilities are not + # supported because they would complicate updates too much + # The whole configuration of all sub-profiles is expected and applied + policy_spec.constraints = pbm.profile.SubProfileCapabilityConstraints() + subprofiles = [] + for subprofile_dict in policy_dict['subprofiles']: + subprofile_spec = \ + pbm.profile.SubProfileCapabilityConstraints.SubProfile( + name=subprofile_dict['name']) + cap_specs = [] + if subprofile_dict.get('force_provision'): + subprofile_spec.forceProvision = \ + subprofile_dict['force_provision'] + for cap_dict in subprofile_dict['capabilities']: + prop_inst_spec = pbm.capability.PropertyInstance( + id=cap_dict['id'] + ) + setting_type = cap_dict['setting']['type'] + if setting_type == 'set': + prop_inst_spec.value = pbm.capability.types.DiscreteSet() + prop_inst_spec.value.values = cap_dict['setting']['values'] + elif setting_type == 'range': + prop_inst_spec.value = pbm.capability.types.Range() + prop_inst_spec.value.max = cap_dict['setting']['max'] + prop_inst_spec.value.min = cap_dict['setting']['min'] + elif setting_type == 'scalar': + prop_inst_spec.value = cap_dict['setting']['value'] + cap_spec = pbm.capability.CapabilityInstance( + id=pbm.capability.CapabilityMetadata.UniqueId( + id=cap_dict['id'], + namespace=cap_dict['namespace']), + constraint=[pbm.capability.ConstraintInstance( + propertyInstance=[prop_inst_spec])]) + cap_specs.append(cap_spec) + subprofile_spec.capability = cap_specs + subprofiles.append(subprofile_spec) + policy_spec.constraints.subProfiles = subprofiles + log.trace('updated policy_spec = {0}'.format(policy_spec)) + return policy_spec + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From a5ae51f6166267efdf1986b122dd978560e1db68 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 12:36:56 -0400 Subject: [PATCH 276/633] Added salt.modules.vsphere.create_storage_policy --- salt/modules/vsphere.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 2a0000b8c5..551ecedc7d 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4783,6 +4783,47 @@ def _apply_policy_config(policy_spec, policy_dict): return policy_spec +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def create_storage_policy(policy_name, policy_dict, service_instance=None): + ''' + Creates a storage policy. + + Supported capability types: scalar, set, range. + + policy_name + Name of the policy to create. + The value of the argument will override any existing name in + ``policy_dict``. + + policy_dict + Dictionary containing the changes to apply to the policy. + (exmaple in salt.states.pbm) + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.create_storage_policy policy_name='policy name' + policy_dict="$policy_dict" + ''' + log.trace('create storage policy \'{0}\', dict = {1}' + ''.format(policy_name, policy_dict)) + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + policy_create_spec = pbm.profile.CapabilityBasedProfileCreateSpec() + # Hardcode the storage profile resource type + policy_create_spec.resourceType = pbm.profile.ResourceType( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + # Set name argument + policy_dict['name'] = policy_name + log.trace('Setting policy values in policy_update_spec') + _apply_policy_config(policy_create_spec, policy_dict) + salt.utils.pbm.create_storage_policy(profile_manager, policy_create_spec) + return {'create_storage_policy': True} + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 41a65bf4140d31a84f395d533eaabbbbc8abb8c2 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 12:37:41 -0400 Subject: [PATCH 277/633] Added salt.modules.vsphere.update_storage_policy --- salt/modules/vsphere.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 551ecedc7d..481df498a9 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4824,6 +4824,47 @@ def create_storage_policy(policy_name, policy_dict, service_instance=None): return {'create_storage_policy': True} +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def update_storage_policy(policy, policy_dict, service_instance=None): + ''' + Updates a storage policy. + + Supported capability types: scalar, set, range. + + policy + Name of the policy to update. + + policy_dict + Dictionary containing the changes to apply to the policy. + (exmaple in salt.states.pbm) + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.update_storage_policy policy='policy name' + policy_dict="$policy_dict" + ''' + log.trace('updating storage policy, dict = {0}'.format(policy_dict)) + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + policies = salt.utils.pbm.get_storage_policies(profile_manager, [policy]) + if not policies: + raise excs.VMwareObjectRetrievalError('Policy \'{0}\' was not found' + ''.format(policy)) + policy_ref = policies[0] + policy_update_spec = pbm.profile.CapabilityBasedProfileUpdateSpec() + log.trace('Setting policy values in policy_update_spec') + for prop in ['description', 'constraints']: + setattr(policy_update_spec, prop, getattr(policy_ref, prop)) + _apply_policy_config(policy_update_spec, policy_dict) + salt.utils.pbm.update_storage_policy(profile_manager, policy_ref, + policy_update_spec) + return {'update_storage_policy': True} + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 582919f5513ad25625cf9d81854a68129184dc1d Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 12:38:30 -0400 Subject: [PATCH 278/633] Added salt.modules.vsphere.list_default_storage_policy_of_datastore that lists the dict representation of the policy assigned by default to a datastore --- salt/modules/vsphere.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 481df498a9..cb6f6953c6 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4865,6 +4865,41 @@ def update_storage_policy(policy, policy_dict, service_instance=None): return {'update_storage_policy': True} +@depends(HAS_PYVMOMI) +@supports_proxies('esxcluster', 'esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def list_default_storage_policy_of_datastore(datastore, service_instance=None): + ''' + Returns a list of datastores assign the the storage policies. + + datastore + Name of the datastore to assign. + The datastore needs to be visible to the VMware entity the proxy + points to. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.list_default_storage_policy_of_datastore datastore=ds1 + ''' + log.trace('Listing the default storage policy of datastore \'{0}\'' + ''.format(datastore)) + # Find datastore + target_ref = _get_proxy_target(service_instance) + ds_refs = salt.utils.vmware.get_datastores(service_instance, target_ref, + datastore_names=[datastore]) + if not ds_refs: + raise excs.VMwareObjectRetrievalError('Datastore \'{0}\' was not ' + 'found'.format(datastore)) + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + policy = salt.utils.pbm.get_default_storage_policy_of_datastore( + profile_manager, ds_refs[0]) + return _get_policy_dict(policy) + + + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy From 0b2b79692a056498a5f1c87b0a1f1bb306e11627 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 18:10:40 -0400 Subject: [PATCH 279/633] Added salt.modules.vsphere.assign_default_storage_policy_to_datastore --- salt/modules/vsphere.py | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index cb6f6953c6..dce73ffa1a 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4899,6 +4899,51 @@ def list_default_storage_policy_of_datastore(datastore, service_instance=None): return _get_policy_dict(policy) +@depends(HAS_PYVMOMI) +@supports_proxies('esxcluster', 'esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def assign_default_storage_policy_to_datastore(policy, datastore, + service_instance=None): + ''' + Assigns a storage policy as the default policy to a datastore. + + policy + Name of the policy to assign. + + datastore + Name of the datastore to assign. + The datastore needs to be visible to the VMware entity the proxy + points to. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.assign_storage_policy_to_datastore + policy='policy name' datastore=ds1 + ''' + log.trace('Assigning policy {0} to datastore {1}' + ''.format(policy, datastore)) + profile_manager = utils_pbm.get_profile_manager(service_instance) + # Find policy + policies = utils_pbm.get_storage_policies(profile_manager, [policy]) + if not policies: + raise excs.VMwareObjectRetrievalError('Policy \'{0}\' was not found' + ''.format(policy)) + policy_ref = policies[0] + # Find datastore + target_ref = _get_proxy_target(service_instance) + ds_refs = salt.utils.vmware.get_datastores(service_instance, target_ref, + datastore_names=[datastore]) + if not ds_refs: + raise excs.VMwareObjectRetrievalError('Datastore \'{0}\' was not ' + 'found'.format(datastore)) + ds_ref = ds_refs[0] + utils_pbm.assign_default_storage_policy_to_datastore(profile_manager, + policy_ref, ds_ref) + return {'assign_storage_policy_to_datastore': True} + @depends(HAS_PYVMOMI) @supports_proxies('esxdatacenter', 'esxcluster') From 507910b9560bcfe248f6ed4c6815d4625e62420f Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 20:10:04 -0400 Subject: [PATCH 280/633] Added VCenterProxySchema JSON schema that validates the vcenter proxy --- salt/config/schemas/vcenter.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/salt/config/schemas/vcenter.py b/salt/config/schemas/vcenter.py index 4867923f27..1d76fb43a5 100644 --- a/salt/config/schemas/vcenter.py +++ b/salt/config/schemas/vcenter.py @@ -14,6 +14,8 @@ from __future__ import absolute_import # Import Salt libs from salt.utils.schema import (Schema, + ArrayItem, + IntegerItem, StringItem) @@ -31,3 +33,25 @@ class VCenterEntitySchema(Schema): vcenter = StringItem(title='vCenter', description='Specifies the vcenter hostname', required=True) + + +class VCenterProxySchema(Schema): + ''' + Schema for the configuration for the proxy to connect to a VCenter. + ''' + title = 'VCenter Proxy Connection Schema' + description = 'Schema that describes the connection to a VCenter' + additional_properties = False + proxytype = StringItem(required=True, + enum=['vcenter']) + vcenter = StringItem(required=True, pattern=r'[^\s]+') + mechanism = StringItem(required=True, enum=['userpass', 'sspi']) + username = StringItem() + passwords = ArrayItem(min_items=1, + items=StringItem(), + unique_items=True) + + domain = StringItem() + principal = StringItem(default='host') + protocol = StringItem(default='https') + port = IntegerItem(minimum=1) From 176222b0cf262b0197930d91b9ac4fce07d5e687 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 20:27:19 -0400 Subject: [PATCH 281/633] Added vcenter proxy --- salt/proxy/vcenter.py | 338 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 salt/proxy/vcenter.py diff --git a/salt/proxy/vcenter.py b/salt/proxy/vcenter.py new file mode 100644 index 0000000000..7b9c9f95e3 --- /dev/null +++ b/salt/proxy/vcenter.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +''' +Proxy Minion interface module for managing VMWare vCenters. + +:codeauthor: :email:`Rod McKenzie (roderick.mckenzie@morganstanley.com)` +:codeauthor: :email:`Alexandru Bleotu (alexandru.bleotu@morganstanley.com)` + +Dependencies +============ + +- pyVmomi Python Module + +pyVmomi +------- + +PyVmomi can be installed via pip: + +.. code-block:: bash + + pip install pyVmomi + +.. note:: + + Version 6.0 of pyVmomi has some problems with SSL error handling on certain + versions of Python. If using version 6.0 of pyVmomi, Python 2.6, + Python 2.7.9, or newer must be present. This is due to an upstream dependency + in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the + version of Python is not in the supported range, you will need to install an + earlier version of pyVmomi. See `Issue #29537`_ for more information. + +.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 + +Based on the note above, to install an earlier version of pyVmomi than the +version currently listed in PyPi, run the following: + +.. code-block:: bash + + pip install pyVmomi==5.5.0.2014.1.1 + +The 5.5.0.2014.1.1 is a known stable version that this original ESXi State +Module was developed against. + + +Configuration +============= +To use this proxy module, please use on of the following configurations: + + +.. code-block:: yaml + + proxy: + proxytype: vcenter + vcenter: + username: + mechanism: userpass + passwords: + - first_password + - second_password + - third_password + + proxy: + proxytype: vcenter + vcenter: + username: + domain: + mechanism: sspi + principal: + +proxytype +^^^^^^^^^ +The ``proxytype`` key and value pair is critical, as it tells Salt which +interface to load from the ``proxy`` directory in Salt's install hierarchy, +or from ``/srv/salt/_proxy`` on the Salt Master (if you have created your +own proxy module, for example). To use this Proxy Module, set this to +``vcenter``. + +vcenter +^^^^^^^ +The location of the VMware vCenter server (host of ip). Required + +username +^^^^^^^^ +The username used to login to the vcenter, such as ``root``. +Required only for userpass. + +mechanism +^^^^^^^^ +The mechanism used to connect to the vCenter server. Supported values are +``userpass`` and ``sspi``. Required. + +passwords +^^^^^^^^^ +A list of passwords to be used to try and login to the vCenter server. At least +one password in this list is required if mechanism is ``userpass`` + +The proxy integration will try the passwords listed in order. + +domain +^^^^^^ +User domain. Required if mechanism is ``sspi`` + +principal +^^^^^^^^ +Kerberos principal. Rquired if mechanism is ``sspi`` + +protocol +^^^^^^^^ +If the vCenter is not using the default protocol, set this value to an +alternate protocol. Default is ``https``. + +port +^^^^ +If the ESXi host is not using the default port, set this value to an +alternate port. Default is ``443``. + + +Salt Proxy +---------- + +After your pillar is in place, you can test the proxy. The proxy can run on +any machine that has network connectivity to your Salt Master and to the +vCenter server in the pillar. SaltStack recommends that the machine running the +salt-proxy process also run a regular minion, though it is not strictly +necessary. + +On the machine that will run the proxy, make sure there is an ``/etc/salt/proxy`` +file with at least the following in it: + +.. code-block:: yaml + + master: + +You can then start the salt-proxy process with: + +.. code-block:: bash + + salt-proxy --proxyid + +You may want to add ``-l debug`` to run the above in the foreground in +debug mode just to make sure everything is OK. + +Next, accept the key for the proxy on your salt-master, just like you +would for a regular minion: + +.. code-block:: bash + + salt-key -a + +You can confirm that the pillar data is in place for the proxy: + +.. code-block:: bash + + salt pillar.items + +And now you should be able to ping the ESXi host to make sure it is +responding: + +.. code-block:: bash + + salt test.ping + +At this point you can execute one-off commands against the vcenter. For +example, you can get if the proxy can actually connect to the vCenter: + +.. code-block:: bash + + salt vsphere.test_vcenter_connection + +Note that you don't need to provide credentials or an ip/hostname. Salt +knows to use the credentials you stored in Pillar. + +It's important to understand how this particular proxy works. +:mod:`Salt.modules.vsphere ` is a +standard Salt execution module. + + If you pull up the docs for it you'll see +that almost every function in the module takes credentials and a targets either +a vcenter or a host. When credentials and a host aren't passed, Salt runs commands +through ``pyVmomi`` against the local machine. If you wanted, you could run +functions from this module on any host where an appropriate version of +``pyVmomi`` is installed, and that host would reach out over the network +and communicate with the ESXi host. +''' + +# Import Python Libs +from __future__ import absolute_import +import logging +import os + +# Import Salt Libs +import salt.exceptions +from salt.config.schemas.vcenter import VCenterProxySchema +from salt.utils.dictupdate import merge + +# This must be present or the Salt loader won't load this module. +__proxyenabled__ = ['vcenter'] + +# External libraries +try: + import jsonschema + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False + +# Variables are scoped to this module so we can have persistent data +# across calls to fns in here. +DETAILS = {} + + +# Set up logging +log = logging.getLogger(__name__) +# Define the module's virtual name +__virtualname__ = 'vcenter' + + +def __virtual__(): + ''' + Only load if the vsphere execution module is available. + ''' + if HAS_JSONSCHEMA: + return __virtualname__ + + return False, 'The vcenter proxy module did not load.' + + +def init(opts): + ''' + This function gets called when the proxy starts up. + For login the protocol and port are cached. + ''' + log.info('Initting vcenter proxy module in process {0}' + ''.format(os.getpid())) + log.trace('VCenter Proxy Validating vcenter proxy input') + schema = VCenterProxySchema.serialize() + log.trace('schema = {}'.format(schema)) + proxy_conf = merge(opts.get('proxy', {}), __pillar__.get('proxy', {})) + log.trace('proxy_conf = {0}'.format(proxy_conf)) + try: + jsonschema.validate(proxy_conf, schema) + except jsonschema.exceptions.ValidationError as exc: + raise salt.exceptions.InvalidConfigError(exc) + + # Save mandatory fields in cache + for key in ('vcenter', 'mechanism'): + DETAILS[key] = proxy_conf[key] + + # Additional validation + if DETAILS['mechanism'] == 'userpass': + if 'username' not in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'userpass\' , but no ' + '\'username\' key found in proxy config') + if not 'passwords' in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'userpass\' , but no ' + '\'passwords\' key found in proxy config') + for key in ('username', 'passwords'): + DETAILS[key] = proxy_conf[key] + else: + if not 'domain' in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'sspi\' , but no ' + '\'domain\' key found in proxy config') + if not 'principal' in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'sspi\' , but no ' + '\'principal\' key found in proxy config') + for key in ('domain', 'principal'): + DETAILS[key] = proxy_conf[key] + + # Save optional + DETAILS['protocol'] = proxy_conf.get('protocol') + DETAILS['port'] = proxy_conf.get('port') + + # Test connection + if DETAILS['mechanism'] == 'userpass': + # Get the correct login details + log.info('Retrieving credentials and testing vCenter connection for ' + 'mehchanism \'userpass\'') + try: + username, password = find_credentials() + DETAILS['password'] = password + except salt.exceptions.SaltSystemExit as err: + log.critical('Error: {0}'.format(err)) + return False + return True + + +def ping(): + ''' + Returns True. + + CLI Example: + + .. code-block:: bash + + salt vcenter test.ping + ''' + return True + + +def shutdown(): + ''' + Shutdown the connection to the proxy device. For this proxy, + shutdown is a no-op. + ''' + log.debug('VCenter proxy shutdown() called...') + + +def find_credentials(): + ''' + Cycle through all the possible credentials and return the first one that + works. + ''' + + # if the username and password were already found don't fo though the + # connection process again + if 'username' in DETAILS and 'password' in DETAILS: + return DETAILS['username'], DETAILS['password'] + + passwords = __pillar__['proxy']['passwords'] + for password in passwords: + DETAILS['password'] = password + if not __salt__['vsphere.test_vcenter_connection'](): + # We are unable to authenticate + continue + # If we have data returned from above, we've successfully authenticated. + return DETAILS['username'], password + # We've reached the end of the list without successfully authenticating. + raise salt.exceptions.VMwareConnectionError('Cannot complete login due to ' + 'incorrect credentials.') + + +def get_details(): + ''' + Function that returns the cached details + ''' + return DETAILS From 483fa0d8382ef1a10a656afe41bde10964b373c2 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 20:36:57 -0400 Subject: [PATCH 282/633] Added salt.modules.vcenter shim execution module between the proxy and other execution modules --- salt/modules/vcenter.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 salt/modules/vcenter.py diff --git a/salt/modules/vcenter.py b/salt/modules/vcenter.py new file mode 100644 index 0000000000..bac3c674b4 --- /dev/null +++ b/salt/modules/vcenter.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +''' +Module used to access the vcenter proxy connection methods +''' +from __future__ import absolute_import + +# Import python libs +import logging +import salt.utils + + +log = logging.getLogger(__name__) + +__proxyenabled__ = ['vcenter'] +# Define the module's virtual name +__virtualname__ = 'vcenter' + + +def __virtual__(): + ''' + Only work on proxy + ''' + if salt.utils.is_proxy(): + return __virtualname__ + return False + + +def get_details(): + return __proxy__['vcenter.get_details']() From 94929d541520456583bb549aab6d98b9b84c9142 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 20:38:14 -0400 Subject: [PATCH 283/633] Added support for vcenter proxy in salt.modules.vsphere --- salt/modules/vsphere.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index dce73ffa1a..d4421ce1de 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -208,7 +208,7 @@ else: log = logging.getLogger(__name__) __virtualname__ = 'vsphere' -__proxyenabled__ = ['esxi', 'esxcluster', 'esxdatacenter'] +__proxyenabled__ = ['esxi', 'esxcluster', 'esxdatacenter', 'vcenter'] def __virtual__(): @@ -255,6 +255,8 @@ def _get_proxy_connection_details(): details = __salt__['esxcluster.get_details']() elif proxytype == 'esxdatacenter': details = __salt__['esxdatacenter.get_details']() + elif proxytype == 'vcenter': + details = __salt__['vcenter.get_details']() else: raise CommandExecutionError('\'{0}\' proxy is not supported' ''.format(proxytype)) @@ -380,7 +382,7 @@ def gets_service_instance_via_proxy(fn): @depends(HAS_PYVMOMI) -@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter', 'vcenter') def get_service_instance_via_proxy(service_instance=None): ''' Returns a service instance to the proxied endpoint (vCenter/ESXi host). @@ -400,7 +402,7 @@ def get_service_instance_via_proxy(service_instance=None): @depends(HAS_PYVMOMI) -@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter', 'vcenter') def disconnect(service_instance): ''' Disconnects from a vCenter or ESXi host @@ -1935,7 +1937,7 @@ def get_vsan_eligible_disks(host, username, password, protocol=None, port=None, @depends(HAS_PYVMOMI) -@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter', 'vcenter') @gets_service_instance_via_proxy def test_vcenter_connection(service_instance=None): ''' @@ -4946,7 +4948,7 @@ def assign_default_storage_policy_to_datastore(policy, datastore, @depends(HAS_PYVMOMI) -@supports_proxies('esxdatacenter', 'esxcluster') +@supports_proxies('esxdatacenter', 'esxcluster', 'vcenter') @gets_service_instance_via_proxy def list_datacenters_via_proxy(datacenter_names=None, service_instance=None): ''' @@ -4984,7 +4986,7 @@ def list_datacenters_via_proxy(datacenter_names=None, service_instance=None): @depends(HAS_PYVMOMI) -@supports_proxies('esxdatacenter') +@supports_proxies('esxdatacenter', 'vcenter') @gets_service_instance_via_proxy def create_datacenter(datacenter_name, service_instance=None): ''' @@ -6439,7 +6441,7 @@ def add_host_to_dvs(host, username, password, vmknic_name, vmnic_name, @depends(HAS_PYVMOMI) -@supports_proxies('esxcluster', 'esxdatacenter') +@supports_proxies('esxcluster', 'esxdatacenter', 'vcenter') def _get_proxy_target(service_instance): ''' Returns the target object of a proxy. @@ -6467,6 +6469,9 @@ def _get_proxy_target(service_instance): reference = salt.utils.vmware.get_datacenter(service_instance, datacenter) + elif proxy_type == 'vcenter': + # vcenter proxy - the target is the root folder + reference = salt.utils.vmware.get_root_folder(service_instance) log.trace('reference = {0}'.format(reference)) return reference From 58445e927b8295bcd74f4047b619565458d0aeaa Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 21:00:39 -0400 Subject: [PATCH 284/633] Updated all vsphere tests to support the vcenter proxy --- tests/unit/modules/test_vsphere.py | 47 +++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index 56669b900e..9ebad77363 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -639,6 +639,15 @@ class _GetProxyConnectionDetailsTestCase(TestCase, LoaderModuleMockMixin): 'mechanism': 'fake_mechanism', 'principal': 'fake_principal', 'domain': 'fake_domain'} + self.vcenter_details = {'vcenter': 'fake_vcenter', + 'username': 'fake_username', + 'password': 'fake_password', + 'protocol': 'fake_protocol', + 'port': 'fake_port', + 'mechanism': 'fake_mechanism', + 'principal': 'fake_principal', + 'domain': 'fake_domain'} + def tearDown(self): for attrname in ('esxi_host_details', 'esxi_vcenter_details', @@ -693,6 +702,17 @@ class _GetProxyConnectionDetailsTestCase(TestCase, LoaderModuleMockMixin): 'fake_protocol', 'fake_port', 'fake_mechanism', 'fake_principal', 'fake_domain'), ret) + def test_vcenter_proxy_details(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='vcenter')): + with patch.dict(vsphere.__salt__, + {'vcenter.get_details': MagicMock( + return_value=self.vcenter_details)}): + ret = vsphere._get_proxy_connection_details() + self.assertEqual(('fake_vcenter', 'fake_username', 'fake_password', + 'fake_protocol', 'fake_port', 'fake_mechanism', + 'fake_principal', 'fake_domain'), ret) + def test_unsupported_proxy_details(self): with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value='unsupported')): @@ -890,7 +910,7 @@ class GetServiceInstanceViaProxyTestCase(TestCase, LoaderModuleMockMixin): } def test_supported_proxies(self): - supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -933,7 +953,7 @@ class DisconnectTestCase(TestCase, LoaderModuleMockMixin): } def test_supported_proxies(self): - supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -974,7 +994,7 @@ class TestVcenterConnectionTestCase(TestCase, LoaderModuleMockMixin): } def test_supported_proxies(self): - supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -1049,7 +1069,7 @@ class ListDatacentersViaProxyTestCase(TestCase, LoaderModuleMockMixin): } def test_supported_proxies(self): - supported_proxies = ['esxcluster', 'esxdatacenter'] + supported_proxies = ['esxcluster', 'esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -1127,7 +1147,7 @@ class CreateDatacenterTestCase(TestCase, LoaderModuleMockMixin): } def test_supported_proxies(self): - supported_proxies = ['esxdatacenter'] + supported_proxies = ['esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -1339,12 +1359,15 @@ class _GetProxyTargetTestCase(TestCase, LoaderModuleMockMixin): def setUp(self): attrs = (('mock_si', MagicMock()), ('mock_dc', MagicMock()), - ('mock_cl', MagicMock())) + ('mock_cl', MagicMock()), + ('mock_root', MagicMock())) for attr, mock_obj in attrs: setattr(self, attr, mock_obj) self.addCleanup(delattr, self, attr) attrs = (('mock_get_datacenter', MagicMock(return_value=self.mock_dc)), - ('mock_get_cluster', MagicMock(return_value=self.mock_cl))) + ('mock_get_cluster', MagicMock(return_value=self.mock_cl)), + ('mock_get_root_folder', + MagicMock(return_value=self.mock_root))) for attr, mock_obj in attrs: setattr(self, attr, mock_obj) self.addCleanup(delattr, self, attr) @@ -1360,7 +1383,8 @@ class _GetProxyTargetTestCase(TestCase, LoaderModuleMockMixin): MagicMock(return_value=(None, None, None, None, None, None, None, None, 'datacenter'))), ('salt.utils.vmware.get_datacenter', self.mock_get_datacenter), - ('salt.utils.vmware.get_cluster', self.mock_get_cluster)) + ('salt.utils.vmware.get_cluster', self.mock_get_cluster), + ('salt.utils.vmware.get_root_folder', self.mock_get_root_folder)) for module, mock_obj in patches: patcher = patch(module, mock_obj) patcher.start() @@ -1409,3 +1433,10 @@ class _GetProxyTargetTestCase(TestCase, LoaderModuleMockMixin): MagicMock(return_value='esxdatacenter')): ret = vsphere._get_proxy_target(self.mock_si) self.assertEqual(ret, self.mock_dc) + + def test_vcenter_proxy_return(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='vcenter')): + ret = vsphere._get_proxy_target(self.mock_si) + self.mock_get_root_folder.assert_called_once_with(self.mock_si) + self.assertEqual(ret, self.mock_root) From da39e7ce842d969032cd3184dde1024c450efdab Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 21:05:53 -0400 Subject: [PATCH 285/633] Comments, imports, init function in salt.states.pbm --- salt/states/pbm.py | 134 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 salt/states/pbm.py diff --git a/salt/states/pbm.py b/salt/states/pbm.py new file mode 100644 index 0000000000..3026368f4b --- /dev/null +++ b/salt/states/pbm.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +''' +Manages VMware storage policies +(called pbm because the vCenter endpoint is /pbm) + +Examples +======== + +Storage policy +-------------- + +.. code-block:: python + +{ + "name": "salt_storage_policy" + "description": "Managed by Salt. Random capability values.", + "resource_type": "STORAGE", + "subprofiles": [ + { + "capabilities": [ + { + "setting": { + "type": "scalar", + "value": 2 + }, + "namespace": "VSAN", + "id": "hostFailuresToTolerate" + }, + { + "setting": { + "type": "scalar", + "value": 2 + }, + "namespace": "VSAN", + "id": "stripeWidth" + }, + { + "setting": { + "type": "scalar", + "value": true + }, + "namespace": "VSAN", + "id": "forceProvisioning" + }, + { + "setting": { + "type": "scalar", + "value": 50 + }, + "namespace": "VSAN", + "id": "proportionalCapacity" + }, + { + "setting": { + "type": "scalar", + "value": 0 + }, + "namespace": "VSAN", + "id": "cacheReservation" + } + ], + "name": "Rule-Set 1: VSAN", + "force_provision": null + } + ], +} + +Dependencies +============ + + +- pyVmomi Python Module + + +pyVmomi +------- + +PyVmomi can be installed via pip: + +.. code-block:: bash + + pip install pyVmomi + +.. note:: + + Version 6.0 of pyVmomi has some problems with SSL error handling on certain + versions of Python. If using version 6.0 of pyVmomi, Python 2.6, + Python 2.7.9, or newer must be present. This is due to an upstream dependency + in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the + version of Python is not in the supported range, you will need to install an + earlier version of pyVmomi. See `Issue #29537`_ for more information. + +.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 +''' + +# Import Python Libs +from __future__ import absolute_import +import sys +import logging +import json +import time +import copy + +# Import Salt Libs +from salt.exceptions import CommandExecutionError, ArgumentValueError +import salt.modules.vsphere as vsphere +from salt.utils import is_proxy +from salt.utils.dictdiffer import recursive_diff +from salt.utils.listdiffer import list_diff + +# External libraries +try: + import jsonschema + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False + +# Get Logging Started +log = logging.getLogger(__name__) +# TODO change with vcenter +ALLOWED_PROXY_TYPES = ['esxcluster', 'vcenter'] +LOGIN_DETAILS = {} + +def __virtual__(): + if HAS_JSONSCHEMA: + return True + return False + + +def mod_init(low): + ''' + Init function + ''' + return True From 9f96c1fcc091452c844165d8b1cd10cc4bdc1914 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 21:07:19 -0400 Subject: [PATCH 286/633] Added salt.states.pbm.default_vsan_policy_configured state that configures the default storage policy --- salt/states/pbm.py | 141 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/salt/states/pbm.py b/salt/states/pbm.py index 3026368f4b..a30eba6456 100644 --- a/salt/states/pbm.py +++ b/salt/states/pbm.py @@ -132,3 +132,144 @@ def mod_init(low): Init function ''' return True + + +def default_vsan_policy_configured(name, policy): + ''' + Configures the default VSAN policy on a vCenter. + The state assumes there is only one default VSAN policy on a vCenter. + + policy + Dict representation of a policy + ''' + # TODO Refactor when recurse_differ supports list_differ + # It's going to make the whole thing much easier + policy_copy = copy.deepcopy(policy) + proxy_type = __salt__['vsphere.get_proxy_type']() + log.trace('proxy_type = {0}'.format(proxy_type)) + # All allowed proxies have a shim execution module with the same + # name which implementes a get_details function + # All allowed proxies have a vcenter detail + vcenter = __salt__['{0}.get_details'.format(proxy_type)]()['vcenter'] + log.info('Running {0} on vCenter ' + '\'{1}\''.format(name, vcenter)) + log.trace('policy = {0}'.format(policy)) + changes_required = False + ret = {'name': name, 'changes': {}, 'result': None, 'comment': None, + 'pchanges': {}} + comments = [] + changes = {} + changes_required = False + si = None + + try: + #TODO policy schema validation + si = __salt__['vsphere.get_service_instance_via_proxy']() + current_policy = __salt__['vsphere.list_default_vsan_policy'](si) + log.trace('current_policy = {0}'.format(current_policy)) + # Building all diffs between the current and expected policy + # XXX We simplify the comparison by assuming we have at most 1 + # sub_profile + if policy.get('subprofiles'): + if len(policy['subprofiles']) > 1: + raise ArgumentValueError('Multiple sub_profiles ({0}) are not ' + 'supported in the input policy') + subprofile = policy['subprofiles'][0] + current_subprofile = current_policy['subprofiles'][0] + capabilities_differ = list_diff(current_subprofile['capabilities'], + subprofile.get('capabilities', []), + key='id') + del policy['subprofiles'] + if subprofile.get('capabilities'): + del subprofile['capabilities'] + del current_subprofile['capabilities'] + # Get the subprofile diffs without the capability keys + subprofile_differ = recursive_diff(current_subprofile, + dict(subprofile)) + + del current_policy['subprofiles'] + policy_differ = recursive_diff(current_policy, policy) + if policy_differ.diffs or capabilities_differ.diffs or \ + subprofile_differ.diffs: + + if 'name' in policy_differ.new_values or \ + 'description' in policy_differ.new_values: + + raise ArgumentValueError( + '\'name\' and \'description\' of the default VSAN policy ' + 'cannot be updated') + changes_required = True + if __opts__['test']: + str_changes = [] + if policy_differ.diffs: + str_changes.extend([change for change in + policy_differ.changes_str.split('\n')]) + if subprofile_differ.diffs or capabilities_differ.diffs: + str_changes.append('subprofiles:') + if subprofile_differ.diffs: + str_changes.extend( + [' {0}'.format(change) for change in + subprofile_differ.changes_str.split('\n')]) + if capabilities_differ.diffs: + str_changes.append(' capabilities:') + str_changes.extend( + [' {0}'.format(change) for change in + capabilities_differ.changes_str2.split('\n')]) + comments.append( + 'State {0} will update the default VSAN policy on ' + 'vCenter \'{1}\':\n{2}' + ''.format(name, vcenter, '\n'.join(str_changes))) + else: + __salt__['vsphere.update_storage_policy']( + policy=current_policy['name'], + policy_dict=policy_copy, + service_instance=si) + comments.append('Updated the default VSAN policy in vCenter ' + '\'{0}\''.format(vcenter)) + log.info(comments[-1]) + + new_values = policy_differ.new_values + new_values['subprofiles'] = [subprofile_differ.new_values] + new_values['subprofiles'][0]['capabilities'] = \ + capabilities_differ.new_values + if not new_values['subprofiles'][0]['capabilities']: + del new_values['subprofiles'][0]['capabilities'] + if not new_values['subprofiles'][0]: + del new_values['subprofiles'] + old_values = policy_differ.old_values + old_values['subprofiles'] = [subprofile_differ.old_values] + old_values['subprofiles'][0]['capabilities'] = \ + capabilities_differ.old_values + if not old_values['subprofiles'][0]['capabilities']: + del old_values['subprofiles'][0]['capabilities'] + if not old_values['subprofiles'][0]: + del old_values['subprofiles'] + changes.update({'default_vsan_policy': + {'new': new_values, + 'old': old_values}}) + log.trace(changes) + __salt__['vsphere.disconnect'](si) + except CommandExecutionError as exc: + log.error('Error: {}'.format(exc)) + if si: + __salt__['vsphere.disconnect'](si) + if not __opts__['test']: + ret['result'] = False + ret.update({'comment': exc.strerror, + 'result': False if not __opts__['test'] else None}) + return ret + if not changes_required: + # We have no changes + ret.update({'comment': ('Default VSAN policy in vCenter ' + '\'{0}\' is correctly configured. ' + 'Nothing to be done.'.format(vcenter)), + 'result': True}) + else: + ret.update({'comment': '\n'.join(comments)}) + if __opts__['test']: + ret.update({'pchanges': changes, + 'result': None}) + else: + ret.update({'changes': changes, + 'result': True}) + return ret From bb52e0d3318d1026061a1cc2350654ad86bdfb3e Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 21:08:42 -0400 Subject: [PATCH 287/633] Added salt.states.pbm.storage_policies_configured state that creates/configures storage policies --- salt/states/pbm.py | 164 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/salt/states/pbm.py b/salt/states/pbm.py index a30eba6456..5483315195 100644 --- a/salt/states/pbm.py +++ b/salt/states/pbm.py @@ -273,3 +273,167 @@ def default_vsan_policy_configured(name, policy): ret.update({'changes': changes, 'result': True}) return ret + + +def storage_policies_configured(name, policies): + ''' + Configures storage policies on a vCenter. + + policies + List of dict representation of the required storage policies + ''' + comments = [] + changes = [] + changes_required = False + ret = {'name': name, 'changes': {}, 'result': None, 'comment': None, + 'pchanges': {}} + log.trace('policies = {0}'.format(policies)) + si = None + try: + proxy_type = __salt__['vsphere.get_proxy_type']() + log.trace('proxy_type = {0}'.format(proxy_type)) + # All allowed proxies have a shim execution module with the same + # name which implementes a get_details function + # All allowed proxies have a vcenter detail + vcenter = __salt__['{0}.get_details'.format(proxy_type)]()['vcenter'] + log.info('Running state \'{0}\' on vCenter ' + '\'{0}\''.format(name, vcenter)) + si = __salt__['vsphere.get_service_instance_via_proxy']() + current_policies = __salt__['vsphere.list_storage_policies']( + policy_names=[policy['name'] for policy in policies], + service_instance=si) + log.trace('current_policies = {0}'.format(current_policies)) + # TODO Refactor when recurse_differ supports list_differ + # It's going to make the whole thing much easier + for policy in policies: + policy_copy = copy.deepcopy(policy) + filtered_policies = [p for p in current_policies + if p['name'] == policy['name']] + current_policy = filtered_policies[0] \ + if filtered_policies else None + + if not current_policy: + changes_required = True + if __opts__['test']: + comments.append('State {0} will create the storage policy ' + '\'{1}\' on vCenter \'{2}\'' + ''.format(name, policy['name'], vcenter)) + else: + __salt__['vsphere.create_storage_policy']( + policy['name'], policy, service_instance=si) + comments.append('Created storage policy \'{0}\' on ' + 'vCenter \'{1}\''.format(policy['name'], + vcenter)) + changes.append({'new': policy, 'old': None}) + log.trace(comments[-1]) + # Continue with next + continue + + # Building all diffs between the current and expected policy + # XXX We simplify the comparison by assuming we have at most 1 + # sub_profile + if policy.get('subprofiles'): + if len(policy['subprofiles']) > 1: + raise ArgumentValueError('Multiple sub_profiles ({0}) are not ' + 'supported in the input policy') + subprofile = policy['subprofiles'][0] + current_subprofile = current_policy['subprofiles'][0] + capabilities_differ = list_diff(current_subprofile['capabilities'], + subprofile.get('capabilities', []), + key='id') + del policy['subprofiles'] + if subprofile.get('capabilities'): + del subprofile['capabilities'] + del current_subprofile['capabilities'] + # Get the subprofile diffs without the capability keys + subprofile_differ = recursive_diff(current_subprofile, + dict(subprofile)) + + del current_policy['subprofiles'] + policy_differ = recursive_diff(current_policy, policy) + if policy_differ.diffs or capabilities_differ.diffs or \ + subprofile_differ.diffs: + + changes_required = True + if __opts__['test']: + str_changes = [] + if policy_differ.diffs: + str_changes.extend( + [change for change in + policy_differ.changes_str.split('\n')]) + if subprofile_differ.diffs or \ + capabilities_differ.diffs: + + str_changes.append('subprofiles:') + if subprofile_differ.diffs: + str_changes.extend( + [' {0}'.format(change) for change in + subprofile_differ.changes_str.split('\n')]) + if capabilities_differ.diffs: + str_changes.append(' capabilities:') + str_changes.extend( + [' {0}'.format(change) for change in + capabilities_differ.changes_str2.split('\n')]) + comments.append( + 'State {0} will update the storage policy \'{1}\'' + ' on vCenter \'{2}\':\n{3}' + ''.format(name, policy['name'], vcenter, + '\n'.join( str_changes))) + else: + __salt__['vsphere.update_storage_policy']( + policy=current_policy['name'], + policy_dict=policy_copy, + service_instance=si) + comments.append('Updated the storage policy \'{0}\'' + 'in vCenter \'{1}\'' + ''.format(policy['name'], vcenter)) + log.info(comments[-1]) + + # Build new/old values to report what was changed + new_values = policy_differ.new_values + new_values['subprofiles'] = [subprofile_differ.new_values] + new_values['subprofiles'][0]['capabilities'] = \ + capabilities_differ.new_values + if not new_values['subprofiles'][0]['capabilities']: + del new_values['subprofiles'][0]['capabilities'] + if not new_values['subprofiles'][0]: + del new_values['subprofiles'] + old_values = policy_differ.old_values + old_values['subprofiles'] = [subprofile_differ.old_values] + old_values['subprofiles'][0]['capabilities'] = \ + capabilities_differ.old_values + if not old_values['subprofiles'][0]['capabilities']: + del old_values['subprofiles'][0]['capabilities'] + if not old_values['subprofiles'][0]: + del old_values['subprofiles'] + changes.append({'new': new_values, + 'old': old_values}) + else: + # No diffs found - no updates required + comments.append('Storage policy \'{0}\' is up to date. ' + 'Nothing to be done.'.format(policy['name'])) + __salt__['vsphere.disconnect'](si) + except CommandExecutionError as exc: + log.error('Error: {0}'.format(exc)) + if si: + __salt__['vsphere.disconnect'](si) + if not __opts__['test']: + ret['result'] = False + ret.update({'comment': exc.strerror, + 'result': False if not __opts__['test'] else None}) + return ret + if not changes_required: + # We have no changes + ret.update({'comment': ('All storage policy in vCenter ' + '\'{0}\' is correctly configured. ' + 'Nothing to be done.'.format(vcenter)), + 'result': True}) + else: + ret.update({'comment': '\n'.join(comments)}) + if __opts__['test']: + ret.update({'pchanges': {'storage_policies': changes}, + 'result': None}) + else: + ret.update({'changes': {'storage_policies': changes}, + 'result': True}) + return ret From 36fc89c9a2515c2ed4bdee6a375dae239377f403 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 20 Sep 2017 21:09:45 -0400 Subject: [PATCH 288/633] Added salt.states.pbm.default_storage_policy_assigned state that manages default storage policies to datastore assigments --- salt/states/pbm.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/salt/states/pbm.py b/salt/states/pbm.py index 5483315195..e77f16f48b 100644 --- a/salt/states/pbm.py +++ b/salt/states/pbm.py @@ -437,3 +437,64 @@ def storage_policies_configured(name, policies): ret.update({'changes': {'storage_policies': changes}, 'result': True}) return ret + + +def default_storage_policy_assigned(name, policy, datastore): + ''' + Assigns a default storage policy to a datastore + + policy + Name of storage policy + + datastore + Name of datastore + ''' + log.info('Running state {0} for policy \'{1}\, datastore \'{2}\'.' + ''.format(name, policy, datastore)) + changes = {} + changes_required = False + ret = {'name': name, 'changes': {}, 'result': None, 'comment': None, + 'pchanges': {}} + si = None + try: + si = __salt__['vsphere.get_service_instance_via_proxy']() + existing_policy = \ + __salt__['vsphere.list_default_storage_policy_of_datastore']( + datastore=datastore, service_instance=si) + if existing_policy['name'] == policy: + comment = ('Storage policy \'{0}\' is already assigned to ' + 'datastore \'{1}\'. Nothing to be done.' + ''.format(policy, datastore)) + else: + changes_required = True + changes = { + 'default_storage_policy': {'old': existing_policy['name'], + 'new': policy}} + if (__opts__['test']): + comment = ('State {0} will assign storage policy \'{1}\' to ' + 'datastore \'{2}\'.').format(name, policy, + datastore) + else: + __salt__['vsphere.assign_default_storage_policy_to_datastore']( + policy=policy, datastore=datastore, service_instance=si) + comment = ('Storage policy \'{0} was assigned to datastore ' + '\'{1}\'.').format(policy, name) + log.info(comment) + except CommandExecutionError as exc: + log.error('Error: {}'.format(exc)) + if si: + __salt__['vsphere.disconnect'](si) + ret.update({'comment': exc.strerror, + 'result': False if not __opts__['test'] else None}) + return ret + ret['comment'] = comment + if changes_required: + if __opts__['test']: + ret.update({'result': None, + 'pchanges': changes}) + else: + ret.update({'result': True, + 'changes': changes}) + else: + ret['result'] = True + return ret From b6577e432894ad31cd3781201415b51e0af1c541 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 21 Sep 2017 12:54:23 -0400 Subject: [PATCH 289/633] pylint --- salt/modules/vsphere.py | 26 +++++++++++++------------- salt/proxy/vcenter.py | 8 ++++---- salt/states/pbm.py | 21 +++++---------------- salt/utils/pbm.py | 5 ++++- salt/utils/vmware.py | 2 +- tests/unit/modules/test_vsphere.py | 1 - tests/unit/utils/test_pbm.py | 14 +++++++------- 7 files changed, 34 insertions(+), 43 deletions(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index d4421ce1de..bc59077b1a 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4700,8 +4700,8 @@ def list_default_vsan_policy(service_instance=None): def_policies = [p for p in policies if p.systemCreatedProfileType == 'VsanDefaultProfile'] if not def_policies: - raise excs.VMwareObjectRetrievalError('Default VSAN policy was not ' - 'retrieved') + raise VMwareObjectRetrievalError('Default VSAN policy was not ' + 'retrieved') return _get_policy_dict(def_policies[0]) @@ -4854,8 +4854,8 @@ def update_storage_policy(policy, policy_dict, service_instance=None): profile_manager = salt.utils.pbm.get_profile_manager(service_instance) policies = salt.utils.pbm.get_storage_policies(profile_manager, [policy]) if not policies: - raise excs.VMwareObjectRetrievalError('Policy \'{0}\' was not found' - ''.format(policy)) + raise VMwareObjectRetrievalError('Policy \'{0}\' was not found' + ''.format(policy)) policy_ref = policies[0] policy_update_spec = pbm.profile.CapabilityBasedProfileUpdateSpec() log.trace('Setting policy values in policy_update_spec') @@ -4893,8 +4893,8 @@ def list_default_storage_policy_of_datastore(datastore, service_instance=None): ds_refs = salt.utils.vmware.get_datastores(service_instance, target_ref, datastore_names=[datastore]) if not ds_refs: - raise excs.VMwareObjectRetrievalError('Datastore \'{0}\' was not ' - 'found'.format(datastore)) + raise VMwareObjectRetrievalError('Datastore \'{0}\' was not ' + 'found'.format(datastore)) profile_manager = salt.utils.pbm.get_profile_manager(service_instance) policy = salt.utils.pbm.get_default_storage_policy_of_datastore( profile_manager, ds_refs[0]) @@ -4927,12 +4927,12 @@ def assign_default_storage_policy_to_datastore(policy, datastore, ''' log.trace('Assigning policy {0} to datastore {1}' ''.format(policy, datastore)) - profile_manager = utils_pbm.get_profile_manager(service_instance) + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) # Find policy - policies = utils_pbm.get_storage_policies(profile_manager, [policy]) + policies = salt.utils.pbm.get_storage_policies(profile_manager, [policy]) if not policies: - raise excs.VMwareObjectRetrievalError('Policy \'{0}\' was not found' - ''.format(policy)) + raise VMwareObjectRetrievalError('Policy \'{0}\' was not found' + ''.format(policy)) policy_ref = policies[0] # Find datastore target_ref = _get_proxy_target(service_instance) @@ -4942,9 +4942,9 @@ def assign_default_storage_policy_to_datastore(policy, datastore, raise excs.VMwareObjectRetrievalError('Datastore \'{0}\' was not ' 'found'.format(datastore)) ds_ref = ds_refs[0] - utils_pbm.assign_default_storage_policy_to_datastore(profile_manager, - policy_ref, ds_ref) - return {'assign_storage_policy_to_datastore': True} + salt.utils.pbm.assign_default_storage_policy_to_datastore( + profile_manager, policy_ref, ds_ref) + return True @depends(HAS_PYVMOMI) diff --git a/salt/proxy/vcenter.py b/salt/proxy/vcenter.py index 7b9c9f95e3..5c5ad797d1 100644 --- a/salt/proxy/vcenter.py +++ b/salt/proxy/vcenter.py @@ -189,7 +189,7 @@ import os # Import Salt Libs import salt.exceptions -from salt.config.schemas.vcenter import VCenterProxySchema +from salt.config.schemas.vcenter import VCenterProxySchema from salt.utils.dictupdate import merge # This must be present or the Salt loader won't load this module. @@ -250,18 +250,18 @@ def init(opts): raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'userpass\' , but no ' '\'username\' key found in proxy config') - if not 'passwords' in proxy_conf: + if 'passwords' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'userpass\' , but no ' '\'passwords\' key found in proxy config') for key in ('username', 'passwords'): DETAILS[key] = proxy_conf[key] else: - if not 'domain' in proxy_conf: + if 'domain' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'sspi\' , but no ' '\'domain\' key found in proxy config') - if not 'principal' in proxy_conf: + if 'principal' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'sspi\' , but no ' '\'principal\' key found in proxy config') diff --git a/salt/states/pbm.py b/salt/states/pbm.py index e77f16f48b..bf54f620ad 100644 --- a/salt/states/pbm.py +++ b/salt/states/pbm.py @@ -95,32 +95,21 @@ PyVmomi can be installed via pip: # Import Python Libs from __future__ import absolute_import -import sys import logging -import json -import time import copy # Import Salt Libs from salt.exceptions import CommandExecutionError, ArgumentValueError -import salt.modules.vsphere as vsphere -from salt.utils import is_proxy from salt.utils.dictdiffer import recursive_diff from salt.utils.listdiffer import list_diff -# External libraries -try: - import jsonschema - HAS_JSONSCHEMA = True -except ImportError: - HAS_JSONSCHEMA = False - # Get Logging Started log = logging.getLogger(__name__) # TODO change with vcenter ALLOWED_PROXY_TYPES = ['esxcluster', 'vcenter'] LOGIN_DETAILS = {} + def __virtual__(): if HAS_JSONSCHEMA: return True @@ -297,7 +286,7 @@ def storage_policies_configured(name, policies): # All allowed proxies have a vcenter detail vcenter = __salt__['{0}.get_details'.format(proxy_type)]()['vcenter'] log.info('Running state \'{0}\' on vCenter ' - '\'{0}\''.format(name, vcenter)) + '\'{1}\''.format(name, vcenter)) si = __salt__['vsphere.get_service_instance_via_proxy']() current_policies = __salt__['vsphere.list_storage_policies']( policy_names=[policy['name'] for policy in policies], @@ -378,7 +367,7 @@ def storage_policies_configured(name, policies): 'State {0} will update the storage policy \'{1}\'' ' on vCenter \'{2}\':\n{3}' ''.format(name, policy['name'], vcenter, - '\n'.join( str_changes))) + '\n'.join(str_changes))) else: __salt__['vsphere.update_storage_policy']( policy=current_policy['name'], @@ -449,7 +438,7 @@ def default_storage_policy_assigned(name, policy, datastore): datastore Name of datastore ''' - log.info('Running state {0} for policy \'{1}\, datastore \'{2}\'.' + log.info('Running state {0} for policy \'{1}\', datastore \'{2}\'.' ''.format(name, policy, datastore)) changes = {} changes_required = False @@ -470,7 +459,7 @@ def default_storage_policy_assigned(name, policy, datastore): changes = { 'default_storage_policy': {'old': existing_policy['name'], 'new': policy}} - if (__opts__['test']): + if __opts__['test']: comment = ('State {0} will assign storage policy \'{1}\' to ' 'datastore \'{2}\'.').format(name, policy, datastore) diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py index 17b25aceca..c7fa43eaa4 100644 --- a/salt/utils/pbm.py +++ b/salt/utils/pbm.py @@ -171,7 +171,7 @@ def get_policies_by_id(profile_manager, policy_ids): raise VMwareRuntimeError(exc.msg) -def get_storage_policies(profile_manager, policy_names=[], +def get_storage_policies(profile_manager, policy_names=None, get_all_policies=False): ''' Returns a list of the storage policies, filtered by name. @@ -181,6 +181,7 @@ def get_storage_policies(profile_manager, policy_names=[], policy_names List of policy names to filter by. + Default is None. get_all_policies Flag specifying to return all policies, regardless of the specified @@ -207,6 +208,8 @@ def get_storage_policies(profile_manager, policy_names=[], pbm.profile.ResourceTypeEnum.STORAGE] if get_all_policies: return policies + if not policy_names: + policy_names = [] return [p for p in policies if p.name in policy_names] diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index cbfb741dc0..018bb10417 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -434,7 +434,7 @@ def get_new_service_instance_stub(service_instance, path, ns=None, #connection handshaking rule. We may need turn of the hostname checking #and client side cert verification context = None - if sys.version_info[:3] > (2,7,8): + if sys.version_info[:3] > (2, 7, 8): context = ssl.create_default_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index 9ebad77363..ed043f2728 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -648,7 +648,6 @@ class _GetProxyConnectionDetailsTestCase(TestCase, LoaderModuleMockMixin): 'principal': 'fake_principal', 'domain': 'fake_domain'} - def tearDown(self): for attrname in ('esxi_host_details', 'esxi_vcenter_details', 'esxdatacenter_details', 'esxcluster_details'): diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index 4e08229e26..aec9a51da5 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -10,7 +10,6 @@ from __future__ import absolute_import import logging # Import Salt testing libraries -from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import TestCase, skipIf from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, \ PropertyMock @@ -18,6 +17,7 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, \ # Import Salt libraries from salt.exceptions import VMwareApiError, VMwareRuntimeError, \ VMwareObjectRetrievalError +from salt.ext.six.moves import range import salt.utils.pbm try: @@ -187,9 +187,9 @@ class GetCapabilityDefinitionsTestCase(TestCase): '''Tests for salt.utils.pbm.get_capability_definitions''' def setUp(self): self.mock_res_type = MagicMock() - self.mock_cap_cats =[MagicMock(capabilityMetadata=['fake_cap_meta1', - 'fake_cap_meta2']), - MagicMock(capabilityMetadata=['fake_cap_meta3'])] + self.mock_cap_cats = [MagicMock(capabilityMetadata=['fake_cap_meta1', + 'fake_cap_meta2']), + MagicMock(capabilityMetadata=['fake_cap_meta3'])] self.mock_prof_mgr = MagicMock( FetchCapabilityMetadata=MagicMock(return_value=self.mock_cap_cats)) patches = ( @@ -312,7 +312,7 @@ class GetStoragePoliciesTestCase(TestCase): self.mock_prof_mgr = MagicMock( QueryProfile=MagicMock(return_value=self.mock_policy_ids)) # Policies - self.mock_policies=[] + self.mock_policies = [] for i in range(4): mock_obj = MagicMock(resourceType=MagicMock( resourceType=pbm.profile.ResourceTypeEnum.STORAGE)) @@ -576,7 +576,7 @@ class GetDefaultStoragePolicyOfDatastoreTestCase(TestCase): def test_no_policy_refs(self): mock_get_policies_by_id = MagicMock() - with path('salt.utils.pbm.get_policies_by_id', + with patch('salt.utils.pbm.get_policies_by_id', MagicMock(return_value=None)): with self.assertRaises(VMwareObjectRetrievalError) as excinfo: salt.utils.pbm.get_default_storage_policy_of_datastore( @@ -585,7 +585,7 @@ class GetDefaultStoragePolicyOfDatastoreTestCase(TestCase): 'Storage policy with id \'fake_policy_id\' was not ' 'found') - def test_no_policy_refs(self): + def test_return_policy_ref(self): mock_get_policies_by_id = MagicMock() ret = salt.utils.pbm.get_default_storage_policy_of_datastore( self.mock_prof_mgr, self.mock_ds) From f484bd52fd863af1ab55d729e9bf4182858cb6a6 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 21 Sep 2017 15:33:00 -0400 Subject: [PATCH 290/633] more pylint --- salt/states/pbm.py | 4 +--- tests/unit/utils/vmware/test_connection.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/salt/states/pbm.py b/salt/states/pbm.py index bf54f620ad..775b716f44 100644 --- a/salt/states/pbm.py +++ b/salt/states/pbm.py @@ -111,9 +111,7 @@ LOGIN_DETAILS = {} def __virtual__(): - if HAS_JSONSCHEMA: - return True - return False + return True def mod_init(low): diff --git a/tests/unit/utils/vmware/test_connection.py b/tests/unit/utils/vmware/test_connection.py index dd357d4870..d8afbb0504 100644 --- a/tests/unit/utils/vmware/test_connection.py +++ b/tests/unit/utils/vmware/test_connection.py @@ -25,7 +25,7 @@ import salt.utils.vmware from salt.ext import six try: - from pyVmomi import vim, vmodl, VmomiSupport + from pyVmomi import vim, vmodl HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False From e1bfe248915d6fc623e3bfb4a96c008821dd760a Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 22 Sep 2017 08:59:33 -0400 Subject: [PATCH 291/633] Removed excs reference from new methods in salt.modules.vsphere --- salt/modules/vsphere.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index bc59077b1a..0c92385804 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -4939,8 +4939,8 @@ def assign_default_storage_policy_to_datastore(policy, datastore, ds_refs = salt.utils.vmware.get_datastores(service_instance, target_ref, datastore_names=[datastore]) if not ds_refs: - raise excs.VMwareObjectRetrievalError('Datastore \'{0}\' was not ' - 'found'.format(datastore)) + raise VMwareObjectRetrievalError('Datastore \'{0}\' was not ' + 'found'.format(datastore)) ds_ref = ds_refs[0] salt.utils.pbm.assign_default_storage_policy_to_datastore( profile_manager, policy_ref, ds_ref) From 4ff745d2c5d30805222d9d7981aa7302b94c4542 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 22 Sep 2017 15:15:47 -0400 Subject: [PATCH 292/633] Added python/pyvmomi compatibility check to salt.states.pbm + removed reference to Python 2.6 --- salt/states/pbm.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/salt/states/pbm.py b/salt/states/pbm.py index 775b716f44..00945fc65c 100644 --- a/salt/states/pbm.py +++ b/salt/states/pbm.py @@ -97,20 +97,34 @@ PyVmomi can be installed via pip: from __future__ import absolute_import import logging import copy +import sys # Import Salt Libs from salt.exceptions import CommandExecutionError, ArgumentValueError from salt.utils.dictdiffer import recursive_diff from salt.utils.listdiffer import list_diff +# External libraries +try: + from pyVmomi import VmomiSupport + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + # Get Logging Started log = logging.getLogger(__name__) -# TODO change with vcenter -ALLOWED_PROXY_TYPES = ['esxcluster', 'vcenter'] -LOGIN_DETAILS = {} def __virtual__(): + if not HAS_PYVMOMI: + return False, 'State module did not load: pyVmomi not found' + + # We check the supported vim versions to infer the pyVmomi version + if 'vim25/6.0' in VmomiSupport.versionMap and \ + sys.version_info > (2, 7) and sys.version_info < (2, 7, 9): + + return False, ('State module did not load: Incompatible versions ' + 'of Python and pyVmomi present. See Issue #29537.') return True From ac79f89ffa92bbb2c27c3a1418f6fb63b93838e0 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 26 Sep 2017 04:59:00 -0400 Subject: [PATCH 293/633] Fixed utils.pbm unit tests --- tests/unit/utils/test_pbm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py index aec9a51da5..6c2be0f9b5 100644 --- a/tests/unit/utils/test_pbm.py +++ b/tests/unit/utils/test_pbm.py @@ -214,7 +214,7 @@ class GetCapabilityDefinitionsTestCase(TestCase): def test_fetch_capabilities(self): salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) - self.mock_prof_mgr.FetchCapabilityMetadata.assert_callend_once_with( + self.mock_prof_mgr.FetchCapabilityMetadata.assert_called_once_with( self.mock_res_type) def test_fetch_capabilities_raises_no_permissions(self): @@ -268,7 +268,7 @@ class GetPoliciesByIdTestCase(TestCase): def test_retrieve_policies(self): salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) - self.mock_prof_mgr.RetrieveContent.assert_callend_once_with( + self.mock_prof_mgr.RetrieveContent.assert_called_once_with( self.policy_ids) def test_retrieve_policies_raises_no_permissions(self): From 6e5c99bda0329e2ccec7514edff1ba533b176755 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 25 Sep 2017 16:14:51 -0500 Subject: [PATCH 294/633] Allow docker_events engine to work with newer docker-py The Client attribute was renamed to APIClient in docker-py 2.0 --- salt/engines/docker_events.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/salt/engines/docker_events.py b/salt/engines/docker_events.py index 9028ad8d82..6971c69324 100644 --- a/salt/engines/docker_events.py +++ b/salt/engines/docker_events.py @@ -74,8 +74,12 @@ def start(docker_url='unix://var/run/docker.sock', else: __salt__['event.send'](tag, msg) - client = docker.Client(base_url=docker_url, - timeout=timeout) + try: + # docker-py 2.0 renamed this client attribute + client = docker.APIClient(base_url=docker_url, timeout=timeout) + except AttributeError: + client = docker.Client(base_url=docker_url, timeout=timeout) + try: events = client.events() for event in events: From 553335b1c939df6b4cbe6871e8764178a9a50d5e Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 26 Sep 2017 07:52:59 -0500 Subject: [PATCH 295/633] Fix incorrect value in docstring --- salt/modules/win_wua.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/win_wua.py b/salt/modules/win_wua.py index 237fb74924..63409951e2 100644 --- a/salt/modules/win_wua.py +++ b/salt/modules/win_wua.py @@ -110,7 +110,7 @@ def available(software=True, Include software updates in the results (default is True) drivers (bool): - Include driver updates in the results (default is False) + Include driver updates in the results (default is True) summary (bool): - True: Return a summary of updates available for each category. From 7b9c3726771e911c9590e2f399077c93e8870cd6 Mon Sep 17 00:00:00 2001 From: Eric Radman Date: Tue, 26 Sep 2017 09:38:41 -0400 Subject: [PATCH 296/633] Only inspect file attribute if lsattr(1) is installed lsattr/chattr is not installed on many Unix-like platforms by default, including *BSD, Solaris, and minimal Linux distributions such as Alpine. --- salt/modules/file.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index 7dfd5ced01..f2ee22655a 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -4281,7 +4281,8 @@ def extract_hash(hash_fn, def check_perms(name, ret, user, group, mode, attrs=None, follow_symlinks=False): ''' - Check the permissions on files, modify attributes and chown if needed + Check the permissions on files, modify attributes and chown if needed. File + attributes are only verified if lsattr(1) is installed. CLI Example: @@ -4293,6 +4294,7 @@ def check_perms(name, ret, user, group, mode, attrs=None, follow_symlinks=False) ``follow_symlinks`` option added ''' name = os.path.expanduser(name) + lsattr_cmd = salt.utils.path.which('lsattr') if not ret: ret = {'name': name, @@ -4318,7 +4320,7 @@ def check_perms(name, ret, user, group, mode, attrs=None, follow_symlinks=False) perms['lmode'] = salt.utils.normalize_mode(cur['mode']) is_dir = os.path.isdir(name) - if not salt.utils.platform.is_windows() and not is_dir: + if not salt.utils.platform.is_windows() and not is_dir and lsattr_cmd: # List attributes on file perms['lattrs'] = ''.join(lsattr(name)[name]) # Remove attributes on file so changes can be enforced. @@ -4429,7 +4431,7 @@ def check_perms(name, ret, user, group, mode, attrs=None, follow_symlinks=False) if __opts__['test'] is True and ret['changes']: ret['result'] = None - if not salt.utils.platform.is_windows() and not is_dir: + if not salt.utils.platform.is_windows() and not is_dir and lsattr_cmd: # Replace attributes on file if it had been removed if perms['lattrs']: chattr(name, operator='add', attributes=perms['lattrs']) From 23bb4a5ddeeb3fc86a8b8e50bddb2b3c9a893046 Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 25 Sep 2017 17:18:28 -0400 Subject: [PATCH 297/633] Add GPG Verification section to Contributing Docs When we enable GPG Verification for pull request reviews, we should make sure there is information available in our Contributing docs about how to sign commits. --- doc/topics/development/contributing.rst | 27 ++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/doc/topics/development/contributing.rst b/doc/topics/development/contributing.rst index 09286295f3..ed4fd2aa12 100644 --- a/doc/topics/development/contributing.rst +++ b/doc/topics/development/contributing.rst @@ -488,9 +488,31 @@ to help review incoming pull-requests based on their past contribution to files which are being changed. If you do not wish to receive these notifications, please add your GitHub -handle to the blacklist line in the `.mention-bot` file located in the +handle to the blacklist line in the ``.mention-bot`` file located in the root of the Salt repository. +.. _probot-gpg-verification: + +GPG Verification +---------------- + +SaltStack has enabled `GPG Probot`_ to enforce GPG signatures for all +commits included in a Pull Request. + +In order for the GPG verification status check to pass, *every* contributor in +the pull request must: + +- Set up a GPG key on local machine +- Sign all commits in the pull request with key +- Link key with GitHub account + +This applies to all commits in the pull request. + +GitHub hosts a number of `help articles`_ for creating a GPG key, using the +GPG key with ``git`` locally, and linking the GPG key to your GitHub account. +Once these steps are completed, the commit signing verification will look like +the example in GitHub's `GPG Signature Verification feature announcement`_. + .. _`saltstack/salt`: https://github.com/saltstack/salt .. _`GitHub Fork a Repo Guide`: https://help.github.com/articles/fork-a-repo .. _`GitHub issue tracker`: https://github.com/saltstack/salt/issues @@ -499,3 +521,6 @@ root of the Salt repository. .. _`Closing issues via commit message`: https://help.github.com/articles/closing-issues-via-commit-messages .. _`git format-patch`: https://www.kernel.org/pub/software/scm/git/docs/git-format-patch.html .. _salt-users: https://groups.google.com/forum/#!forum/salt-users +.. _GPG Probot: https://probot.github.io/apps/gpg/ +.. _help articles: https://help.github.com/articles/signing-commits-with-gpg/ +.. _GPG Signature Verification feature announcement: https://github.com/blog/2144-gpg-signature-verification From 5a2593dbd34bf1a14120c085112070b7c2c515ef Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 26 Sep 2017 10:27:20 -0400 Subject: [PATCH 298/633] Add message to boto_kinesis modules if boto libs are missing Fixes #43737 --- salt/modules/boto_kinesis.py | 6 ++++-- salt/states/boto_kinesis.py | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/salt/modules/boto_kinesis.py b/salt/modules/boto_kinesis.py index fa75140e2d..cd0e3d7d3e 100644 --- a/salt/modules/boto_kinesis.py +++ b/salt/modules/boto_kinesis.py @@ -66,15 +66,17 @@ except ImportError: log = logging.getLogger(__name__) +__virtualname__ = 'boto_kinesis' + def __virtual__(): ''' Only load if boto3 libraries exist. ''' if not HAS_BOTO: - return False + return False, 'The boto_kinesis module could not be loaded: boto libraries not found.' __utils__['boto3.assign_funcs'](__name__, 'kinesis') - return True + return __virtualname__ def _get_basic_stream(stream_name, conn): diff --git a/salt/states/boto_kinesis.py b/salt/states/boto_kinesis.py index 6b7e73a495..ae8a5d63a7 100644 --- a/salt/states/boto_kinesis.py +++ b/salt/states/boto_kinesis.py @@ -63,13 +63,16 @@ import logging log = logging.getLogger(__name__) +__virtualname__ = 'boto_kinesis' + def __virtual__(): ''' Only load if boto_kinesis is available. ''' - ret = 'boto_kinesis' if 'boto_kinesis.exists' in __salt__ else False - return ret + if 'boto_kinesis.exists' in __salt__: + return __virtualname__ + return False, 'The boto_kinesis module could not be loaded: boto libraries not found.' def present(name, From f7df41fa9411da7cc96808680e8a65e9b99e753e Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 26 Sep 2017 12:59:23 -0600 Subject: [PATCH 299/633] split build and install for pkg osx --- pkg/osx/build.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/osx/build.sh b/pkg/osx/build.sh index 7850d48cd8..fcf4b4e061 100755 --- a/pkg/osx/build.sh +++ b/pkg/osx/build.sh @@ -88,7 +88,8 @@ sudo $PKGRESOURCES/build_env.sh $PYVER echo -n -e "\033]0;Build: Install Salt\007" sudo rm -rf $SRCDIR/build sudo rm -rf $SRCDIR/dist -sudo $PYTHON $SRCDIR/setup.py build -e "$PYTHON -E -s" install +sudo $PYTHON $SRCDIR/setup.py build -e "$PYTHON -E -s" +sudo $PYTHON $SRCDIR/setup.py install ############################################################################ # Build Package From 617c5b72acf76c77f48c966d7d3ad07111f9abe9 Mon Sep 17 00:00:00 2001 From: Eric Radman Date: Tue, 26 Sep 2017 15:11:57 -0400 Subject: [PATCH 300/633] Fix DeprecationWarning for use of 'salt.utils.is_windows' --- salt/modules/kubernetes.py | 4 ++-- tests/unit/modules/test_hosts.py | 4 ++-- tests/unit/returners/test_local_cache.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index 2257580270..36d7cc4df1 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -83,7 +83,7 @@ def __virtual__(): return False, 'python kubernetes library not found' -if not salt.utils.is_windows(): +if not salt.utils.platform.is_windows(): @contextmanager def _time_limit(seconds): def signal_handler(signum, frame): @@ -713,7 +713,7 @@ def delete_deployment(name, namespace='default', **kwargs): namespace=namespace, body=body) mutable_api_response = api_response.to_dict() - if not salt.utils.is_windows(): + if not salt.utils.platform.is_windows(): try: with _time_limit(POLLING_TIME_LIMIT): while show_deployment(name, namespace) is not None: diff --git a/tests/unit/modules/test_hosts.py b/tests/unit/modules/test_hosts.py index 56f01f56ab..7cd7699453 100644 --- a/tests/unit/modules/test_hosts.py +++ b/tests/unit/modules/test_hosts.py @@ -94,7 +94,7 @@ class HostsTestCase(TestCase, LoaderModuleMockMixin): Tests true if the alias is set ''' hosts_file = '/etc/hosts' - if salt.utils.is_windows(): + if salt.utils.platform.is_windows(): hosts_file = r'C:\Windows\System32\Drivers\etc\hosts' with patch('salt.modules.hosts.__get_hosts_filename', @@ -198,7 +198,7 @@ class HostsTestCase(TestCase, LoaderModuleMockMixin): Tests if specified host entry gets added from the hosts file ''' hosts_file = '/etc/hosts' - if salt.utils.is_windows(): + if salt.utils.platform.is_windows(): hosts_file = r'C:\Windows\System32\Drivers\etc\hosts' with patch('salt.utils.files.fopen', mock_open()), \ diff --git a/tests/unit/returners/test_local_cache.py b/tests/unit/returners/test_local_cache.py index 741957ffd8..aa7117efb5 100644 --- a/tests/unit/returners/test_local_cache.py +++ b/tests/unit/returners/test_local_cache.py @@ -97,7 +97,7 @@ class LocalCacheCleanOldJobsTestCase(TestCase, LoaderModuleMockMixin): local_cache.clean_old_jobs() # Get the name of the JID directory that was created to test against - if salt.utils.is_windows(): + if salt.utils.platform.is_windows(): jid_dir_name = jid_dir.rpartition('\\')[2] else: jid_dir_name = jid_dir.rpartition('/')[2] From 61f8a2f7ff28637ca22e8fe07bdbbf4dc342e6ca Mon Sep 17 00:00:00 2001 From: twangboy Date: Mon, 25 Sep 2017 15:14:13 -0600 Subject: [PATCH 301/633] Skip mac specific tests --- tests/unit/states/test_mac_package.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/states/test_mac_package.py b/tests/unit/states/test_mac_package.py index 3b2ebb5820..bb0399ff8a 100644 --- a/tests/unit/states/test_mac_package.py +++ b/tests/unit/states/test_mac_package.py @@ -2,19 +2,20 @@ # Import Python libs from __future__ import absolute_import +import sys # Import Salt Libs import salt.states.mac_package as macpackage # Import Salt Testing Libs from tests.support.mixins import LoaderModuleMockMixin -from tests.support.unit import TestCase +from tests.support.unit import TestCase, skipIf from tests.support.mock import ( MagicMock, patch ) - +@skipIf(not sys.platform.startswith('darwin'), "Mac specific test") class MacPackageTestCase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): return {macpackage: {}} From ec99a3ce3c6bc668d8f4b084746ea4f9118cd9dd Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 26 Sep 2017 09:23:38 -0600 Subject: [PATCH 302/633] Fix lint error --- tests/unit/states/test_mac_package.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/states/test_mac_package.py b/tests/unit/states/test_mac_package.py index bb0399ff8a..4446c2eb5e 100644 --- a/tests/unit/states/test_mac_package.py +++ b/tests/unit/states/test_mac_package.py @@ -15,6 +15,7 @@ from tests.support.mock import ( patch ) + @skipIf(not sys.platform.startswith('darwin'), "Mac specific test") class MacPackageTestCase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): From 1c01e06097dc66ef3f8b4ce16cff4f312d1223e6 Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 26 Sep 2017 14:01:03 -0600 Subject: [PATCH 303/633] Only skip test on Windows --- tests/unit/states/test_mac_package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/states/test_mac_package.py b/tests/unit/states/test_mac_package.py index 4446c2eb5e..beaf3db515 100644 --- a/tests/unit/states/test_mac_package.py +++ b/tests/unit/states/test_mac_package.py @@ -16,7 +16,7 @@ from tests.support.mock import ( ) -@skipIf(not sys.platform.startswith('darwin'), "Mac specific test") +@skipIf(sys.platform.startswith('win'), "Not a Windows test") class MacPackageTestCase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): return {macpackage: {}} From 8b16300495ce93686f8b835fc7f0db057a85d6bc Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 26 Sep 2017 15:24:58 -0500 Subject: [PATCH 304/633] Fix some regressions in recent legacy git_pillar deprecation These didn't get caught in PR 42823 because of how we invoke the git_pillar code. Firstly, the "pillar" argument needed to stay. This is because even though we're not using it, _external_pillar_data() is still passing it now that git_pillar is not specially invoked there. Secondly, since the input comes in as a list, and _external_pillar_data uses single-asterisk expansion, the repos are passed separately when they should be passed as a single list. To fix these issues, I've done the following: 1. Re-introduced the "pillar" argument in git_pillar's ext_pillar function. 2. Changed the "pillar" variable to avoid confusion with the (unused) "pillar" argument being passed in. 3. Instead of git_pillar accepting the repos as a list, the ext_pillar function now uses single-asterisk expansion to make it conform with how _external_pillar_data() invokes it. --- salt/pillar/git_pillar.py | 16 ++++++++-------- tests/support/gitfs.py | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/salt/pillar/git_pillar.py b/salt/pillar/git_pillar.py index 53e58be0ac..1c0f7b700f 100644 --- a/salt/pillar/git_pillar.py +++ b/salt/pillar/git_pillar.py @@ -374,20 +374,20 @@ def __virtual__(): return False -def ext_pillar(minion_id, repo): +def ext_pillar(minion_id, pillar, *repos): # pylint: disable=unused-argument ''' Checkout the ext_pillar sources and compile the resulting pillar SLS ''' opts = copy.deepcopy(__opts__) opts['pillar_roots'] = {} opts['__git_pillar'] = True - pillar = salt.utils.gitfs.GitPillar(opts) - pillar.init_remotes(repo, PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) + git_pillar = salt.utils.gitfs.GitPillar(opts) + git_pillar.init_remotes(repos, PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) if __opts__.get('__role') == 'minion': # If masterless, fetch the remotes. We'll need to remove this once # we make the minion daemon able to run standalone. - pillar.fetch_remotes() - pillar.checkout() + git_pillar.fetch_remotes() + git_pillar.checkout() ret = {} merge_strategy = __opts__.get( 'pillar_source_merging_strategy', @@ -397,7 +397,7 @@ def ext_pillar(minion_id, repo): 'pillar_merge_lists', False ) - for pillar_dir, env in six.iteritems(pillar.pillar_dirs): + for pillar_dir, env in six.iteritems(git_pillar.pillar_dirs): # If pillarenv is set, only grab pillars with that match pillarenv if opts['pillarenv'] and env != opts['pillarenv']: log.debug( @@ -406,7 +406,7 @@ def ext_pillar(minion_id, repo): env, pillar_dir, opts['pillarenv'] ) continue - if pillar_dir in pillar.pillar_linked_dirs: + if pillar_dir in git_pillar.pillar_linked_dirs: log.debug( 'git_pillar is skipping processing on %s as it is a ' 'mounted repo', pillar_dir @@ -433,7 +433,7 @@ def ext_pillar(minion_id, repo): # list, so that its top file is sourced from the correct # location and not from another git_pillar remote. pillar_roots.extend( - [d for (d, e) in six.iteritems(pillar.pillar_dirs) + [d for (d, e) in six.iteritems(git_pillar.pillar_dirs) if env == e and d != pillar_dir] ) diff --git a/tests/support/gitfs.py b/tests/support/gitfs.py index 411bfd27ce..7287147601 100644 --- a/tests/support/gitfs.py +++ b/tests/support/gitfs.py @@ -341,7 +341,8 @@ class GitPillarTestBase(GitTestBase, LoaderModuleMockMixin): with patch.dict(git_pillar.__opts__, ext_pillar_opts): return git_pillar.ext_pillar( 'minion', - ext_pillar_opts['ext_pillar'][0]['git'], + {}, + *ext_pillar_opts['ext_pillar'][0]['git'] ) def make_repo(self, root_dir, user='root'): From 3d5fce09559b3bd7b3f931fa2fd45208ac606006 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Tue, 26 Sep 2017 17:21:24 -0400 Subject: [PATCH 305/633] Add 2017.7.2 Release Notes --- doc/topics/releases/2017.7.2.rst | 3150 ++++++++++++++++++++++++++++++ 1 file changed, 3150 insertions(+) create mode 100644 doc/topics/releases/2017.7.2.rst diff --git a/doc/topics/releases/2017.7.2.rst b/doc/topics/releases/2017.7.2.rst new file mode 100644 index 0000000000..7ef2d4363a --- /dev/null +++ b/doc/topics/releases/2017.7.2.rst @@ -0,0 +1,3150 @@ +============================ +Salt 2017.7.2 Release Notes +============================ + +Version 2017.7.2 is a bugfix release for :ref:`2017.7.0 `. + +Changes for v2017.7.1..v2017.7.2 +-------------------------------- + +Extended changelog courtesy of Todd Stansell (https://github.com/tjstansell/salt-changelogs): + +*Generated at: 2017-09-26T21:06:19Z* + +Statistics: + +- Total Merges: **326** +- Total Issue references: **133** +- Total PR references: **389** + +Changes: + + +- **PR** `#43585`_: (*rallytime*) Back-port `#43330`_ to 2017.7.2 + @ *2017-09-19T17:33:34Z* + + - **ISSUE** `#43077`_: (*Manoj2087*) Issue with deleting key via wheel + | refs: `#43330`_ + - **PR** `#43330`_: (*terminalmage*) Fix reactor regression + unify reactor config schema + | refs: `#43585`_ + * 89f629233f Merge pull request `#43585`_ from rallytime/`bp-43330`_ + * c4f693bae8 Merge branch '2017.7.2' into `bp-43330`_ + +- **PR** `#43586`_: (*rallytime*) Back-port `#43526`_ to 2017.7.2 + @ *2017-09-19T15:36:27Z* + + - **ISSUE** `#43447`_: (*UtahDave*) When using Syndic with Multi Master the top level master doesn't reliably get returns from lower minion. + | refs: `#43526`_ + - **PR** `#43526`_: (*DmitryKuzmenko*) Forward events to all masters syndic connected to + | refs: `#43586`_ + * abb7fe4422 Merge pull request `#43586`_ from rallytime/`bp-43526`_ + * e076e9b634 Forward events to all masters syndic connected to. + + * 7abd07fa07 Simplify client logic + + * b5f10696c2 Improve the reactor documentation + + * 7a2f12b96a Include a better example for reactor in master conf file + + * 531cac610e Rewrite the reactor unit tests + + * 2a35ab7f39 Unify reactor configuration, fix caller reactors + + * 4afb179bad Un-deprecate passing kwargs outside of 'kwarg' param + +- **PR** `#43551`_: (*twangboy*) Fix preinstall script on OSX for 2017.7.2 + @ *2017-09-18T18:35:35Z* + + * 3d3b09302d Merge pull request `#43551`_ from twangboy/osx_fix_preinstall_2017.7.2 + * c3d9fb63f0 Merge branch '2017.7.2' into osx_fix_preinstall_2017.7.2 + +- **PR** `#43509`_: (*rallytime*) Back-port `#43333`_ to 2017.7.2 + @ *2017-09-15T21:21:40Z* + + - **ISSUE** `#2`_: (*thatch45*) salt job queries + - **PR** `#43333`_: (*damon-atkins*) Docs are wrong cache_dir (bool) and cache_file (str) cannot be passed as params + 1 bug + | refs: `#43509`_ + * 24691da888 Merge pull request `#43509`_ from rallytime/`bp-43333`_-2017.7.2 + * b3dbafb035 Update doco + + * 5cdcdbf428 Update win_pkg.py + + * c3e16661c3 Docs are wrong cache_dir (bool) and cache_file (str) cannot be passed on the cli (`#2`_) + + * f33395f1ee Fix logic in `/etc/paths.d/salt` detection + +- **PR** `#43440`_: (*rallytime*) Back-port `#43421`_ to 2017.7.2 + @ *2017-09-11T20:59:53Z* + + - **PR** `#43421`_: (*gtmanfred*) Revert "Reduce fileclient.get_file latency by merging _file_find and … + | refs: `#43440`_ + * 8964cacbf8 Merge pull request `#43440`_ from rallytime/`bp-43421`_ + * ea6e661755 Revert "Reduce fileclient.get_file latency by merging _file_find and _file_hash" + +- **PR** `#43377`_: (*rallytime*) Back-port `#43193`_ to 2017.7.2 + @ *2017-09-11T15:32:23Z* + + - **PR** `#43193`_: (*jettero*) Prevent spurious "Template does not exist" error + | refs: `#43377`_ + - **PR** `#39516`_: (*jettero*) Prevent spurious "Template does not exist" error + | refs: `#43193`_ + * 7fda186b18 Merge pull request `#43377`_ from rallytime/`bp-43193`_ + * 842b07fd25 Prevent spurious "Template does not exist" error + +- **PR** `#43315`_: (*rallytime*) Back-port `#43283`_ to 2017.7.2 + @ *2017-09-05T20:04:25Z* + + - **ISSUE** `#42459`_: (*iavael*) Broken ldap groups retrieval in salt.auth.ldap after upgrade to 2017.7 + | refs: `#43283`_ + - **PR** `#43283`_: (*DmitryKuzmenko*) Fix ldap token groups auth. + | refs: `#43315`_ + * 85dba1e898 Merge pull request `#43315`_ from rallytime/`bp-43283`_ + * f29f5b0cce Fix for tests: don't require 'groups' in the eauth token. + + * 56938d5bf2 Fix ldap token groups auth. + +- **PR** `#43266`_: (*gtmanfred*) switch virtualbox cloud driver to use __utils__ + @ *2017-08-30T18:36:20Z* + + - **ISSUE** `#43259`_: (*mahesh21*) NameError: global name '__opts__' is not defined + | refs: `#43266`_ + * 26ff8088cb Merge pull request `#43266`_ from gtmanfred/virtualbox + * 382bf92de7 switch virtualbox cloud driver to use __utils__ + +- **PR** `#43073`_: (*Mapel88*) Fix bug `#42936`_ - win_iis module container settings + @ *2017-08-30T18:34:37Z* + + - **ISSUE** `#43110`_: (*Mapel88*) bug in iis_module - create_cert_binding + - **ISSUE** `#42936`_: (*Mapel88*) bug in win_iis module & state - container_setting + | refs: `#43073`_ + * ee209b144c Merge pull request `#43073`_ from Mapel88/patch-2 + * b1a3d15b28 Remove trailing whitespace for linter + + * 25c8190e48 Fix pylint errors + + * 1eba8c4b8e Fix pylint errors + + * 290d7b54af Fix plint errors + + * f4f32421ab Fix plint errors + + * ec20e9a19a Fix bug `#43110`_ - win_iis module + + * 009ef6686b Fix dictionary keys from string to int + + * dc793f9a05 Fix bug `#42936`_ - win_iis state + + * 13404a47b5 Fix bug `#42936`_ - win_iis module + +- **PR** `#43254`_: (*twangboy*) Fix `unit.modules.test_inspect_collector` on Windows + @ *2017-08-30T15:46:07Z* + + * ec1bedc646 Merge pull request `#43254`_ from twangboy/win_fix_test_inspect_collector + * b401340e6c Fix `unit.modules.test_inspect_collector` on Windows + +- **PR** `#43255`_: (*gtmanfred*) always return a dict object + @ *2017-08-30T14:47:15Z* + + - **ISSUE** `#43241`_: (*mirceaulinic*) Error whilst collecting napalm grains + | refs: `#43255`_ + * 1fc7307735 Merge pull request `#43255`_ from gtmanfred/2017.7 + * 83b0bab34b opt_args needs to be a dict + +- **PR** `#43229`_: (*twangboy*) Bring changes from `#43228`_ to 2017.7 + @ *2017-08-30T14:26:55Z* + + - **PR** `#43228`_: (*twangboy*) Win fix pkg.install + | refs: `#43229`_ + * fa904ee225 Merge pull request `#43229`_ from twangboy/win_fix_pkg.install-2017.7 + * e007a1c26e Fix regex, add `.` + + * 23ec47c74c Add _ to regex search + + * b1788b1e5f Bring changes from `#43228`_ to 2017.7 + +- **PR** `#43251`_: (*twangboy*) Skips `unit.modules.test_groupadd` on Windows + @ *2017-08-30T13:56:36Z* + + * 25666f88f7 Merge pull request `#43251`_ from twangboy/win_skip_test_groupadd + * 5185071d5a Skips `unit.modules.test_groupadd` on Windows + +- **PR** `#43256`_: (*twangboy*) Skip mac tests for user and group + @ *2017-08-30T13:18:13Z* + + * a8e09629b2 Merge pull request `#43256`_ from twangboy/win_skip_mac_tests + * cec627a60b Skip mac tests for user and group + +- **PR** `#43226`_: (*lomeroe*) Fixes for issues in PR `#43166`_ + @ *2017-08-29T19:05:39Z* + + - **ISSUE** `#42279`_: (*dafyddj*) win_lgpo matches multiple policies due to startswith() + | refs: `#43116`_ `#43116`_ `#43166`_ `#43226`_ `#43156`_ + - **PR** `#43166`_: (*lomeroe*) Backport `#43116`_ to 2017.7 + | refs: `#43226`_ + - **PR** `#43156`_: (*lomeroe*) Backport `#43116`_ to 2017.7 + | refs: `#43166`_ + - **PR** `#43116`_: (*lomeroe*) Fix 42279 in develop + | refs: `#43166`_ `#43156`_ + - **PR** `#39773`_: (*twangboy*) Make win_file use the win_dacl salt util + | refs: `#43226`_ + * ac2189c870 Merge pull request `#43226`_ from lomeroe/fix_43166 + * 0c424dc4a3 Merge branch '2017.7' into fix_43166 + + * 324cfd8d1e correcting bad format statement in search for policy to be disabled (fix for `#43166`_) verify that file exists before attempting to remove (fix for commits from `#39773`_) + +- **PR** `#43227`_: (*twangboy*) Fix `unit.fileserver.test_gitfs` for Windows + @ *2017-08-29T19:03:36Z* + + * 6199fb46dc Merge pull request `#43227`_ from twangboy/win_fix_unit_test_gitfs + * c956d24283 Fix is_windows detection when USERNAME missing + + * 869e8cc603 Fix `unit.fileserver.test_gitfs` for Windows + +- **PR** `#43217`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-28T16:36:28Z* + + - **ISSUE** `#43101`_: (*aogier*) genesis.bootstrap fails if no pkg AND exclude_pkgs (which can't be a string) + | refs: `#43103`_ + - **ISSUE** `#42642`_: (*githubcdr*) state.augeas + | refs: `#42669`_ `#43202`_ + - **ISSUE** `#42329`_: (*jagguli*) State git.latest does not pull latest tags + | refs: `#42663`_ + - **PR** `#43202`_: (*garethgreenaway*) Reverting previous augeas module changes + - **PR** `#43103`_: (*aogier*) genesis.bootstrap deboostrap fix + - **PR** `#42663`_: (*jagguli*) Check remote tags before deciding to do a fetch `#42329`_ + * 6adc03e4b4 Merge pull request `#43217`_ from rallytime/merge-2017.7 + * 3911df2f4b Merge branch '2016.11' into '2017.7' + + * 5308c27f9f Merge pull request `#43202`_ from garethgreenaway/42642_2016_11_augeas_module_revert_fix + + * ef7e93eb3f Reverting this change due to it breaking other uses. + + * f16b7246e4 Merge pull request `#43103`_ from aogier/43101-genesis-bootstrap + + * db94f3bb1c better formatting + + * e5cc667762 tests: fix a leftover and simplify some parts + + * 13e5997457 lint + + * 216ced69e5 allow comma-separated pkgs lists, quote args, test deb behaviour + + * d8612ae006 fix debootstrap and enhance packages selection/deletion via cmdline + + * 4863771428 Merge pull request `#42663`_ from StreetHawkInc/fix_git_tag_check + + * 2b5af5b59d Remove refs/tags prefix from remote tags + + * 3f2e96e561 Convert set to list for serializer + + * 2728e5d977 Only include new tags in changes + + * 4b1df2f223 Exclude annotated tags from checks + + * 389c037285 Check remote tags before deciding to do a fetch `#42329`_ + +- **PR** `#43201`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-25T22:56:46Z* + + - **ISSUE** `#43198`_: (*corywright*) disk.format_ needs to be aliased to disk.format + | refs: `#43199`_ + - **ISSUE** `#43143`_: (*abulford*) git.detached does not fetch if rev is missing from local + | refs: `#43178`_ + - **ISSUE** `#495`_: (*syphernl*) mysql.* without having MySQL installed/configured gives traceback + | refs: `#43196`_ + - **PR** `#43199`_: (*corywright*) Add `disk.format` alias for `disk.format_` + - **PR** `#43196`_: (*gtmanfred*) Pin request install to version for npm tests + - **PR** `#43179`_: (*terminalmage*) Fix missed deprecation + - **PR** `#43178`_: (*terminalmage*) git.detached: Fix traceback when rev is a SHA and is not present locally + - **PR** `#43173`_: (*Ch3LL*) Add New Release Branch Strategy to Contribution Docs + - **PR** `#43171`_: (*terminalmage*) Add warning about adding new functions to salt/utils/__init__.py + * a563a9422a Merge pull request `#43201`_ from rallytime/merge-2017.7 + * d40eba6b37 Merge branch '2016.11' into '2017.7' + + * 4193e7f0a2 Merge pull request `#43199`_ from corywright/disk-format-alias + + * f00d3a9ddc Add `disk.format` alias for `disk.format_` + + * 5471f9fe0c Merge pull request `#43196`_ from gtmanfred/2016.11 + + * ccd2241777 Pin request install to version + + * ace2715c60 Merge pull request `#43178`_ from terminalmage/issue43143 + + * 2640833400 git.detached: Fix traceback when rev is a SHA and is not present locally + + * 12e9507b9e Merge pull request `#43179`_ from terminalmage/old-deprecation + + * 3adf8ad04b Fix missed deprecation + + * b595440d90 Merge pull request `#43171`_ from terminalmage/salt-utils-warning + + * 7b5943a31a Add warning about adding new functions to salt/utils/__init__.py + + * 4f273cac4f Merge pull request `#43173`_ from Ch3LL/add_branch_docs + + * 1b24244bd3 Add New Release Branch Strategy to Contribution Docs + +- **PR** `#42997`_: (*twangboy*) Fix `unit.test_test_module_names` for Windows + @ *2017-08-25T21:19:11Z* + + * ce04ab4286 Merge pull request `#42997`_ from twangboy/win_fix_test_module_names + * 2722e9521d Use os.path.join to create paths + +- **PR** `#43006`_: (*SuperPommeDeTerre*) Try to fix `#26995`_ + @ *2017-08-25T21:16:07Z* + + - **ISSUE** `#26995`_: (*jbouse*) Issue with artifactory.downloaded and snapshot artifacts + | refs: `#43006`_ `#43006`_ + * c0279e491e Merge pull request `#43006`_ from SuperPommeDeTerre/SuperPommeDeTerre-patch-`#26995`_ + * 30dd6f5d12 Merge remote-tracking branch 'upstream/2017.7' into SuperPommeDeTerre-patch-`#26995`_ + + * f42ae9b8cd Merge branch 'SuperPommeDeTerre-patch-`#26995`_' of https://github.com/SuperPommeDeTerre/salt into SuperPommeDeTerre-patch-`#26995`_ + + * 50ee3d5682 Merge remote-tracking branch 'remotes/origin/2017.7' into SuperPommeDeTerre-patch-`#26995`_ + + * 0b666e100b Fix typo. + + * 1b8729b3e7 Fix for `#26995`_ + + * e314102978 Fix typo. + + * db11e1985b Fix for `#26995`_ + +- **PR** `#43184`_: (*terminalmage*) docker.compare_container: Perform boolean comparison when one side's value is null/None + @ *2017-08-25T18:42:11Z* + + - **ISSUE** `#43162`_: (*MorphBonehunter*) docker_container.running interference with restart_policy + | refs: `#43184`_ + * b6c5314fe9 Merge pull request `#43184`_ from terminalmage/issue43162 + * 081f42ad71 docker.compare_container: Perform boolean comparison when one side's value is null/None + +- **PR** `#43165`_: (*mirceaulinic*) Improve napalm state output in debug mode + @ *2017-08-24T23:05:37Z* + + * 688125bb4f Merge pull request `#43165`_ from cloudflare/fix-napalm-ret + * c10717dc89 Lint and fix + + * 1cd33cbaa9 Simplify the loaded_ret logic + + * 0bbea6b04c Document the new compliance_report arg + + * 3a906109bd Include compliance reports + + * 3634055e34 Improve napalm state output in debug mode + +- **PR** `#43155`_: (*terminalmage*) Resolve image ID during container comparison + @ *2017-08-24T22:09:47Z* + + * a6a327b1e5 Merge pull request `#43155`_ from terminalmage/issue43001 + * 0186835ebf Fix docstring in test + + * a0bb654e46 Fixing lint issues + + * d5b2a0be68 Resolve image ID during container comparison + +- **PR** `#43170`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-24T19:22:26Z* + + - **PR** `#43151`_: (*ushmodin*) state.sls hangs on file.recurse with clean: True on windows + - **PR** `#42969`_: (*ushmodin*) state.sls hangs on file.recurse with clean: True on windows + | refs: `#43151`_ + * c071fd44c8 Merge pull request `#43170`_ from rallytime/merge-2017.7 + * 3daad5a3a2 Merge branch '2016.11' into '2017.7' + + * 669b376abf Merge pull request `#43151`_ from ushmodin/2016.11 + + * c5841e2ade state.sls hangs on file.recurse with clean: True on windows + +- **PR** `#43168`_: (*rallytime*) Back-port `#43041`_ to 2017.7 + @ *2017-08-24T19:07:23Z* + + - **ISSUE** `#43040`_: (*darcoli*) gitFS ext_pillar with branch name __env__ results in empty pillars + | refs: `#43041`_ `#43041`_ + - **PR** `#43041`_: (*darcoli*) Do not try to match pillarenv with __env__ + | refs: `#43168`_ + * 034c325a09 Merge pull request `#43168`_ from rallytime/`bp-43041`_ + * d010b74b87 Do not try to match pillarenv with __env__ + +- **PR** `#43172`_: (*rallytime*) Move new utils/__init__.py funcs to utils.files.py + @ *2017-08-24T19:05:30Z* + + - **PR** `#43056`_: (*damon-atkins*) safe_filename_leaf(file_basename) and safe_filepath(file_path_name) + | refs: `#43172`_ + * d48938e6b4 Merge pull request `#43172`_ from rallytime/move-utils-funcs + * 5385c7901e Move new utils/__init__.py funcs to utils.files.py + +- **PR** `#43061`_: (*pabloh007*) Have docker.save use the image name when valid if not use image id, i… + @ *2017-08-24T16:32:02Z* + + - **ISSUE** `#43043`_: (*pabloh007*) docker.save and docker.load problem + | refs: `#43061`_ `#43061`_ + * e60f586442 Merge pull request `#43061`_ from pabloh007/fix-save-image-name-id + * 0ffc57d1df Have docker.save use the image name when valid if not use image id, issue when loading and image is savid with id issue `#43043`_ + +- **PR** `#43166`_: (*lomeroe*) Backport `#43116`_ to 2017.7 + | refs: `#43226`_ + @ *2017-08-24T15:01:23Z* + + - **ISSUE** `#42279`_: (*dafyddj*) win_lgpo matches multiple policies due to startswith() + | refs: `#43116`_ `#43116`_ `#43166`_ `#43226`_ `#43156`_ + - **PR** `#43156`_: (*lomeroe*) Backport `#43116`_ to 2017.7 + | refs: `#43166`_ + - **PR** `#43116`_: (*lomeroe*) Fix 42279 in develop + | refs: `#43166`_ `#43156`_ + * 9da57543f8 Merge pull request `#43166`_ from lomeroe/`bp-43116`_-2017.7 + * af181b3257 correct fopen calls from salt.utils for 2017.7 + + * f74480f11e lint fix + + * ecd446fd55 track xml namespace to ensure policies w/duplicate IDs or Names do not conflict + + * 9f3047c420 add additional checks for ADM policies that have the same ADMX policy ID (`#42279`_) + +- **PR** `#43056`_: (*damon-atkins*) safe_filename_leaf(file_basename) and safe_filepath(file_path_name) + | refs: `#43172`_ + @ *2017-08-23T17:35:02Z* + + * 44b3caead1 Merge pull request `#43056`_ from damon-atkins/2017.7 + * 08ded1546e more lint + + * 6e9c0957fb fix typo + + * ee41171c9f lint fixes + + * 8c864f02c7 fix missing imports + + * 964cebd954 safe_filename_leaf(file_basename) and safe_filepath(file_path_name) + +- **PR** `#43146`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-23T16:56:10Z* + + - **ISSUE** `#43036`_: (*mcarlton00*) Linux VMs in Bhyve aren't displayed properly in grains + | refs: `#43037`_ + - **PR** `#43100`_: (*vutny*) [DOCS] Add missing `utils` sub-dir listed for `extension_modules` + - **PR** `#43037`_: (*mcarlton00*) Issue `#43036`_ Bhyve virtual grain in Linux VMs + - **PR** `#42986`_: (*renner*) Notify systemd synchronously (via NOTIFY_SOCKET) + * 6ca9131a23 Merge pull request `#43146`_ from rallytime/merge-2017.7 + * bcbe180fbc Merge branch '2016.11' into '2017.7' + + * ae9d2b7985 Merge pull request `#42986`_ from renner/systemd-notify + + * 79c53f3f81 Fallback to systemd_notify_call() in case of socket.error + + * f1765472dd Notify systemd synchronously (via NOTIFY_SOCKET) + + * b420fbe618 Merge pull request `#43037`_ from mcarlton00/fix-bhyve-grains + + * 73315f0cf0 Issue `#43036`_ Bhyve virtual grain in Linux VMs + + * 0a86f2d884 Merge pull request `#43100`_ from vutny/doc-add-missing-utils-ext + + * af743ff6c3 [DOCS] Add missing `utils` sub-dir listed for `extension_modules` + +- **PR** `#43123`_: (*twangboy*) Fix `unit.utils.test_which` for Windows + @ *2017-08-23T16:01:39Z* + + * 03f652159f Merge pull request `#43123`_ from twangboy/win_fix_test_which + * ed97cff5f6 Fix `unit.utils.test_which` for Windows + +- **PR** `#43142`_: (*rallytime*) Back-port `#43068`_ to 2017.7 + @ *2017-08-23T15:56:48Z* + + - **ISSUE** `#42505`_: (*ikogan*) selinux.fcontext_policy_present exception looking for selinux.filetype_id_to_string + | refs: `#43068`_ + - **PR** `#43068`_: (*ixs*) Mark selinux._filetype_id_to_string as public function + | refs: `#43142`_ + * 5a4fc07863 Merge pull request `#43142`_ from rallytime/`bp-43068`_ + * efc1c8c506 Mark selinux._filetype_id_to_string as public function + +- **PR** `#43038`_: (*twangboy*) Fix `unit.utils.test_url` for Windows + @ *2017-08-23T13:35:25Z* + + * 0467a0e3bf Merge pull request `#43038`_ from twangboy/win_unit_utils_test_url + * 7f5ee55f57 Fix `unit.utils.test_url` for Windows + +- **PR** `#43097`_: (*twangboy*) Fix `group.present` for Windows + @ *2017-08-23T13:19:56Z* + + * e9ccaa61d2 Merge pull request `#43097`_ from twangboy/win_fix_group + * 43b0360763 Fix lint + + * 9ffe315d7d Add kwargs + + * 4f4e34c79f Fix group state for Windows + +- **PR** `#43115`_: (*rallytime*) Back-port `#42067`_ to 2017.7 + @ *2017-08-22T20:09:52Z* + + - **PR** `#42067`_: (*vitaliyf*) Removed several uses of name.split('.')[0] in SoftLayer driver. + | refs: `#43115`_ + * 8140855627 Merge pull request `#43115`_ from rallytime/`bp-42067`_ + * 8a6ad0a9cf Fixed typo. + + * 9a5ae2bba1 Removed several uses of name.split('.')[0] in SoftLayer driver. + +- **PR** `#42962`_: (*twangboy*) Fix `unit.test_doc test` for Windows + @ *2017-08-22T18:06:23Z* + + * 1e1a81036c Merge pull request `#42962`_ from twangboy/win_unit_test_doc + * 201ceae4c4 Fix lint, remove debug statement + + * 37029c1a16 Fix unit.test_doc test + +- **PR** `#42995`_: (*twangboy*) Fix malformed requisite for Windows + @ *2017-08-22T16:50:01Z* + + * d347d1cf8f Merge pull request `#42995`_ from twangboy/win_fix_invalid_requisite + * 93390de88b Fix malformed requisite for Windows + +- **PR** `#43108`_: (*rallytime*) Back-port `#42988`_ to 2017.7 + @ *2017-08-22T16:49:27Z* + + - **PR** `#42988`_: (*thusoy*) Fix broken negation in iptables + | refs: `#43108`_ + * 1c7992a832 Merge pull request `#43108`_ from rallytime/`bp-42988`_ + * 1a987cb948 Fix broken negation in iptables + +- **PR** `#43107`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-22T16:11:25Z* + + - **ISSUE** `#42869`_: (*abednarik*) Git Module : Failed to update repository + | refs: `#43064`_ + - **ISSUE** `#42041`_: (*lorengordon*) pkg.list_repo_pkgs fails to find pkgs with spaces around yum repo enabled value + | refs: `#43054`_ + - **ISSUE** `#15171`_: (*JensRantil*) Maximum recursion limit hit related to requisites + | refs: `#42985`_ + - **PR** `#43092`_: (*blarghmatey*) Fixed issue with silently passing all tests in Testinfra module + - **PR** `#43064`_: (*terminalmage*) Fix race condition in git.latest + - **PR** `#43060`_: (*twangboy*) Osx update pkg scripts + - **PR** `#43054`_: (*lorengordon*) Uses ConfigParser to read yum config files + - **PR** `#42985`_: (*DmitryKuzmenko*) Properly handle `prereq` having lost requisites. + - **PR** `#42045`_: (*arount*) Fix: salt.modules.yumpkg: ConfigParser to read ini like files. + | refs: `#43054`_ + * c6993f4a84 Merge pull request `#43107`_ from rallytime/merge-2017.7 + * 328dd6aa23 Merge branch '2016.11' into '2017.7' + + * e2bf2f448e Merge pull request `#42985`_ from DSRCorporation/bugs/15171_recursion_limit + + * 651b1bab09 Properly handle `prereq` having lost requisites. + + * e51333306c Merge pull request `#43092`_ from mitodl/2016.11 + + * d4b113acdf Fixed issue with silently passing all tests in Testinfra module + + * 77a443ce8e Merge pull request `#43060`_ from twangboy/osx_update_pkg_scripts + + * ef8a14cdf9 Remove /opt/salt instead of /opt/salt/bin + + * 2dd62aa1da Add more information to the description + + * f44f5b70dc Only stop services if they are running + + * 3b62bf953c Remove salt from the path + + * ebdca3a0f5 Update pkg-scripts + + * 1b1b6da803 Merge pull request `#43064`_ from terminalmage/issue42869 + + * 093c0c2f77 Fix race condition in git.latest + + * 96e8e836d1 Merge pull request `#43054`_ from lorengordon/fix/yumpkg/config-parser + + * 3b2cb81a72 fix typo in salt.modules.yumpkg + + * 38add0e4a2 break if leading comments are all fetched + + * d7f65dc7a7 fix configparser import & log if error was raised + + * ca1b1bb633 use configparser to parse yum repo file + +- **PR** `#42996`_: (*twangboy*) Fix `unit.test_stateconf` for Windows + @ *2017-08-21T22:43:58Z* + + * f9b4976c02 Merge pull request `#42996`_ from twangboy/win_fix_test_stateconf + * 92dc3c0ece Use os.sep for path + +- **PR** `#43024`_: (*twangboy*) Fix `unit.utils.test_find` for Windows + @ *2017-08-21T22:38:10Z* + + * 19fc644c9b Merge pull request `#43024`_ from twangboy/win_unit_utils_test_find + * fbe54c9a33 Remove unused import six (lint) + + * b04d1a2f18 Fix `unit.utils.test_find` for Windows + +- **PR** `#43088`_: (*gtmanfred*) allow docker util to be reloaded with reload_modules + @ *2017-08-21T22:14:37Z* + + * 1a531169fc Merge pull request `#43088`_ from gtmanfred/2017.7 + * 373a9a0be4 allow docker util to be reloaded with reload_modules + +- **PR** `#43091`_: (*blarghmatey*) Fixed issue with silently passing all tests in Testinfra module + @ *2017-08-21T22:06:22Z* + + * 83e528f0b3 Merge pull request `#43091`_ from mitodl/2017.7 + * b502560e61 Fixed issue with silently passing all tests in Testinfra module + +- **PR** `#41994`_: (*twangboy*) Fix `unit.modules.test_cmdmod` on Windows + @ *2017-08-21T21:53:01Z* + + * 5482524270 Merge pull request `#41994`_ from twangboy/win_unit_test_cmdmod + * a5f7288ad9 Skip test that uses pwd, not available on Windows + +- **PR** `#42933`_: (*garethgreenaway*) Fixes to osquery module + @ *2017-08-21T20:48:31Z* + + - **ISSUE** `#42873`_: (*TheVakman*) osquery Data Empty Upon Return / Reporting Not Installed + | refs: `#42933`_ + * b33c4abc15 Merge pull request `#42933`_ from garethgreenaway/42873_2017_7_osquery_fix + * 8915e62bd9 Removing an import that is not needed. + + * 74bc377eb4 Updating the other function that uses cmd.run_all + + * e6a4619ec1 Better approach without using python_shell=True. + + * 5ac41f496d When running osquery commands through cmd.run we should pass python_shell=True to ensure everything is formatted right. `#42873`_ + +- **PR** `#43093`_: (*gtmanfred*) Fix ec2 list_nodes_full to work on 2017.7 + @ *2017-08-21T20:21:21Z* + + * 53c2115769 Merge pull request `#43093`_ from gtmanfred/ec2 + * c7cffb5a04 This block isn't necessary + + * b7283bcc6f _vm_provider_driver isn't needed anymore + +- **PR** `#43087`_: (*rallytime*) Back-port `#42174`_ to 2017.7 + @ *2017-08-21T18:40:18Z* + + - **ISSUE** `#43085`_: (*brejoc*) Patch for Kubernetes module missing from 2017.7 and 2017.7.1 + | refs: `#43087`_ + - **PR** `#42174`_: (*mcalmer*) kubernetes: provide client certificate authentication + | refs: `#43087`_ + * 32f9ade4db Merge pull request `#43087`_ from rallytime/`bp-42174`_ + * cf6563645b add support for certificate authentication to kubernetes module + +- **PR** `#43029`_: (*terminalmage*) Normalize the salt caching API + @ *2017-08-21T16:54:58Z* + + * 882fcd846f Merge pull request `#43029`_ from terminalmage/fix-func-alias + * f8f74a310c Update localfs cache tests to reflect changes to func naming + + * c4ae79b229 Rename other refs to cache.ls with cache.list + + * ee59d127e8 Normalize the salt caching API + +- **PR** `#43039`_: (*gtmanfred*) catch ImportError for kubernetes.client import + @ *2017-08-21T14:32:38Z* + + - **ISSUE** `#42843`_: (*brejoc*) Kubernetes module won't work with Kubernetes Python client > 1.0.2 + | refs: `#42845`_ + - **PR** `#42845`_: (*brejoc*) API changes for Kubernetes version 2.0.0 + | refs: `#43039`_ + * dbee735f6e Merge pull request `#43039`_ from gtmanfred/kube + * 7e269cb368 catch ImportError for kubernetes.client import + +- **PR** `#43058`_: (*rallytime*) Update release version number for jenkins.run function + @ *2017-08-21T14:13:34Z* + + * c56a8499b3 Merge pull request `#43058`_ from rallytime/fix-release-num + * d7eef70df0 Update release version number for jenkins.run function + +- **PR** `#43051`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-18T17:05:57Z* + + - **ISSUE** `#42992`_: (*pabloh007*) docker.save flag push does is ignored + - **ISSUE** `#42627`_: (*taigrrr8*) salt-cp no longer works. Was working a few months back. + | refs: `#42890`_ + - **ISSUE** `#40490`_: (*alxwr*) saltstack x509 incompatible to m2crypto 0.26.0 + | refs: `#42760`_ + - **PR** `#43048`_: (*rallytime*) Back-port `#43031`_ to 2016.11 + - **PR** `#43033`_: (*rallytime*) Back-port `#42760`_ to 2016.11 + - **PR** `#43032`_: (*rallytime*) Back-port `#42547`_ to 2016.11 + - **PR** `#43031`_: (*gtmanfred*) use a ruby gem that doesn't have dependencies + | refs: `#43048`_ + - **PR** `#43027`_: (*pabloh007*) Fixes ignore push flag for docker.push module issue `#42992`_ + - **PR** `#43026`_: (*rallytime*) Back-port `#43020`_ to 2016.11 + - **PR** `#43023`_: (*terminalmage*) Fixes/improvements to Jenkins state/module + - **PR** `#43021`_: (*terminalmage*) Use socket.AF_INET6 to get the correct value instead of doing an OS check + - **PR** `#43020`_: (*gtmanfred*) test with gem that appears to be abandoned + | refs: `#43026`_ + - **PR** `#43019`_: (*rallytime*) Update bootstrap script to latest stable: v2017.08.17 + - **PR** `#43014`_: (*Ch3LL*) Change AF_INET6 family for mac in test_host_to_ips + | refs: `#43021`_ + - **PR** `#43009`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + - **PR** `#42954`_: (*Ch3LL*) [2016.3] Bump latest and previous versions + - **PR** `#42949`_: (*Ch3LL*) Add Security Notice to 2016.3.7 Release Notes + - **PR** `#42942`_: (*Ch3LL*) [2016.3] Add clean_id function to salt.utils.verify.py + - **PR** `#42890`_: (*DmitryKuzmenko*) Make chunked mode in salt-cp optional + - **PR** `#42760`_: (*AFriemann*) Catch TypeError thrown by m2crypto when parsing missing subjects in c… + | refs: `#43033`_ + - **PR** `#42547`_: (*blarghmatey*) Updated testinfra modules to work with more recent versions + | refs: `#43032`_ + * 7b0c94768a Merge pull request `#43051`_ from rallytime/merge-2017.7 + * 153a463b86 Lint: Add missing blank line + + * 84829a6f8c Merge branch '2016.11' into '2017.7' + + * 43aa46f512 Merge pull request `#43048`_ from rallytime/`bp-43031`_ + + * 35e45049e2 use a ruby gem that doesn't have dependencies + + * ad89ff3104 Merge pull request `#43023`_ from terminalmage/fix-jenkins-xml-caching + + * 33fd8ff939 Update jenkins.py + + * fc306fc8c3 Add missing colon in `if` statement + + * 822eabcc81 Catch exceptions raised when making changes to jenkins + + * 91b583b493 Improve and correct execption raising + + * f096917a0e Raise an exception if we fail to cache the config xml + + * 2957467ed7 Merge pull request `#43026`_ from rallytime/`bp-43020`_ + + * 0eb15a1f67 test with gem that appears to be abandoned + + * 4150b094fe Merge pull request `#43033`_ from rallytime/`bp-42760`_ + + * 3e3f7f5d8e Catch TypeError thrown by m2crypto when parsing missing subjects in certificate files. + + * b124d3667e Merge pull request `#43032`_ from rallytime/`bp-42547`_ + + * ea4d7f4176 Updated testinfra modules to work with more recent versions + + * a88386ad44 Merge pull request `#43027`_ from pabloh007/fix-docker-save-push-2016-11 + + * d0fd949f85 Fixes ignore push flag for docker.push module issue `#42992`_ + + * 51d16840bb Merge pull request `#42890`_ from DSRCorporation/bugs/42627_salt-cp + + * cfddbf1c75 Apply code review: update the doc + + * afedd3b654 Typos and version fixes in the doc. + + * 9fedf6012e Fixed 'test_valid_docs' test. + + * 999388680c Make chunked mode in salt-cp optional (disabled by default). + + * b3c253cdfa Merge pull request `#43009`_ from rallytime/merge-2016.11 + + * 566ba4fe76 Merge branch '2016.3' into '2016.11' + + * 13b8637d53 Merge pull request `#42942`_ from Ch3LL/2016.3.6_follow_up + + * f281e1795f move additional minion config options to 2016.3.8 release notes + + * 168604ba6b remove merge conflict + + * 8a07d95212 update release notes with cve number + + * 149633fdca Add release notes for 2016.3.7 release + + * 7a4cddcd95 Add clean_id function to salt.utils.verify.py + + * bbb1b29ccb Merge pull request `#42954`_ from Ch3LL/latest_2016.3 + + * b551e66744 [2016.3] Bump latest and previous versions + + * 5d5edc54b7 Merge pull request `#42949`_ from Ch3LL/2016.3.7_docs + + * d75d3741f8 Add Security Notice to 2016.3.7 Release Notes + + * 37c63e7cf2 Merge pull request `#43021`_ from terminalmage/fix-network-test + + * 4089b7b1bc Use socket.AF_INET6 to get the correct value instead of doing an OS check + + * 8f6423247c Merge pull request `#43019`_ from rallytime/bootstrap_2017.08.17 + + * 2f762b3a17 Update bootstrap script to latest stable: v2017.08.17 + + * ff1caeee68 Merge pull request `#43014`_ from Ch3LL/fix_network_mac + + * b8eee4401e Change AF_INET6 family for mac in test_host_to_ips + +- **PR** `#43035`_: (*rallytime*) [2017.7] Merge forward from 2017.7.1 to 2017.7 + @ *2017-08-18T12:58:17Z* + + - **PR** `#42948`_: (*Ch3LL*) [2017.7.1] Add clean_id function to salt.utils.verify.py + | refs: `#43035`_ + - **PR** `#42945`_: (*Ch3LL*) [2017.7] Add clean_id function to salt.utils.verify.py + | refs: `#43035`_ + * d15b0ca937 Merge pull request `#43035`_ from rallytime/merge-2017.7 + * 756128a896 Merge branch '2017.7.1' into '2017.7' + + * ab1b099730 Merge pull request `#42948`_ from Ch3LL/2017.7.0_follow_up + +- **PR** `#43034`_: (*rallytime*) Back-port `#43002`_ to 2017.7 + @ *2017-08-17T23:18:16Z* + + - **ISSUE** `#42989`_: (*blbradley*) GitFS GitPython performance regression in 2017.7.1 + | refs: `#43002`_ `#43002`_ + - **PR** `#43002`_: (*the-glu*) Try to fix `#42989`_ + | refs: `#43034`_ + * bcbb973a71 Merge pull request `#43034`_ from rallytime/`bp-43002`_ + * 350c0767dc Try to fix `#42989`_ by doing sslVerify and refspecs for origin remote only if there is no remotes + +- **PR** `#42958`_: (*gtmanfred*) runit module should also be loaded as runit + @ *2017-08-17T22:30:23Z* + + - **ISSUE** `#42375`_: (*dragonpaw*) salt.modules.*.__virtualname__ doens't work as documented. + | refs: `#42523`_ `#42958`_ + * 9182f55bbb Merge pull request `#42958`_ from gtmanfred/2017.7 + * fd6874668b runit module should also be loaded as runit + +- **PR** `#43031`_: (*gtmanfred*) use a ruby gem that doesn't have dependencies + | refs: `#43048`_ + @ *2017-08-17T22:26:25Z* + + * 5985cc4e8e Merge pull request `#43031`_ from gtmanfred/test_gem + * ba80a7d4b5 use a ruby gem that doesn't have dependencies + +- **PR** `#43030`_: (*rallytime*) Small cleanup to dockermod.save + @ *2017-08-17T22:26:00Z* + + * 246176b1a6 Merge pull request `#43030`_ from rallytime/dockermod-minor-change + * d6a5e85632 Small cleanup to dockermod.save + +- **PR** `#42993`_: (*pabloh007*) Fixes ignored push flag for docker.push module issue `#42992`_ + @ *2017-08-17T18:50:37Z* + + - **ISSUE** `#42992`_: (*pabloh007*) docker.save flag push does is ignored + * 160001120b Merge pull request `#42993`_ from pabloh007/fix-docker-save-push + * fe7554cfeb Fixes ignored push flag for docker.push module issue `#42992`_ + +- **PR** `#42967`_: (*terminalmage*) Fix bug in on_header callback when no Content-Type is found in headers + @ *2017-08-17T18:48:52Z* + + - **ISSUE** `#42941`_: (*danlsgiga*) pkg.installed fails on installing from HTTPS rpm source + | refs: `#42967`_ + * 9009a971b1 Merge pull request `#42967`_ from terminalmage/issue42941 + * b838460816 Fix bug in on_header callback when no Content-Type is found in headers + +- **PR** `#43016`_: (*gtmanfred*) service should return false on exception + @ *2017-08-17T18:08:05Z* + + - **ISSUE** `#43008`_: (*fillarios*) states.service.running always succeeds when watched state has changes + | refs: `#43016`_ + * 58f070d7a7 Merge pull request `#43016`_ from gtmanfred/service + * 21c264fe55 service should return false on exception + +- **PR** `#43020`_: (*gtmanfred*) test with gem that appears to be abandoned + | refs: `#43026`_ + @ *2017-08-17T16:40:41Z* + + * 973d288eca Merge pull request `#43020`_ from gtmanfred/test_gem + * 0a1f40a664 test with gem that appears to be abandoned + +- **PR** `#42999`_: (*garethgreenaway*) Fixes to slack engine + @ *2017-08-17T15:46:24Z* + + * 9cd0607fd4 Merge pull request `#42999`_ from garethgreenaway/slack_engine_allow_editing_messages + * 0ece2a8f0c Fixing a bug that prevented editing Slack messages and having the commands resent to the Slack engine. + +- **PR** `#43010`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-17T15:10:29Z* + + - **ISSUE** `#42803`_: (*gmcwhistler*) master_type: str, not working as expected, parent salt-minion process dies. + | refs: `#42848`_ + - **ISSUE** `#42753`_: (*grichmond-salt*) SaltReqTimeout Error on Some Minions when One Master in a Multi-Master Configuration is Unavailable + | refs: `#42848`_ + - **ISSUE** `#42644`_: (*stamak*) nova salt-cloud -P Private IPs returned, but not public. Checking for misidentified IPs + | refs: `#42940`_ + - **ISSUE** `#38839`_: (*DaveOHenry*) Invoking runner.cloud.action via reactor sls fails + | refs: `#42291`_ + - **PR** `#42968`_: (*vutny*) [DOCS] Fix link to Salt Cloud Feature Matrix + - **PR** `#42959`_: (*rallytime*) Back-port `#42883`_ to 2016.11 + - **PR** `#42952`_: (*Ch3LL*) [2016.11] Bump latest and previous versions + - **PR** `#42950`_: (*Ch3LL*) Add Security Notice to 2016.11.7 Release Notes + - **PR** `#42944`_: (*Ch3LL*) [2016.11] Add clean_id function to salt.utils.verify.py + - **PR** `#42940`_: (*gtmanfred*) create new ip address before checking list of allocated ips + - **PR** `#42919`_: (*rallytime*) Back-port `#42871`_ to 2016.11 + - **PR** `#42918`_: (*rallytime*) Back-port `#42848`_ to 2016.11 + - **PR** `#42883`_: (*rallytime*) Fix failing boto tests + | refs: `#42959`_ + - **PR** `#42871`_: (*amalleo25*) Update joyent.rst + | refs: `#42919`_ + - **PR** `#42861`_: (*twangboy*) Fix pkg.install salt-minion using salt-call + - **PR** `#42848`_: (*DmitryKuzmenko*) Execute fire_master asynchronously in the main minion thread. + | refs: `#42918`_ + - **PR** `#42836`_: (*aneeshusa*) Backport salt.utils.versions from develop to 2016.11 + - **PR** `#42835`_: (*aneeshusa*) Fix typo in utils/versions.py module + | refs: `#42836`_ + - **PR** `#42798`_: (*s-sebastian*) Update return data before calling returners + - **PR** `#42291`_: (*vutny*) Fix `#38839`_: remove `state` from Reactor runner kwags + * 31627a9163 Merge pull request `#43010`_ from rallytime/merge-2017.7 + * 8a0f948e4a Merge branch '2016.11' into '2017.7' + + * 1ee9499d28 Merge pull request `#42968`_ from vutny/doc-salt-cloud-ref + + * 44ed53b1df [DOCS] Fix link to Salt Cloud Feature Matrix + + * 923f9741fe Merge pull request `#42291`_ from vutny/`fix-38839`_ + + * 5f8f98a01f Fix `#38839`_: remove `state` from Reactor runner kwags + + * c20bc7d515 Merge pull request `#42940`_ from gtmanfred/2016.11 + + * 253e216a8d fix IP address spelling + + * bd63074e7a create new ip address before checking list of allocated ips + + * d6496eca72 Merge pull request `#42959`_ from rallytime/`bp-42883`_ + + * c6b9ca4b9e Lint fix: add missing space + + * 5597b1a30e Skip 2 failing tests in Python 3 due to upstream bugs + + * a0b19bdc27 Update account id value in boto_secgroup module unit test + + * 60b406e088 @mock_elb needs to be changed to @mock_elb_deprecated as well + + * 6ae1111295 Replace @mock_ec2 calls with @mock_ec2_deprecated calls + + * 6366e05d0d Merge pull request `#42944`_ from Ch3LL/2016.11.6_follow_up + + * 7e0a20afca Add release notes for 2016.11.7 release + + * 63823f8c3e Add clean_id function to salt.utils.verify.py + + * 49d339c976 Merge pull request `#42952`_ from Ch3LL/latest_2016.11 + + * 74e7055d54 [2016.11] Bump latest and previous versions + + * b0d2e05a79 Merge pull request `#42950`_ from Ch3LL/2016.11.7_docs + + * a6f902db40 Add Security Notice to 2016.11.77 Release Notes + + * c0ff69f88c Merge pull request `#42836`_ from lyft/backport-utils.versions-to-2016.11 + + * 86ce7004a2 Backport salt.utils.versions from develop to 2016.11 + + * 64a79dd5ac Merge pull request `#42919`_ from rallytime/`bp-42871`_ + + * 4e46c968e6 Update joyent.rst + + * bea8ec1098 Merge pull request `#42918`_ from rallytime/`bp-42848`_ + + * cdb48126f7 Make lint happier. + + * 62eca9b00b Execute fire_master asynchronously in the main minion thread. + + * 52bce329cb Merge pull request `#42861`_ from twangboy/win_pkg_install_salt + + * 0d3789f0c6 Fix pkg.install salt-minion using salt-call + + * b9f4f87aa5 Merge pull request `#42798`_ from s-sebastian/2016.11 + + * 1cc86592ed Update return data before calling returners + +- **PR** `#42884`_: (*Giandom*) Convert to dict type the pillar string value passed from slack + @ *2017-08-16T22:30:43Z* + + - **ISSUE** `#42842`_: (*Giandom*) retreive kwargs passed with slack engine + | refs: `#42884`_ + * 82be9dceb6 Merge pull request `#42884`_ from Giandom/2017.7.1-fix-slack-engine-pillar-args + * 80fd733c99 Update slack.py + +- **PR** `#42963`_: (*twangboy*) Fix `unit.test_fileclient` for Windows + @ *2017-08-16T14:18:18Z* + + * 42bd553b98 Merge pull request `#42963`_ from twangboy/win_unit_test_fileclient + * e9febe4893 Fix unit.test_fileclient + +- **PR** `#42964`_: (*twangboy*) Fix `salt.utils.recursive_copy` for Windows + @ *2017-08-16T14:17:27Z* + + * 7dddeeea8d Merge pull request `#42964`_ from twangboy/win_fix_recursive_copy + * 121cd4ef81 Fix `salt.utils.recursive_copy` for Windows + +- **PR** `#42946`_: (*mirceaulinic*) extension_modules should default to $CACHE_DIR/proxy/extmods + @ *2017-08-15T21:26:36Z* + + - **ISSUE** `#42943`_: (*mirceaulinic*) `extension_modules` defaulting to `/var/cache/minion` although running under proxy minion + | refs: `#42946`_ + * 6da4d1d95e Merge pull request `#42946`_ from cloudflare/px_extmods_42943 + * 73f9135340 extension_modules should default to /proxy/extmods + +- **PR** `#42945`_: (*Ch3LL*) [2017.7] Add clean_id function to salt.utils.verify.py + | refs: `#43035`_ + @ *2017-08-15T18:04:20Z* + + * 95645d49f9 Merge pull request `#42945`_ from Ch3LL/2017.7.0_follow_up + * dcd92042e3 remove extra doc + + * 693a504ef0 update release notes with cve number + +- **PR** `#42812`_: (*terminalmage*) Update custom YAML loader tests to properly test unicode literals + @ *2017-08-15T17:50:22Z* + + - **ISSUE** `#42427`_: (*grichmond-salt*) Issue Passing Variables created from load_json as Inline Pillar Between States + | refs: `#42435`_ + - **PR** `#42435`_: (*terminalmage*) Modify our custom YAML loader to treat unicode literals as unicode strings + | refs: `#42812`_ + * 47ff9d5627 Merge pull request `#42812`_ from terminalmage/yaml-loader-tests + * 9d8486a894 Add test for custom YAML loader with unicode literal strings + + * a0118bcece Remove bytestrings and use textwrap.dedent for readability + +- **PR** `#42953`_: (*Ch3LL*) [2017.7] Bump latest and previous versions + @ *2017-08-15T17:23:28Z* + + * 5d0c2198ac Merge pull request `#42953`_ from Ch3LL/latest_2017.7 + * cbecf65823 [2017.7] Bump latest and previous versions + +- **PR** `#42951`_: (*Ch3LL*) Add Security Notice to 2017.7.1 Release Notes + @ *2017-08-15T16:49:56Z* + + * 730e71db17 Merge pull request `#42951`_ from Ch3LL/2017.7.1_docs + * 1d8f827c58 Add Security Notice to 2017.7.1 Release Notes + +- **PR** `#42868`_: (*carsonoid*) Stub out required functions in redis_cache + @ *2017-08-15T14:33:54Z* + + * c1c8cb9bfa Merge pull request `#42868`_ from carsonoid/redisjobcachefix + * 885bee2a7d Stub out required functions for redis cache + +- **PR** `#42810`_: (*amendlik*) Ignore error values when listing Windows SNMP community strings + @ *2017-08-15T03:55:15Z* + + * e192d6e0af Merge pull request `#42810`_ from amendlik/win-snmp-community + * dc20e4651b Ignore error values when listing Windows SNMP community strings + +- **PR** `#42920`_: (*cachedout*) pid_race + @ *2017-08-15T03:49:10Z* + + * a1817f1de3 Merge pull request `#42920`_ from cachedout/pid_race + * 5e930b8cbd If we catch the pid file in a transistory state, return None + +- **PR** `#42925`_: (*terminalmage*) Add debug logging to troubleshoot test failures + @ *2017-08-15T03:47:51Z* + + * 11a33fe692 Merge pull request `#42925`_ from terminalmage/f26-debug-logging + * 8165f46165 Add debug logging to troubleshoot test failures + +- **PR** `#42913`_: (*twangboy*) Change service shutdown timeouts for salt-minion service (Windows) + @ *2017-08-14T20:55:24Z* + + * a537197030 Merge pull request `#42913`_ from twangboy/win_change_timeout + * ffb23fbe47 Remove the line that wipes out the cache + + * a3becf8342 Change service shutdown timeouts + +- **PR** `#42800`_: (*skizunov*) Fix exception when master_type=disable + @ *2017-08-14T20:53:38Z* + + * ca0555f616 Merge pull request `#42800`_ from skizunov/develop6 + * fa5822009f Fix exception when master_type=disable + +- **PR** `#42679`_: (*mirceaulinic*) Add multiprocessing option for NAPALM proxy + @ *2017-08-14T20:45:06Z* + + * 3af264b664 Merge pull request `#42679`_ from cloudflare/napalm-multiprocessing + * 9c4566db0c multiprocessing option tagged for 2017.7.2 + + * 37bca1b902 Add multiprocessing option for NAPALM proxy + + * a2565ba8e5 Add new napalm option: multiprocessing + +- **PR** `#42657`_: (*nhavens*) back-port `#42612`_ to 2017.7 + @ *2017-08-14T19:42:26Z* + + - **ISSUE** `#42611`_: (*nhavens*) selinux.boolean state does not return changes + | refs: `#42612`_ + - **PR** `#42612`_: (*nhavens*) fix for issue `#42611`_ + | refs: `#42657`_ + * 4fcdab3ae9 Merge pull request `#42657`_ from nhavens/2017.7 + * d73c4b55b7 back-port `#42612`_ to 2017.7 + +- **PR** `#42709`_: (*whiteinge*) Add token_expire_user_override link to auth runner docstring + @ *2017-08-14T19:03:06Z* + + * d2b6ce327a Merge pull request `#42709`_ from whiteinge/doc-token_expire_user_override + * c7ea631558 Add more docs on the token_expire param + + * 4a9f6ba44f Add token_expire_user_override link to auth runner docstring + +- **PR** `#42848`_: (*DmitryKuzmenko*) Execute fire_master asynchronously in the main minion thread. + | refs: `#42918`_ + @ *2017-08-14T18:28:38Z* + + - **ISSUE** `#42803`_: (*gmcwhistler*) master_type: str, not working as expected, parent salt-minion process dies. + | refs: `#42848`_ + - **ISSUE** `#42753`_: (*grichmond-salt*) SaltReqTimeout Error on Some Minions when One Master in a Multi-Master Configuration is Unavailable + | refs: `#42848`_ + * c6a7bf02e9 Merge pull request `#42848`_ from DSRCorporation/bugs/42753_mmaster_timeout + * 7f5412c19e Make lint happier. + + * ff66b7aaf0 Execute fire_master asynchronously in the main minion thread. + +- **PR** `#42911`_: (*gtmanfred*) cloud driver isn't a provider + @ *2017-08-14T17:47:16Z* + + * 6a3279ea50 Merge pull request `#42911`_ from gtmanfred/2017.7 + * 99046b441f cloud driver isn't a provider + +- **PR** `#42860`_: (*skizunov*) hash_and_stat_file should return a 2-tuple + @ *2017-08-14T15:44:54Z* + + * 4456f7383d Merge pull request `#42860`_ from skizunov/develop7 + * 5f85a03636 hash_and_stat_file should return a 2-tuple + +- **PR** `#42889`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-14T14:16:20Z* + + - **ISSUE** `#41976`_: (*abulford*) dockerng network states do not respect test=True + | refs: `#41977`_ `#41977`_ + - **ISSUE** `#41770`_: (*Ch3LL*) NPM v5 incompatible with salt.modules.cache_list + | refs: `#42856`_ + - **ISSUE** `#475`_: (*thatch45*) Change yaml to use C bindings + | refs: `#42856`_ + - **PR** `#42886`_: (*sarcasticadmin*) Adding missing output flags to salt cli docs + - **PR** `#42882`_: (*gtmanfred*) make sure cmd is not run when npm isn't installed + - **PR** `#42877`_: (*terminalmage*) Add virtual func for cron state module + - **PR** `#42864`_: (*whiteinge*) Make syndic_log_file respect root_dir setting + - **PR** `#42859`_: (*terminalmage*) Add note about git CLI requirement for GitPython to GitFS tutorial + - **PR** `#42856`_: (*gtmanfred*) skip cache_clean test if npm version is >= 5.0.0 + - **PR** `#42788`_: (*amendlik*) Remove waits and retries from Saltify deployment + - **PR** `#41977`_: (*abulford*) Fix dockerng.network_* ignoring of tests=True + * c6ca7d639f Merge pull request `#42889`_ from rallytime/merge-2017.7 + * fb7117f2ac Use salt.utils.versions.LooseVersion instead of distutils + + * 29ff19c587 Merge branch '2016.11' into '2017.7' + + * c15d0034fe Merge pull request `#41977`_ from redmatter/fix-dockerng-network-ignores-test + + * 1cc2aa503a Fix dockerng.network_* ignoring of tests=True + + * 3b9c3c5671 Merge pull request `#42886`_ from sarcasticadmin/adding_docs_salt_outputs + + * 744bf954ff Adding missing output flags to salt cli + + * e5b98c8a88 Merge pull request `#42882`_ from gtmanfred/2016.11 + + * da3402a53d make sure cmd is not run when npm isn't installed + + * 5962c9588b Merge pull request `#42788`_ from amendlik/saltify-timeout + + * 928b523797 Remove waits and retries from Saltify deployment + + * 227ecddd13 Merge pull request `#42877`_ from terminalmage/add-cron-state-virtual + + * f1de196740 Add virtual func for cron state module + + * ab9f6cef33 Merge pull request `#42859`_ from terminalmage/gitpython-git-cli-note + + * 35e05c9515 Add note about git CLI requirement for GitPython to GitFS tutorial + + * 682b4a8d14 Merge pull request `#42856`_ from gtmanfred/2016.11 + + * b458b89fb8 skip cache_clean test if npm version is >= 5.0.0 + + * 01ea854029 Merge pull request `#42864`_ from whiteinge/syndic-log-root_dir + + * 4b1f55da9c Make syndic_log_file respect root_dir setting + +- **PR** `#42898`_: (*mirceaulinic*) Minor eos doc correction + @ *2017-08-14T13:42:21Z* + + * 4b6fe2ee59 Merge pull request `#42898`_ from mirceaulinic/patch-11 + * 93be79a135 Index eos under the installation instructions list + + * f903e7bc39 Minor eos doc correction + +- **PR** `#42883`_: (*rallytime*) Fix failing boto tests + | refs: `#42959`_ + @ *2017-08-11T20:29:12Z* + + * 1764878754 Merge pull request `#42883`_ from rallytime/fix-boto-tests + * 6a7bf99848 Lint fix: add missing space + + * 43643227c6 Skip 2 failing tests in Python 3 due to upstream bugs + + * 7f46603e9c Update account id value in boto_secgroup module unit test + + * 7c1d493fdd @mock_elb needs to be changed to @mock_elb_deprecated as well + + * 3055e17ed5 Replace @mock_ec2 calls with @mock_ec2_deprecated calls + +- **PR** `#42885`_: (*terminalmage*) Move weird tearDown test to an actual tearDown + @ *2017-08-11T19:14:42Z* + + * b21778efac Merge pull request `#42885`_ from terminalmage/fix-f26-tests + * 462d653082 Move weird tearDown test to an actual tearDown + +- **PR** `#42887`_: (*rallytime*) Remove extraneous "deprecated" notation + @ *2017-08-11T18:34:25Z* + + - **ISSUE** `#42870`_: (*boltronics*) webutil.useradd marked as deprecated:: 2016.3.0 by mistake? + | refs: `#42887`_ + * 9868ab6f3b Merge pull request `#42887`_ from rallytime/`fix-42870`_ + * 71e7581a2d Remove extraneous "deprecated" notation + +- **PR** `#42881`_: (*gtmanfred*) fix vmware for python 3.4.2 in salt.utils.vmware + @ *2017-08-11T17:52:29Z* + + * da71f2a11b Merge pull request `#42881`_ from gtmanfred/vmware + * 05ecc6ac8d fix vmware for python 3.4.2 in salt.utils.vmware + +- **PR** `#42845`_: (*brejoc*) API changes for Kubernetes version 2.0.0 + | refs: `#43039`_ + @ *2017-08-11T14:04:30Z* + + - **ISSUE** `#42843`_: (*brejoc*) Kubernetes module won't work with Kubernetes Python client > 1.0.2 + | refs: `#42845`_ + * c7750d5717 Merge pull request `#42845`_ from brejoc/updates-for-kubernetes-2.0.0 + * 81674aa88a Version info in :optdepends: not needed anymore + + * 71995505bc Not depending on specific K8s version anymore + + * d8f7d7a7c0 API changes for Kubernetes version 2.0.0 + +- **PR** `#42678`_: (*frankiexyz*) Add eos.rst in the installation guide + @ *2017-08-11T13:58:37Z* + + * 459fdedc67 Merge pull request `#42678`_ from frankiexyz/2017.7 + * 1598571f52 Add eos.rst in the installation guide + +- **PR** `#42778`_: (*gtmanfred*) make sure to use the correct out_file + @ *2017-08-11T13:44:48Z* + + - **ISSUE** `#42646`_: (*gmacon*) SPM fails to install multiple packages + | refs: `#42778`_ + * 4ce96eb1a1 Merge pull request `#42778`_ from gtmanfred/spm + * 7ef691e8da make sure to use the correct out_file + +- **PR** `#42857`_: (*gtmanfred*) use older name if _create_unverified_context is unvailable + @ *2017-08-11T13:37:59Z* + + - **ISSUE** `#480`_: (*zyluo*) PEP8 types clean-up + | refs: `#42857`_ + * 3d05d89e09 Merge pull request `#42857`_ from gtmanfred/vmware + * c1f673eca4 use older name if _create_unverified_context is unvailable + +- **PR** `#42866`_: (*twangboy*) Change to GitPython version 2.1.1 + @ *2017-08-11T13:23:52Z* + + * 7e8cfff21c Merge pull request `#42866`_ from twangboy/osx_downgrade_gitpython + * 28053a84a6 Change GitPython version to 2.1.1 + +- **PR** `#42855`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-10T21:40:39Z* + + - **ISSUE** `#42747`_: (*whiteinge*) Outputters mutate data which can be a problem for Runners and perhaps other things + | refs: `#42748`_ + - **ISSUE** `#42731`_: (*infoveinx*) http.query template_data render exception + | refs: `#42804`_ + - **ISSUE** `#42690`_: (*ChristianBeer*) git.latest state with remote set fails on first try + | refs: `#42694`_ + - **ISSUE** `#42683`_: (*rgcosma*) Gluster module broken in 2017.7 + | refs: `#42806`_ + - **ISSUE** `#42600`_: (*twangboy*) Unable to set 'Not Configured' using win_lgpo execution module + | refs: `#42744`_ `#42794`_ `#42795`_ + - **PR** `#42851`_: (*terminalmage*) Backport `#42651`_ to 2016.11 + - **PR** `#42838`_: (*twangboy*) Document requirements for win_pki + - **PR** `#42829`_: (*twangboy*) Fix passing version in pkgs as shown in docs + - **PR** `#42826`_: (*terminalmage*) Fix misspelling of "versions" + - **PR** `#42806`_: (*rallytime*) Update doc references in glusterfs.volume_present + - **PR** `#42805`_: (*rallytime*) Back-port `#42552`_ to 2016.11 + - **PR** `#42804`_: (*rallytime*) Back-port `#42784`_ to 2016.11 + - **PR** `#42795`_: (*lomeroe*) backport `#42744`_ to 2016.11 + - **PR** `#42786`_: (*Ch3LL*) Fix typo for template_dict in http docs + - **PR** `#42784`_: (*gtmanfred*) only read file if ret is not a string in http.query + | refs: `#42804`_ + - **PR** `#42764`_: (*amendlik*) Fix infinite loop with salt-cloud and Windows nodes + - **PR** `#42748`_: (*whiteinge*) Workaround Orchestrate problem that highstate outputter mutates data + - **PR** `#42744`_: (*lomeroe*) fix `#42600`_ in develop + | refs: `#42794`_ `#42795`_ + - **PR** `#42694`_: (*gtmanfred*) allow adding extra remotes to a repository + - **PR** `#42651`_: (*gtmanfred*) python2- prefix for fedora 26 packages + - **PR** `#42552`_: (*remijouannet*) update consul module following this documentation https://www.consul.… + | refs: `#42805`_ + * 3ce18637be Merge pull request `#42855`_ from rallytime/merge-2017.7 + * 08bbcf5790 Merge branch '2016.11' into '2017.7' + + * 2dde1f77e9 Merge pull request `#42851`_ from terminalmage/`bp-42651`_ + + * a3da86eea8 fix syntax + + * 6ecdbcec1d make sure names are correct + + * f83b553d6e add py3 for versionlock + + * 21934f61bb python2- prefix for fedora 26 packages + + * c746f79a3a Merge pull request `#42806`_ from rallytime/`fix-42683`_ + + * 8c8640d6b8 Update doc references in glusterfs.volume_present + + * 27a8a2695a Merge pull request `#42829`_ from twangboy/win_pkg_fix_install + + * 83b9b230cd Add winrepo to docs about supporting versions in pkgs + + * 81fefa6e67 Add ability to pass version in pkgs list + + * 3c3ac6aeb2 Merge pull request `#42838`_ from twangboy/win_doc_pki + + * f0a1d06b46 Standardize PKI Client + + * 7de687aa57 Document requirements for win_pki + + * b3e2ae3c58 Merge pull request `#42805`_ from rallytime/`bp-42552`_ + + * 5a91c1f2d1 update consul module following this documentation https://www.consul.io/api/acl.html + + * d2ee7934ed Merge pull request `#42804`_ from rallytime/`bp-42784`_ + + * dbd29e4aaa only read file if it is not a string + + * 4cbf8057b3 Merge pull request `#42826`_ from terminalmage/fix-spelling + + * 00f93142e4 Fix misspelling of "versions" + + * de997edd90 Merge pull request `#42786`_ from Ch3LL/fix_typo + + * 90a2fb66a2 Fix typo for template_dict in http docs + + * bf6153ebe5 Merge pull request `#42795`_ from lomeroe/`bp-42744`__201611 + + * 695f8c1ae4 fix `#42600`_ in develop + + * 61fad97286 Merge pull request `#42748`_ from whiteinge/save-before-output + + * de60b77c82 Workaround Orchestrate problem that highstate outputter mutates data + + * a4e3e7e786 Merge pull request `#42764`_ from amendlik/cloud-win-loop + + * f3dcfca4e0 Fix infinite loops on failed Windows deployments + + * da85326ad4 Merge pull request `#42694`_ from gtmanfred/2016.11 + + * 1a0457af51 allow adding extra remotes to a repository + +- **PR** `#42808`_: (*terminalmage*) Fix regression in yum/dnf version specification + @ *2017-08-10T15:59:22Z* + + - **ISSUE** `#42774`_: (*rossengeorgiev*) pkg.installed succeeds, but fails when you specify package version + | refs: `#42808`_ + * f954f4f33a Merge pull request `#42808`_ from terminalmage/issue42774 + * c69f17dd18 Add integration test for `#42774`_ + + * 78d826dd14 Fix regression in yum/dnf version specification + +- **PR** `#42807`_: (*rallytime*) Update modules --> states in kubernetes doc module + @ *2017-08-10T14:10:40Z* + + - **ISSUE** `#42639`_: (*amnonbc*) k8s module needs a way to manage configmaps + | refs: `#42807`_ + * d9b0f44885 Merge pull request `#42807`_ from rallytime/`fix-42639`_ + * 152eb88d9f Update modules --> states in kubernetes doc module + +- **PR** `#42841`_: (*Mapel88*) Fix bug `#42818`_ in win_iis module + @ *2017-08-10T13:44:21Z* + + - **ISSUE** `#42818`_: (*Mapel88*) Bug in win_iis module - "create_cert_binding" + | refs: `#42841`_ + * b8c7bda68d Merge pull request `#42841`_ from Mapel88/patch-1 + * 497241fbcb Fix bug `#42818`_ in win_iis module + +- **PR** `#42782`_: (*rallytime*) Add a cmp compatibility function utility + @ *2017-08-09T22:37:29Z* + + - **ISSUE** `#42697`_: (*Ch3LL*) [Python3] NameError when running salt-run manage.versions + | refs: `#42782`_ + * 135f9522d0 Merge pull request `#42782`_ from rallytime/`fix-42697`_ + * d707f94863 Update all other calls to "cmp" function + + * 5605104285 Add a cmp compatibility function utility + +- **PR** `#42784`_: (*gtmanfred*) only read file if ret is not a string in http.query + | refs: `#42804`_ + @ *2017-08-08T17:20:13Z* + + * ac752223ad Merge pull request `#42784`_ from gtmanfred/http + * d397c90e92 only read file if it is not a string + +- **PR** `#42794`_: (*lomeroe*) Backport `#42744`_ to 2017.7 + @ *2017-08-08T17:16:31Z* + + - **ISSUE** `#42600`_: (*twangboy*) Unable to set 'Not Configured' using win_lgpo execution module + | refs: `#42744`_ `#42794`_ `#42795`_ + - **PR** `#42744`_: (*lomeroe*) fix `#42600`_ in develop + | refs: `#42794`_ `#42795`_ + * 44995b1abf Merge pull request `#42794`_ from lomeroe/`bp-42744`_ + * 0acffc6df5 fix `#42600`_ in develop + +- **PR** `#42708`_: (*cro*) Do not change the arguments of the function when memoizing + @ *2017-08-08T13:47:01Z* + + - **ISSUE** `#42707`_: (*cro*) Service module and state fails on FreeBSD + | refs: `#42708`_ + * dcf474c47c Merge pull request `#42708`_ from cro/dont_change_args_during_memoize + * a260e913b5 Do not change the arguments of the function when memoizing + +- **PR** `#42783`_: (*rallytime*) Sort lists before comparing them in python 3 unit test + @ *2017-08-08T13:25:15Z* + + - **PR** `#42206`_: (*rallytime*) [PY3] Fix test that is flaky in Python 3 + | refs: `#42783`_ + * ddb671b8fe Merge pull request `#42783`_ from rallytime/fix-flaky-py3-test + * 998834fbac Sort lists before compairing them in python 3 unit test + +- **PR** `#42721`_: (*hibbert*) Allow no ip sg + @ *2017-08-07T22:07:18Z* + + * d69822fe93 Merge pull request `#42721`_ from hibbert/allow_no_ip_sg + * f58256802a allow_no_ip_sg: Allow user to not supply ipaddress or securitygroups when running boto_efs.create_mount_target + +- **PR** `#42769`_: (*terminalmage*) Fix domainname parameter input translation + @ *2017-08-07T20:46:07Z* + + - **ISSUE** `#42538`_: (*marnovdm*) docker_container.running issue since 2017.7.0: passing domainname gives Error 500: json: cannot unmarshal array into Go value of type string + | refs: `#42769`_ + * bf7938fbe0 Merge pull request `#42769`_ from terminalmage/issue42538 + * 665de2d1f9 Fix domainname parameter input translation + +- **PR** `#42388`_: (*The-Loeki*) pillar.items pillar_env & pillar_override are never used + @ *2017-08-07T17:51:48Z* + + * 7bf2cdb363 Merge pull request `#42388`_ from The-Loeki/patch-1 + * 664f4b577b pillar.items pillar_env & pillar_override are never used + +- **PR** `#42770`_: (*rallytime*) [2017.7] Merge forward from 2017.7.1 to 2017.7 + @ *2017-08-07T16:21:45Z* + + * 9a8c9ebffc Merge pull request `#42770`_ from rallytime/merge-2017.7.1-into-2017.7 + * 6d17c9d227 Merge branch '2017.7.1' into '2017.7' + +- **PR** `#42768`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-07T16:21:17Z* + + - **ISSUE** `#42686`_: (*gilbsgilbs*) Unable to set multiple RabbitMQ tags + | refs: `#42693`_ `#42693`_ + - **ISSUE** `#42642`_: (*githubcdr*) state.augeas + | refs: `#42669`_ `#43202`_ + - **ISSUE** `#41433`_: (*sbojarski*) boto_cfn.present fails when reporting error for failed state + | refs: `#42574`_ + - **PR** `#42693`_: (*gilbsgilbs*) Fix RabbitMQ tags not properly set. + - **PR** `#42669`_: (*garethgreenaway*) [2016.11] Fixes to augeas module + - **PR** `#42655`_: (*whiteinge*) Reenable cpstats for rest_cherrypy + - **PR** `#42629`_: (*xiaoanyunfei*) tornado api + - **PR** `#42623`_: (*terminalmage*) Fix unicode constructor in custom YAML loader + - **PR** `#42574`_: (*sbojarski*) Fixed error reporting in "boto_cfn.present" function. + - **PR** `#33806`_: (*cachedout*) Work around upstream cherrypy bug + | refs: `#42655`_ + * c765e528d0 Merge pull request `#42768`_ from rallytime/merge-2017.7 + * 0f75482c37 Merge branch '2016.11' into '2017.7' + + * 7b2119feee Merge pull request `#42669`_ from garethgreenaway/42642_2016_11_augeas_module_fix + + * 24413084e2 Updating the call to shlex_split to pass the posix=False argument so that quotes are preserved. + + * 30725769ed Merge pull request `#42629`_ from xiaoanyunfei/tornadoapi + + * 1e13383b95 tornado api + + * f0f00fcee1 Merge pull request `#42655`_ from whiteinge/rest_cherrypy-reenable-stats + + * deb6316d67 Fix lint errors + + * 6bd91c8b03 Reenable cpstats for rest_cherrypy + + * 21cf15f9c3 Merge pull request `#42693`_ from gilbsgilbs/fix-rabbitmq-tags + + * 78fccdc7e2 Cast to list in case tags is a tuple. + + * 287b57b5c5 Fix RabbitMQ tags not properly set. + + * f2b0c9b4fa Merge pull request `#42574`_ from sbojarski/boto-cfn-error-reporting + + * 5c945f10c2 Fix debug message in "boto_cfn._validate" function. + + * 181a1beecc Fixed error reporting in "boto_cfn.present" function. + + * bc1effc4f2 Merge pull request `#42623`_ from terminalmage/fix-unicode-constructor + + * fcf45889dd Fix unicode constructor in custom YAML loader + +- **PR** `#42651`_: (*gtmanfred*) python2- prefix for fedora 26 packages + @ *2017-08-07T14:35:04Z* + + * 3f5827f61e Merge pull request `#42651`_ from gtmanfred/2017.7 + * 8784899942 fix syntax + + * 178cc1bd81 make sure names are correct + + * f179b97b52 add py3 for versionlock + + * 1958d18634 python2- prefix for fedora 26 packages + +- **PR** `#42689`_: (*hibbert*) boto_efs_fix_tags: Fix `#42688`_ invalid type for parameter tags + @ *2017-08-06T17:47:07Z* + + - **ISSUE** `#42688`_: (*hibbert*) salt.modules.boto_efs module Invalid type for parameter Tags - type: , valid types: , + | refs: `#42689`_ + * 791248e398 Merge pull request `#42689`_ from hibbert/boto_efs_fix_tags + * 157fb28851 boto_efs_fix_tags: Fix `#42688`_ invalid type for parameter tags + +- **PR** `#42745`_: (*terminalmage*) docker.compare_container: treat null oom_kill_disable as False + @ *2017-08-05T15:28:20Z* + + - **ISSUE** `#42705`_: (*hbruch*) salt.states.docker_container.running replaces container on subsequent runs if oom_kill_disable unsupported + | refs: `#42745`_ + * 1b3407649b Merge pull request `#42745`_ from terminalmage/issue42705 + * 710bdf6115 docker.compare_container: treat null oom_kill_disable as False + +- **PR** `#42704`_: (*whiteinge*) Add import to work around likely multiprocessing scoping bug + @ *2017-08-04T23:03:13Z* + + - **ISSUE** `#42649`_: (*tehsu*) local_batch no longer working in 2017.7.0, 500 error + | refs: `#42704`_ + * 5d5b22021b Merge pull request `#42704`_ from whiteinge/expr_form-warn-scope-bug + * 03b675a618 Add import to work around likely multiprocessing scoping bug + +- **PR** `#42743`_: (*kkoppel*) Fix docker.compare_container for containers with links + @ *2017-08-04T16:00:33Z* + + - **ISSUE** `#42741`_: (*kkoppel*) docker_container.running keeps re-creating containers with links to other containers + | refs: `#42743`_ + * 888e954e73 Merge pull request `#42743`_ from kkoppel/fix-issue-42741 + * de6d3cc0cf Update dockermod.py + + * 58b997c67f Added a helper function that removes container names from container HostConfig:Links values to enable compare_container() to make the correct decision about differences in links. + +- **PR** `#42710`_: (*gtmanfred*) use subtraction instead of or + @ *2017-08-04T15:14:14Z* + + - **ISSUE** `#42668`_: (*UtahDave*) Minions under syndics don't respond to MoM + | refs: `#42710`_ + - **ISSUE** `#42545`_: (*paul-mulvihill*) Salt-api failing to return results for minions connected via syndics. + | refs: `#42710`_ + * 03a7f9bbee Merge pull request `#42710`_ from gtmanfred/syndic + * 683561a711 use subtraction instead of or + +- **PR** `#42670`_: (*gtmanfred*) render kubernetes docs + @ *2017-08-03T20:30:56Z* + + * 005182b6a1 Merge pull request `#42670`_ from gtmanfred/kube + * bca17902f5 add version added info + + * 4bbfc751ae render kubernetes docs + +- **PR** `#42712`_: (*twangboy*) Remove master config file from minion-only installer + @ *2017-08-03T20:25:02Z* + + * df354ddabf Merge pull request `#42712`_ from twangboy/win_build_pkg + * 8604312a7b Remove master conf in minion install + +- **PR** `#42714`_: (*cachedout*) Set fact gathering style to 'old' for test_junos + @ *2017-08-03T13:39:40Z* + + * bb1dfd4a42 Merge pull request `#42714`_ from cachedout/workaround_jnpr_test_bug + * 834d6c605e Set fact gathering style to 'old' for test_junos + +- **PR** `#42481`_: (*twangboy*) Fix `unit.test_crypt` for Windows + @ *2017-08-01T18:10:50Z* + + * 4c1d931654 Merge pull request `#42481`_ from twangboy/win_unit_test_crypt + * 102509029e Remove chown mock, fix path seps + +- **PR** `#42654`_: (*morganwillcock*) Disable ZFS in the core grain for NetBSD + @ *2017-08-01T17:52:36Z* + + * 8bcefb5e67 Merge pull request `#42654`_ from morganwillcock/zfsgrain + * 49023deb94 Disable ZFS grain on NetBSD + +- **PR** `#42453`_: (*gtmanfred*) don't pass user to makedirs on windows + @ *2017-07-31T19:57:57Z* + + - **ISSUE** `#42421`_: (*bartuss7*) archive.extracted on Windows failed when dir not exist + | refs: `#42453`_ + * 5baf2650fc Merge pull request `#42453`_ from gtmanfred/makedirs + * 559d432930 fix tests + + * afa7a13ce3 use logic from file.directory for makedirs + +- **PR** `#42603`_: (*twangboy*) Add runas_passwd as a global for states + @ *2017-07-31T19:49:49Z* + + * fb81e78f71 Merge pull request `#42603`_ from twangboy/win_fix_runas + * 0c9e40012b Remove deprecation, add logic to state.py + + * 464ec34713 Fix another instance of runas_passwd + + * 18d6ce4d55 Add global vars to cmd.call + + * 6c71ab6f80 Remove runas and runas_password after state run + + * 4ea264e3db Change to runas_password in docs + + * 61aba35718 Deprecate password, make runas_password a named arg + + * 41f0f75a06 Add new var to list, change to runas_password + + * b9c91eba60 Add runas_passwd as a global for states + +- **PR** `#42541`_: (*Mareo*) Avoid confusing warning when using file.line + @ *2017-07-31T19:41:58Z* + + * 75ba23c253 Merge pull request `#42541`_ from epita/fix-file-line-warning + * 2fd172e07b Avoid confusing warning when using file.line + +- **PR** `#42625`_: (*twangboy*) Fix the list function in the win_wua execution module + @ *2017-07-31T19:27:16Z* + + * 3d328eba80 Merge pull request `#42625`_ from twangboy/fix_win_wua + * 1340c15ce7 Add general usage instructions + + * 19f34bda55 Fix docs, formatting + + * b17495c9c8 Fix problem with list when install=True + +- **PR** `#42602`_: (*garethgreenaway*) Use superseded and deprecated configuration from pillar + @ *2017-07-31T18:53:06Z* + + - **ISSUE** `#42514`_: (*rickh563*) `module.run` does not work as expected in 2017.7.0 + | refs: `#42602`_ + * 25094ad9b1 Merge pull request `#42602`_ from garethgreenaway/42514_2017_7_superseded_deprecated_from_pillar + * 2e132daa73 Slight update to formatting + + * 74bae13939 Small update to something I missed in the first commit. Updating tests to also test for pillar values. + + * 928a4808dd Updating the superseded and deprecated decorators to work when specified as pillar values. + +- **PR** `#42621`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-28T19:45:51Z* + + - **ISSUE** `#42456`_: (*gdubroeucq*) Use yum lib + | refs: `#42586`_ + - **ISSUE** `#41982`_: (*abulford*) dockerng.network_* matches too easily + | refs: `#41988`_ `#41988`_ `#42006`_ `#42006`_ + - **PR** `#42586`_: (*gdubroeucq*) [Fix] yumpkg.py: add option to the command "check-update" + - **PR** `#42515`_: (*gtmanfred*) Allow not interpreting backslashes in the repl + - **PR** `#41988`_: (*abulford*) Fix dockerng.network_* name matching + | refs: `#42006`_ + * b7cd30d3ee Merge pull request `#42621`_ from rallytime/merge-2017.7 + * 58dcb58a47 Merge branch '2016.11' into '2017.7' + + * cbf752cd73 Merge pull request `#42515`_ from gtmanfred/backslash + + * cc4e45656d Allow not interpreting backslashes in the repl + + * 549495831f Merge pull request `#42586`_ from gdubroeucq/2016.11 + + * 9c0b5cc1d6 Remove extra newline + + * d2ef4483e4 yumpkg.py: clean + + * a96f7c09e0 yumpkg.py: add option to the command "check-update" + + * 6b45debf28 Merge pull request `#41988`_ from redmatter/fix-dockerng-network-matching + + * 9eea796da8 Add regression tests for `#41982`_ + + * 3369f0072f Fix broken unit test test_network_absent + + * 0ef6cf634c Add trace logging of dockerng.networks result + + * 515c612808 Fix dockerng.network_* name matching + +- **PR** `#42618`_: (*rallytime*) Back-port `#41690`_ to 2017.7 + @ *2017-07-28T19:27:11Z* + + - **ISSUE** `#34245`_: (*Talkless*) ini.options_present always report state change + | refs: `#41690`_ + - **PR** `#41690`_: (*m03*) Fix issue `#34245`_ with ini.options_present reporting changes + | refs: `#42618`_ + * d48749b476 Merge pull request `#42618`_ from rallytime/`bp-41690`_ + * 22c6a7c7ff Improve output precision + + * ee4ea6b860 Fix `#34245`_ ini.options_present reporting changes + +- **PR** `#42619`_: (*rallytime*) Back-port `#42589`_ to 2017.7 + @ *2017-07-28T19:26:36Z* + + - **ISSUE** `#42588`_: (*ixs*) salt-ssh fails when using scan roster and detected minions are uncached + | refs: `#42589`_ + - **PR** `#42589`_: (*ixs*) Fix ssh-salt calls with scan roster for uncached clients + | refs: `#42619`_ + * e671242a4f Merge pull request `#42619`_ from rallytime/`bp-42589`_ + * cd5eb93903 Fix ssh-salt calls with scan roster for uncached clients + +- **PR** `#42006`_: (*abulford*) Fix dockerng.network_* name matching + @ *2017-07-28T15:52:52Z* + + - **ISSUE** `#41982`_: (*abulford*) dockerng.network_* matches too easily + | refs: `#41988`_ `#41988`_ `#42006`_ `#42006`_ + - **PR** `#41988`_: (*abulford*) Fix dockerng.network_* name matching + | refs: `#42006`_ + * 7d385f8bdc Merge pull request `#42006`_ from redmatter/fix-dockerng-network-matching-2017.7 + * f83960c02a Lint: Remove extra line at end of file. + + * c7d364ec56 Add regression tests for `#41982`_ + + * d31f2913bd Fix broken unit test test_network_absent + + * d42f781c64 Add trace logging of docker.networks result + + * 8c00c63b55 Fix dockerng.network_* name matching + +- **PR** `#42616`_: (*amendlik*) Sync cloud modules + @ *2017-07-28T15:40:36Z* + + - **ISSUE** `#12587`_: (*Katafalkas*) salt-cloud custom functions/actions + | refs: `#42616`_ + * ee8aee1496 Merge pull request `#42616`_ from amendlik/sync-clouds + * ab21bd9b5b Sync cloud modules when saltutil.sync_all is run + +- **PR** `#42601`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-27T22:32:07Z* + + - **ISSUE** `#1036125`_: (**) + - **ISSUE** `#42477`_: (*aikar*) Invalid ssh_interface value prevents salt-cloud provisioning without reason of why + | refs: `#42479`_ + - **ISSUE** `#42405`_: (*felrivero*) The documentation is incorrectly compiled (PILLAR section) + | refs: `#42516`_ + - **ISSUE** `#42403`_: (*astronouth7303*) [2017.7] Pillar empty when state is applied from orchestrate + | refs: `#42433`_ + - **ISSUE** `#42375`_: (*dragonpaw*) salt.modules.*.__virtualname__ doens't work as documented. + | refs: `#42523`_ `#42958`_ + - **ISSUE** `#42371`_: (*tsaridas*) Minion unresponsive after trying to failover + | refs: `#42387`_ + - **ISSUE** `#41955`_: (*root360-AndreasUlm*) rabbitmq 3.6.10 changed output => rabbitmq-module broken + | refs: `#41968`_ + - **ISSUE** `#23516`_: (*dkiser*) BUG: cron job scheduler sporadically works + | refs: `#42077`_ + - **PR** `#42573`_: (*rallytime*) Back-port `#42433`_ to 2016.11 + - **PR** `#42571`_: (*twangboy*) Avoid loading system PYTHON* environment vars + - **PR** `#42551`_: (*binocvlar*) Remove '-s' (--script) argument to parted within align_check function + - **PR** `#42527`_: (*twangboy*) Document changes to Windows Update in Windows 10/Server 2016 + - **PR** `#42523`_: (*rallytime*) Add a mention of the True/False returns with __virtual__() + - **PR** `#42516`_: (*rallytime*) Add info about top file to pillar walk-through example to include edit.vim + - **PR** `#42479`_: (*gtmanfred*) validate ssh_interface for ec2 + - **PR** `#42433`_: (*terminalmage*) Only force saltenv/pillarenv to be a string when not None + | refs: `#42573`_ + - **PR** `#42414`_: (*vutny*) DOCS: unify hash sum with hash type format + - **PR** `#42387`_: (*DmitryKuzmenko*) Fix race condition in usage of weakvaluedict + - **PR** `#42339`_: (*isbm*) Bugfix: Jobs scheduled to run at a future time stay pending for Salt minions (bsc`#1036125`_) + - **PR** `#42077`_: (*vutny*) Fix scheduled job run on Master if `when` parameter is a list + | refs: `#42107`_ + - **PR** `#41973`_: (*vutny*) Fix Master/Minion scheduled jobs based on Cron expressions + | refs: `#42077`_ + - **PR** `#41968`_: (*root360-AndreasUlm*) Fix rabbitmqctl output sanitizer for version 3.6.10 + * e2dd443002 Merge pull request `#42601`_ from rallytime/merge-2017.7 + * 36a1bcf8c5 Merge branch '2016.11' into '2017.7' + + * 4b16109122 Merge pull request `#42339`_ from isbm/isbm-jobs-scheduled-in-a-future-bsc1036125 + + * bbba84ce2d Bugfix: Jobs scheduled to run at a future time stay pending for Salt minions (bsc`#1036125`_) + + * 6c5a7c604a Merge pull request `#42077`_ from vutny/fix-jobs-scheduled-with-whens + + * b1960cea44 Fix scheduled job run on Master if `when` parameter is a list + + * f9cb536589 Merge pull request `#42414`_ from vutny/unify-hash-params-format + + * d1f2a93368 DOCS: unify hash sum with hash type format + + * 535c922511 Merge pull request `#42523`_ from rallytime/`fix-42375`_ + + * 685c2cced6 Add information about returning a tuple with an error message + + * fa466519c4 Add a mention of the True/False returns with __virtual__() + + * 0df0e7e749 Merge pull request `#42527`_ from twangboy/win_wua + + * 0373791f2a Correct capatlization + + * af3bcc927b Document changes to Windows Update in 10/2016 + + * 69b06586da Merge pull request `#42551`_ from binocvlar/fix-lack-of-align-check-output + + * c4fabaa192 Remove '-s' (--script) argument to parted within align_check function + + * 9e0b4e9faf Merge pull request `#42573`_ from rallytime/`bp-42433`_ + + * 0293429e24 Only force saltenv/pillarenv to be a string when not None + + * e931ed2517 Merge pull request `#42571`_ from twangboy/win_add_pythonpath + + * d55a44dd1a Avoid loading user site packages + + * 9af1eb2741 Ignore any PYTHON* environment vars already on the system + + * 4e2fb03a95 Add pythonpath to batch files and service + + * de2f397041 Merge pull request `#42387`_ from DSRCorporation/bugs/42371_KeyError_WeakValueDict + + * e721c7eee2 Don't use `key in weakvaluedict` because it could lie. + + * 641a9d7efd Merge pull request `#41968`_ from root360-AndreasUlm/fix-rabbitmqctl-output-handler + + * 76fd941d91 added tests for rabbitmq 3.6.10 output handler + + * 3602af1e1b Fix rabbitmqctl output handler for 3.6.10 + + * 66fede378a Merge pull request `#42479`_ from gtmanfred/interface + + * c32c1b2803 fix pylint + + * 99ec634c6b validate ssh_interface for ec2 + + * a925c7029a Merge pull request `#42516`_ from rallytime/`fix-42405`_ + + * e3a6717efa Add info about top file to pillar walk-through example to include edit.vim + +- **PR** `#42290`_: (*isbm*) Backport of `#42270`_ + @ *2017-07-27T22:30:05Z* + + * 22eea389fa Merge pull request `#42290`_ from isbm/isbm-module_run_parambug_42270_217 + * e38d432f90 Fix docs + + * 1e8a56eda5 Describe function tagging + + * 1d7233224b Describe function batching + + * 1391a05d5e Bugfix: syntax error in the example + + * 8c71257a4b Call unnamed parameters properly + + * 94c97a8f25 Update and correct the error message + + * ea8351362c Bugfix: args gets ignored alongside named parameters + + * 74689e3462 Add ability to use tagged functions in the same set + +- **PR** `#42251`_: (*twangboy*) Fix `unit.modules.test_win_ip` for Windows + @ *2017-07-27T19:22:03Z* + + * 4c20f1cfbb Merge pull request `#42251`_ from twangboy/unit_win_test_win_ip + * 97261bfe69 Fix win_inet_pton check for malformatted ip addresses + +- **PR** `#42255`_: (*twangboy*) Fix `unit.modules.test_win_system` for Windows + @ *2017-07-27T19:12:42Z* + + * 2985e4c0e6 Merge pull request `#42255`_ from twangboy/win_unit_test_win_system + * acc0345bc8 Fix unit tests + +- **PR** `#42528`_: (*twangboy*) Namespace `cmp_to_key` in the pkg state for Windows + @ *2017-07-27T18:30:23Z* + + * a573386260 Merge pull request `#42528`_ from twangboy/win_fix_pkg_state + * a040443fa1 Move functools import inside pylint escapes + + * 118d5134e2 Remove namespaced function `cmp_to_key` + + * a02c91adda Namespace `cmp_to_key` in the pkg state for Windows + +- **PR** `#42534`_: (*jmarinaro*) Fixes AttributeError thrown by chocolatey state + @ *2017-07-27T17:59:50Z* + + - **ISSUE** `#42521`_: (*rickh563*) chocolatey.installed broken on 2017.7.0 + | refs: `#42534`_ + * 62ae12bcd9 Merge pull request `#42534`_ from jmarinaro/2017.7 + * b242d2d6b5 Fixes AttributeError thrown by chocolatey state Fixes `#42521`_ + +- **PR** `#42557`_: (*justincbeard*) Fixing output so --force-color and --no-color override master and min… + @ *2017-07-27T17:07:33Z* + + - **ISSUE** `#40354`_: (*exc414*) CentOS 6.8 Init Script - Sed unterminated address regex + | refs: `#42557`_ + - **ISSUE** `#37312`_: (*gtmanfred*) CLI flags should take overload settings in the config files + | refs: `#42557`_ + * 52605c249d Merge pull request `#42557`_ from justincbeard/bugfix_37312 + * ee3bc6eb10 Fixing output so --force-color and --no-color override master and minion config color value + +- **PR** `#42567`_: (*skizunov*) Fix disable_ config option + @ *2017-07-27T17:05:00Z* + + * ab33517efb Merge pull request `#42567`_ from skizunov/develop3 + * 0f0b7e3e0a Fix disable_ config option + +- **PR** `#42577`_: (*twangboy*) Compile scripts with -E -s params for Salt on Mac + @ *2017-07-26T22:44:37Z* + + * 30bb941179 Merge pull request `#42577`_ from twangboy/mac_scripts + * 69d5973651 Compile scripts with -E -s params for python + +- **PR** `#42524`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-26T22:41:06Z* + + - **ISSUE** `#42417`_: (*clem-compilatio*) salt-cloud - openstack - "no more floating IP addresses" error - but public_ip in node + | refs: `#42509`_ + - **ISSUE** `#42413`_: (*goten4*) Invalid error message when proxy_host is set and tornado not installed + | refs: `#42424`_ + - **ISSUE** `#42357`_: (*Giandom*) Salt pillarenv problem with slack engine + | refs: `#42443`_ `#42444`_ + - **ISSUE** `#42198`_: (*shengis*) state sqlite3.row_absent fail with "parameters are of unsupported type" + | refs: `#42200`_ + - **PR** `#42509`_: (*clem-compilatio*) Fix _assign_floating_ips in openstack.py + - **PR** `#42464`_: (*garethgreenaway*) [2016.11] Small fix to modules/git.py + - **PR** `#42443`_: (*garethgreenaway*) [2016.11] Fix to slack engine + - **PR** `#42424`_: (*goten4*) Fix error message when tornado or pycurl is not installed + - **PR** `#42200`_: (*shengis*) Fix `#42198`_ + * 60cd078164 Merge pull request `#42524`_ from rallytime/merge-2017.7 + * 14d8d795f6 Merge branch '2016.11' into '2017.7' + + * 1bd5bbccc2 Merge pull request `#42509`_ from clem-compilatio/`fix-42417`_ + + * 72924b06b8 Fix _assign_floating_ips in openstack.py + + * 4bf35a74de Merge pull request `#42464`_ from garethgreenaway/2016_11_remove_tmp_identity_file + + * ff24102d51 Uncomment the line that removes the temporary identity file. + + * e2120dbd0e Merge pull request `#42443`_ from garethgreenaway/42357_pass_args_kwargs_correctly + + * 635810b3e3 Updating the slack engine in 2016.11 to pass the args and kwrags correctly to LocalClient + + * 8262cc9054 Merge pull request `#42200`_ from shengis/sqlite3_fix_row_absent_2016.11 + + * 407b8f4bb3 Fix `#42198`_ If where_args is not set, not using it in the delete request. + + * d9df97e5a3 Merge pull request `#42424`_ from goten4/2016.11 + + * 1c0574d05e Fix error message when tornado or pycurl is not installed + +- **PR** `#42575`_: (*rallytime*) [2017.7] Merge forward from 2017.7.1 to 2017.7 + @ *2017-07-26T22:39:10Z* + + * 2acde837df Merge pull request `#42575`_ from rallytime/merge-2017.7.1-into-2017.7 + * 63bb0fb2c4 pass in empty kwarg for reactor + + * 2868061ee4 update chunk, not kwarg in chunk + + * 46715e9d94 Merge branch '2017.7.1' into '2017.7' + +- **PR** `#42555`_: (*Ch3LL*) add changelog to 2017.7.1 release notes + @ *2017-07-26T14:57:43Z* + + * 1d93e92194 Merge pull request `#42555`_ from Ch3LL/7.1_add_changelog + * fb69e71093 add changelog to 2017.7.1 release notes + +- **PR** `#42266`_: (*twangboy*) Fix `unit.states.test_file` for Windows + @ *2017-07-25T20:26:32Z* + + * 07c2793e86 Merge pull request `#42266`_ from twangboy/win_unit_states_test_file + * 669aaee10d Mock file exists properly + + * a4231c9827 Fix ret mock for linux + + * 0c484f8979 Fix unit tests on Windows + +- **PR** `#42484`_: (*shengis*) Fix a potential Exception with an explicit error message + @ *2017-07-25T18:34:12Z* + + * df417eae17 Merge pull request `#42484`_ from shengis/fix-explicit-error-msg-x509-sign-remote + * 0b548c72e1 Fix a potential Exception with an explicit error message + +- **PR** `#42529`_: (*gtmanfred*) Fix joyent for python3 + @ *2017-07-25T16:37:48Z* + + - **ISSUE** `#41720`_: (*rallytime*) [Py3] Some salt-cloud drivers do not work using Python 3 + | refs: `#42529`_ + - **PR** `#396`_: (*mb0*) add file state template context and defaults + | refs: `#42529`_ + * 0f25ec76f9 Merge pull request `#42529`_ from gtmanfred/2017.7 + * b7ebb4d81a these drivers do not actually have an issue. + + * e90ca7a114 use salt encoding for joyent on 2017.7 + +- **PR** `#42465`_: (*garethgreenaway*) [2017.7] Small fix to modules/git.py + @ *2017-07-24T17:24:55Z* + + * 488457c5a0 Merge pull request `#42465`_ from garethgreenaway/2017_7_remove_tmp_identity_file + * 1920dc6079 Uncomment the line that removes the temporary identity file. + +- **PR** `#42107`_: (*vutny*) [2017.7] Fix scheduled jobs if `when` parameter is a list + @ *2017-07-24T17:04:12Z* + + - **ISSUE** `#23516`_: (*dkiser*) BUG: cron job scheduler sporadically works + | refs: `#42077`_ + - **PR** `#42077`_: (*vutny*) Fix scheduled job run on Master if `when` parameter is a list + | refs: `#42107`_ + - **PR** `#41973`_: (*vutny*) Fix Master/Minion scheduled jobs based on Cron expressions + | refs: `#42077`_ + * 4f044999fa Merge pull request `#42107`_ from vutny/2017.7-fix-jobs-scheduled-with-whens + * 905be493d4 [2017.7] Fix scheduled jobs if `when` parameter is a list + +- **PR** `#42506`_: (*terminalmage*) Add PER_REMOTE_ONLY to init_remotes call in git_pillar runner + @ *2017-07-24T16:59:21Z* + + * 6eaa0763e1 Merge pull request `#42506`_ from terminalmage/fix-git-pillar-runner + * 6352f447ce Add PER_REMOTE_ONLY to init_remotes call in git_pillar runner + +- **PR** `#42502`_: (*shengis*) Fix azurerm query to show IPs + @ *2017-07-24T15:54:45Z* + + * b88e645f10 Merge pull request `#42502`_ from shengis/fix_azurerm_request_ips + * 92f1890701 Fix azurerm query to show IPs + +- **PR** `#42180`_: (*twangboy*) Fix `unit.modules.test_timezone` for Windows + @ *2017-07-24T14:46:16Z* + + * c793d83d26 Merge pull request `#42180`_ from twangboy/win_unit_test_timezone + * 832a3d86dd Skip tests that use os.symlink on Windows + +- **PR** `#42474`_: (*whiteinge*) Cmd arg kwarg parsing test + @ *2017-07-24T14:13:30Z* + + - **PR** `#39646`_: (*terminalmage*) Handle deprecation of passing string args to load_args_and_kwargs + | refs: `#42474`_ + * 083ff00410 Merge pull request `#42474`_ from whiteinge/cmd-arg-kwarg-parsing-test + * 0cc0c0967a Lint fixes + + * 66093738c8 Add back support for string kwargs + + * 622ff5be40 Add LocalClient.cmd test for arg/kwarg parsing + + * 9f4eb80d90 Add a test.arg variant that cleans the pub kwargs by default + +- **PR** `#42425`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-21T22:43:41Z* + + - **ISSUE** `#42333`_: (*b3hni4*) Getting "invalid type of dict, a list is required" when trying to configure engines in master config file + | refs: `#42352`_ + - **ISSUE** `#32400`_: (*rallytime*) Document Default Config Values + | refs: `#42319`_ + - **PR** `#42370`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + - **PR** `#42368`_: (*twangboy*) Remove build and dist directories before install (2016.11) + - **PR** `#42360`_: (*Ch3LL*) [2016.11] Update version numbers in doc config for 2017.7.0 release + - **PR** `#42359`_: (*Ch3LL*) [2016.3] Update version numbers in doc config for 2017.7.0 release + - **PR** `#42356`_: (*meaksh*) Allow to check whether a function is available on the AliasesLoader wrapper + - **PR** `#42352`_: (*CorvinM*) Multiple documentation fixes + - **PR** `#42350`_: (*twangboy*) Fixes problem with Version and OS Release related grains on certain versions of Python (2016.11) + - **PR** `#42319`_: (*rallytime*) Add more documentation for config options that are missing from master/minion docs + * c91a5e539e Merge pull request `#42425`_ from rallytime/merge-2017.7 + * ea457aa0a5 Remove ALIASES block from template util + + * c673b64583 Merge branch '2016.11' into '2017.7' + + * 42bb1a64ca Merge pull request `#42350`_ from twangboy/win_fix_ver_grains_2016.11 + + * 8c048403d7 Detect Server OS with a desktop release name + + * 0a72e56f6b Merge pull request `#42356`_ from meaksh/2016.11-AliasesLoader-wrapper-fix + + * 915d94219e Allow to check whether a function is available on the AliasesLoader wrapper + + * 10eb7b7a79 Merge pull request `#42368`_ from twangboy/win_fix_build_2016.11 + + * a7c910c31e Remove build and dist directories before install + + * 016189f62f Merge pull request `#42370`_ from rallytime/merge-2016.11 + + * 0aa5dde1de Merge branch '2016.3' into '2016.11' + + * e9b0f20f8a Merge pull request `#42359`_ from Ch3LL/doc-update-2016.3 + + * dc85b5edbe [2016.3] Update version numbers in doc config for 2017.7.0 release + + * f06a6f1796 Merge pull request `#42360`_ from Ch3LL/doc-update-2016.11 + + * b90b7a7506 [2016.11] Update version numbers in doc config for 2017.7.0 release + + * e0595b0a0f Merge pull request `#42319`_ from rallytime/config-docs + + * b40f980632 Add more documentation for config options that are missing from master/minion docs + + * 78940400e3 Merge pull request `#42352`_ from CorvinM/issue42333 + + * 526b6ee14d Multiple documentation fixes + +- **PR** `#42444`_: (*garethgreenaway*) [2017.7] Fix to slack engine + @ *2017-07-21T22:03:48Z* + + - **ISSUE** `#42357`_: (*Giandom*) Salt pillarenv problem with slack engine + | refs: `#42443`_ `#42444`_ + * 10e4d9234b Merge pull request `#42444`_ from garethgreenaway/42357_2017_7_pass_args_kwargs_correctly + * f411cfc2a9 Updating the slack engine in 2017.7 to pass the args and kwrags correctly to LocalClient + +- **PR** `#42461`_: (*rallytime*) Bump warning version from Oxygen to Fluorine in roster cache + @ *2017-07-21T21:33:25Z* + + * 723be49fac Merge pull request `#42461`_ from rallytime/bump-roster-cache-deprecations + * c0df0137f5 Bump warning version from Oxygen to Fluorine in roster cache + +- **PR** `#42436`_: (*garethgreenaway*) Fixes to versions function in manage runner + @ *2017-07-21T19:41:07Z* + + - **ISSUE** `#42374`_: (*tyhunt99*) [2017.7.0] salt-run mange.versions throws exception if minion is offline or unresponsive + | refs: `#42436`_ + * 09521602c1 Merge pull request `#42436`_ from garethgreenaway/42374_manage_runner_minion_offline + * 0fd39498c0 Updating the versions function inside the manage runner to account for when a minion is offline and we are unable to determine it's version. + +- **PR** `#42435`_: (*terminalmage*) Modify our custom YAML loader to treat unicode literals as unicode strings + | refs: `#42812`_ + @ *2017-07-21T19:40:34Z* + + - **ISSUE** `#42427`_: (*grichmond-salt*) Issue Passing Variables created from load_json as Inline Pillar Between States + | refs: `#42435`_ + * 54193ea543 Merge pull request `#42435`_ from terminalmage/issue42427 + * 31273c7ec1 Modify our custom YAML loader to treat unicode literals as unicode strings + +- **PR** `#42399`_: (*rallytime*) Update old "ref" references to "rev" in git.detached state + @ *2017-07-21T19:38:59Z* + + - **ISSUE** `#42381`_: (*zebooka*) Git.detached broken in 2017.7.0 + | refs: `#42399`_ + - **ISSUE** `#38878`_: (*tomlaredo*) [Naming consistency] git.latest "rev" option VS git.detached "ref" option + | refs: `#38898`_ + - **PR** `#38898`_: (*terminalmage*) git.detached: rename ref to rev for consistency + | refs: `#42399`_ + * 0b3179135c Merge pull request `#42399`_ from rallytime/`fix-42381`_ + * d9d94fe02f Update old "ref" references to "rev" in git.detached state + +- **PR** `#42031`_: (*skizunov*) Fix: Reactor emits critical error + @ *2017-07-21T19:38:34Z* + + - **ISSUE** `#42400`_: (*Enquier*) Conflict in execution of passing pillar data to orch/reactor event executions 2017.7.0 + | refs: `#42031`_ + * bd4adb483d Merge pull request `#42031`_ from skizunov/develop3 + * 540977b4b1 Fix: Reactor emits critical error + +- **PR** `#42027`_: (*gtmanfred*) import salt.minion for EventReturn for Windows + @ *2017-07-21T19:37:03Z* + + - **ISSUE** `#41949`_: (*jrporcaro*) Event returner doesn't work with Windows Master + | refs: `#42027`_ + * 3abf7ad7d7 Merge pull request `#42027`_ from gtmanfred/2017.7 + * fd4458b6c7 import salt.minion for EventReturn for Windows + +- **PR** `#42454`_: (*terminalmage*) Document future renaming of new rand_str jinja filter + @ *2017-07-21T18:47:51Z* + + * 994d3dc74a Merge pull request `#42454`_ from terminalmage/jinja-docs-2017.7 + * 98b661406e Document future renaming of new rand_str jinja filter + +- **PR** `#42452`_: (*Ch3LL*) update windows urls to new py2/py3 naming scheme + @ *2017-07-21T17:20:47Z* + + * 4480075129 Merge pull request `#42452`_ from Ch3LL/fix_url_windows + * 3f4a918f73 update windows urls to new py2/py3 naming scheme + +- **PR** `#42411`_: (*seedickcode*) Fix file.managed check_cmd file not found - Issue `#42404`_ + @ *2017-07-20T21:59:17Z* + + - **ISSUE** `#42404`_: (*gabekahen*) [2017.7] file.managed with cmd_check "No such file or directory" + | refs: `#42411`_ + - **ISSUE** `#33708`_: (*pepinje*) visudo check command leaves cache file in /tmp + | refs: `#42411`_ `#38063`_ + - **PR** `#38063`_: (*llua*) tmp file clean up in file.manage - fix for `#33708`_ + | refs: `#42411`_ + * 33e90be1fe Merge pull request `#42411`_ from seedickcode/check_cmd_fix + * 4ae3911f01 Fix file.managed check_cmd file not found - Issue `#42404`_ + +- **PR** `#42409`_: (*twangboy*) Add Scripts to build Py3 on Mac + @ *2017-07-20T21:36:34Z* + + * edde31376a Merge pull request `#42409`_ from twangboy/mac_py3_scripts + * ac0e04af72 Remove build and dist, sign pkgs + + * 9d66e273c4 Fix hard coded pip path + + * 7b8d6cbbd2 Add support for Py3 + + * aa4eed93c8 Update Python and other reqs + +- **PR** `#42433`_: (*terminalmage*) Only force saltenv/pillarenv to be a string when not None + | refs: `#42573`_ + @ *2017-07-20T21:32:24Z* + + - **ISSUE** `#42403`_: (*astronouth7303*) [2017.7] Pillar empty when state is applied from orchestrate + | refs: `#42433`_ + * 82982f940d Merge pull request `#42433`_ from terminalmage/issue42403 +- **PR** `#42408`_: (*CorvinM*) Fix documentation misformat in salt.states.file.replace + @ *2017-07-20T00:45:43Z* + + * a71938cefe Merge pull request `#42408`_ from CorvinM/file-replace-doc-fix + * 246a2b3e74 Fix documentation misformat in salt.states.file.replace + +- **PR** `#42347`_: (*twangboy*) Fixes problem with Version and OS Release related grains on certain versions of Python + @ *2017-07-19T17:05:43Z* + + * d385dfd19d Merge pull request `#42347`_ from twangboy/win_fix_ver_grains + * ef1f663fc9 Detect server OS with a desktop release name + +- **PR** `#42366`_: (*twangboy*) Remove build and dist directories before install + @ *2017-07-19T16:37:41Z* + + * eb9e4206c9 Merge pull request `#42366`_ from twangboy/win_fix_build + * 0946002713 Add blank line after delete + + * f7c0bb4f46 Remove build and dist directories before install + +- **PR** `#42373`_: (*Ch3LL*) Add initial 2017.7.1 Release Notes File + @ *2017-07-19T16:28:46Z* + + * af7820f25d Merge pull request `#42373`_ from Ch3LL/add_2017.7.1 + * ce1c1b6d28 Add initial 2017.7.1 Release Notes File + +- **PR** `#42150`_: (*twangboy*) Fix `unit.modules.test_pip` for Windows + @ *2017-07-19T16:01:17Z* + + * 59e012b485 Merge pull request `#42150`_ from twangboy/win_unit_test_pip + * 4ee24202fc Fix unit tests for test_pip + +- **PR** `#42154`_: (*twangboy*) Fix `unit.modules.test_reg_win` for Windows + @ *2017-07-19T16:00:38Z* + + * ade25c6b34 Merge pull request `#42154`_ from twangboy/win_unit_test_reg + * 00d9a52802 Fix problem with handling REG_QWORD in list values + +- **PR** `#42182`_: (*twangboy*) Fix `unit.modules.test_useradd` for Windows + @ *2017-07-19T15:55:33Z* + + * 07593675e2 Merge pull request `#42182`_ from twangboy/win_unit_test_useradd + * 8260a71c07 Disable tests that require pwd in Windows + +- **PR** `#42364`_: (*twangboy*) Windows Package notes for 2017.7.0 + @ *2017-07-18T19:24:45Z* + + * a175c40c1d Merge pull request `#42364`_ from twangboy/release_notes_2017.7.0 + * 96517d1355 Add note about patched windows packages + +- **PR** `#42361`_: (*Ch3LL*) [2017.7] Update version numbers in doc config for 2017.7.0 release + @ *2017-07-18T19:23:22Z* + + * 4dfe50e558 Merge pull request `#42361`_ from Ch3LL/doc-update-2017.7 + * dc5bb301f7 [2017.7] Update version numbers in doc config for 2017.7.0 release + +- **PR** `#42363`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-18T18:40:48Z* + + - **ISSUE** `#42295`_: (*lubyou*) file.absent fails on windows if the file to be removed has the "readonly" attribute set + | refs: `#42308`_ + - **ISSUE** `#42267`_: (*gzcwnk*) salt-ssh not creating ssh keys automatically as per documentation + | refs: `#42314`_ + - **ISSUE** `#42240`_: (*casselt*) empty_password in user.present always changes password, even with test=True + | refs: `#42289`_ + - **ISSUE** `#42232`_: (*astronouth7303*) Half of dnsutil refers to dig + | refs: `#42235`_ + - **ISSUE** `#42194`_: (*jryberg*) pkg version: latest are now broken, appending -latest to filename + | refs: `#42275`_ + - **ISSUE** `#42152`_: (*dubb-b*) salt-cloud errors on Rackspace driver using -out=yaml + | refs: `#42282`_ + - **ISSUE** `#42137`_: (*kiemlicz*) cmd.run with multiple commands - random order of execution + | refs: `#42181`_ + - **ISSUE** `#42116`_: (*terminalmage*) CLI pillar override regression in 2017.7.0rc1 + | refs: `#42119`_ + - **ISSUE** `#42115`_: (*nomeelnoj*) Installing EPEL repo breaks salt-cloud + | refs: `#42163`_ + - **ISSUE** `#42114`_: (*clallen*) saltenv bug in pillar.get execution module function + | refs: `#42121`_ + - **ISSUE** `#41936`_: (*michaelkarrer81*) git.latest identity does not set the correct user for the private key file on the minion + | refs: `#41945`_ + - **ISSUE** `#41721`_: (*sazaro*) state.sysrc broken when setting the value to YES or NO + | refs: `#42269`_ + - **ISSUE** `#41116`_: (*hrumph*) FAQ has wrong instructions for upgrading Windows minion. + | refs: `#42264`_ + - **ISSUE** `#39365`_: (*dglloyd*) service.running fails if sysv script has no status command and enable: True + | refs: `#39366`_ + - **ISSUE** `#1`_: (*thatch45*) Enable regex on the salt cli + - **PR** `#42353`_: (*terminalmage*) is_windows is a function, not a propery/attribute + - **PR** `#42314`_: (*rallytime*) Add clarification to salt ssh docs about key auto-generation. + - **PR** `#42308`_: (*lubyou*) Force file removal on Windows. Fixes `#42295`_ + - **PR** `#42289`_: (*CorvinM*) Multiple empty_password fixes for state.user + - **PR** `#42282`_: (*rallytime*) Handle libcloud objects that throw RepresenterErrors with --out=yaml + - **PR** `#42275`_: (*terminalmage*) pkg.installed: pack name/version into pkgs argument + - **PR** `#42269`_: (*rallytime*) Add some clarity to "multiple quotes" section of yaml docs + - **PR** `#42264`_: (*rallytime*) Update minion restart section in FAQ doc for windows + - **PR** `#42262`_: (*rallytime*) Back-port `#42224`_ to 2016.11 + - **PR** `#42261`_: (*rallytime*) Some minor doc fixes for dnsutil module so they'll render correctly + - **PR** `#42253`_: (*gtmanfred*) Only use unassociated ips when unable to allocate + - **PR** `#42252`_: (*UtahDave*) simple docstring updates + - **PR** `#42235`_: (*astronouth7303*) Abolish references to `dig` in examples. + - **PR** `#42224`_: (*tdutrion*) Remove duplicate instruction in Openstack Rackspace config example + | refs: `#42262`_ + - **PR** `#42215`_: (*twangboy*) Add missing config to example + - **PR** `#42211`_: (*terminalmage*) Only pass a saltenv in orchestration if one was explicitly passed (2016.11) + - **PR** `#42181`_: (*garethgreenaway*) fixes to state.py for names parameter + - **PR** `#42176`_: (*rallytime*) Back-port `#42109`_ to 2016.11 + - **PR** `#42175`_: (*rallytime*) Back-port `#39366`_ to 2016.11 + - **PR** `#42173`_: (*rallytime*) Back-port `#37424`_ to 2016.11 + - **PR** `#42172`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + - **PR** `#42164`_: (*Ch3LL*) Fix kerberos create_keytab doc + - **PR** `#42163`_: (*vutny*) Fix `#42115`_: parse libcloud "rc" version correctly + - **PR** `#42155`_: (*phsteve*) Fix docs for puppet.plugin_sync + - **PR** `#42142`_: (*Ch3LL*) Update builds available for rc1 + - **PR** `#42141`_: (*rallytime*) Back-port `#42098`_ to 2016.11 + - **PR** `#42140`_: (*rallytime*) Back-port `#42097`_ to 2016.11 + - **PR** `#42123`_: (*vutny*) DOCS: describe importing custom util classes + - **PR** `#42121`_: (*terminalmage*) Fix pillar.get when saltenv is passed + - **PR** `#42119`_: (*terminalmage*) Fix regression in CLI pillar override for salt-call + - **PR** `#42109`_: (*arthurlogilab*) [doc] Update aws.rst - add Debian default username + | refs: `#42176`_ + - **PR** `#42098`_: (*twangboy*) Change repo_ng to repo-ng + | refs: `#42141`_ + - **PR** `#42097`_: (*gtmanfred*) require large timediff for ipv6 warning + | refs: `#42140`_ + - **PR** `#42095`_: (*terminalmage*) Add debug logging to dockerng.login + - **PR** `#42094`_: (*terminalmage*) Prevent command from showing in exception when output_loglevel=quiet + - **PR** `#41945`_: (*garethgreenaway*) Fixes to modules/git.py + - **PR** `#41543`_: (*cri-epita*) Fix user creation with empty password + | refs: `#42289`_ `#42289`_ + - **PR** `#39366`_: (*dglloyd*) Pass sig to service.status in after_toggle + | refs: `#42175`_ + - **PR** `#38965`_: (*toanju*) salt-cloud will use list_floating_ips for OpenStack + | refs: `#42253`_ + - **PR** `#37424`_: (*kojiromike*) Avoid Early Convert ret['comment'] to String + | refs: `#42173`_ + - **PR** `#34280`_: (*kevinanderson1*) salt-cloud will use list_floating_ips for Openstack + | refs: `#38965`_ + * 587138d771 Merge pull request `#42363`_ from rallytime/merge-2017.7 + * 7aa31ff030 Merge branch '2016.11' into '2017.7' + + * b256001760 Merge pull request `#42353`_ from terminalmage/fix-git-test + + * 14cf6ce322 is_windows is a function, not a propery/attribute + + * 866a1febb4 Merge pull request `#42264`_ from rallytime/`fix-41116`_ + + * bd638880e3 Add mono-spacing to salt-minion reference for consistency + + * 30d62f43da Update minion restart section in FAQ doc for windows + + * 9a707088ad Merge pull request `#42275`_ from terminalmage/issue42194 + + * 663874908a pkg.installed: pack name/version into pkgs argument + + * e588f235e0 Merge pull request `#42269`_ from rallytime/`fix-41721`_ + + * f2250d474a Add a note about using different styles of quotes. + + * 38d9b3d553 Add some clarity to "multiple quotes" section of yaml docs + + * 5aaa214a75 Merge pull request `#42282`_ from rallytime/`fix-42152`_ + + * f032223843 Handle libcloud objects that throw RepresenterErrors with --out=yaml + + * fb5697a4bc Merge pull request `#42308`_ from lubyou/42295-fix-file-absent-windows + + * 026ccf401a Force file removal on Windows. Fixes `#42295`_ + + * da2a8a518f Merge pull request `#42314`_ from rallytime/`fix-42267`_ + + * c406046940 Add clarification to salt ssh docs about key auto-generation. + + * acadd54013 Merge pull request `#41945`_ from garethgreenaway/41936_allow_identity_files_with_user + + * 44841e5626 Moving the call to cp.get_file inside the with block to ensure the umask is preserved when we grab the file. + + * f9ba60eed8 Merge pull request `#1`_ from terminalmage/pr-41945 + + * 1b6026177c Restrict set_umask to mkstemp call only + + * 68549f3496 Fixing umask to we can set files as executable. + + * 4949bf3ff3 Updating to swap on the new salt.utils.files.set_umask context_manager + + * 8faa9f6d92 Updating PR with requested changes. + + * 494765e939 Updating the git module to allow an identity file to be used when passing the user parameter + + * f90e04a2bc Merge pull request `#42289`_ from CorvinM/`bp-41543`_ + + * 357dc22f05 Fix user creation with empty password + + * a91a3f81b1 Merge pull request `#42123`_ from vutny/fix-master-utils-import + + * 6bb8b8f98c Add missing doc for ``utils_dirs`` Minion config option + + * f1bc58f6d5 Utils: add example of module import + + * e2aa5114e4 Merge pull request `#42261`_ from rallytime/minor-doc-fix + + * 8c76bbb53d Some minor doc fixes for dnsutil module so they'll render correctly + + * 3e9dfbc9cc Merge pull request `#42262`_ from rallytime/`bp-42224`_ + + * c31ded341c Remove duplicate instruction in Openstack Rackspace config example + + * 7780579c36 Merge pull request `#42181`_ from garethgreenaway/42137_backport_fix_from_2017_7 + + * a34970b45b Back porting the fix for 2017.7 that ensures the order of the names parameter. + + * 72537868a6 Merge pull request `#42253`_ from gtmanfred/2016.11 + + * 53e25760be Only use unassociated ips when unable to allocate + + * b2a4698b5d Merge pull request `#42252`_ from UtahDave/2016.11local + + * e6a9563d47 simple doc updates + + * 781fe13be7 Merge pull request `#42235`_ from astronouth7303/patch-1-2016.3 + + * 4cb51bd03a Make note of dig partial requirement. + + * 08e7d8351a Abolish references to `dig` in examples. + + * 83cbd76f16 Merge pull request `#42215`_ from twangboy/win_iis_docs + + * c07e22041a Add missing config to example + + * 274946ab00 Merge pull request `#42211`_ from terminalmage/issue40928 + + * 22a18fa2ed Only pass a saltenv in orchestration if one was explicitly passed (2016.11) + + * 89261cf06c Merge pull request `#42173`_ from rallytime/`bp-37424`_ + + * 01addb6053 Avoid Early Convert ret['comment'] to String + + * 3b17fb7f83 Merge pull request `#42175`_ from rallytime/`bp-39366`_ + + * 53f7b987e8 Pass sig to service.status in after_toggle + + * ea16f47f0a Merge pull request `#42172`_ from rallytime/merge-2016.11 + + * b1fa332a11 Merge branch '2016.3' into '2016.11' + + * 8fa1fa5bb1 Merge pull request `#42155`_ from phsteve/doc-fix-puppet + + * fb2cb78a31 Fix docs for puppet.plugin_sync so code-block renders properly and sync is spelled consistently + + * 6307b9873f Merge pull request `#42176`_ from rallytime/`bp-42109`_ + + * 686926daf7 Update aws.rst - add Debian default username + + * 28c4e4c3b7 Merge pull request `#42095`_ from terminalmage/docker-login-debugging + + * bd27870a71 Add debug logging to dockerng.login + + * 2b754bc5af Merge pull request `#42119`_ from terminalmage/issue42116 + + * 9a268949e3 Add integration test for 42116 + + * 1bb42bb609 Fix regression when CLI pillar override is used with salt-call + + * 8c0a83cbb5 Merge pull request `#42121`_ from terminalmage/issue42114 + + * d14291267f Fix pillar.get when saltenv is passed + + * 687992c240 Merge pull request `#42094`_ from terminalmage/quiet-exception + + * 47d61f4edf Prevent command from showing in exception when output_loglevel=quiet + + * dad255160c Merge pull request `#42163`_ from vutny/`fix-42115`_ + + * b27b1e340a Fix `#42115`_: parse libcloud "rc" version correctly + + * 2a8ae2b3b6 Merge pull request `#42164`_ from Ch3LL/fix_kerb_doc + + * 7c0fb248ec Fix kerberos create_keytab doc + + * 678d4d4098 Merge pull request `#42141`_ from rallytime/`bp-42098`_ + + * bd80243233 Change repo_ng to repo-ng + + * c8afd7a3c9 Merge pull request `#42140`_ from rallytime/`bp-42097`_ + + * 9c4e132540 Import datetime + + * 1435bf177e require large timediff for ipv6 warning + + * c239664c8b Merge pull request `#42142`_ from Ch3LL/change_builds + + * e1694af39c Update builds available for rc1 + +- **PR** `#42340`_: (*isbm*) Bugfix: Jobs scheduled to run at a future time stay pending for Salt … + @ *2017-07-18T18:13:36Z* + + - **ISSUE** `#1036125`_: (**) + * 55b7a5cb4a Merge pull request `#42340`_ from isbm/isbm-jobs-scheduled-in-a-future-2017.7-bsc1036125 + * 774d204d65 Bugfix: Jobs scheduled to run at a future time stay pending for Salt minions (bsc`#1036125`_) + +- **PR** `#42327`_: (*mirceaulinic*) Default skip_verify to False + @ *2017-07-18T18:04:36Z* + + * e72616c5f1 Merge pull request `#42327`_ from mirceaulinic/patch-10 + * c830573a2c Trailing whitespaces + + * c83e6fc696 Default skip_verify to False + +- **PR** `#42179`_: (*rallytime*) Fix some documentation issues found in jinja filters doc topic + @ *2017-07-18T18:01:57Z* + + - **ISSUE** `#42151`_: (*sjorge*) Doc errors in jinja doc for develop branch + | refs: `#42179`_ `#42179`_ + * ba799b2831 Merge pull request `#42179`_ from rallytime/`fix-42151`_ + * 798d29276e Add note about "to_bytes" jinja filter issues when using yaml_jinja renderer + + * 1bbff572ab Fix some documentation issues found in jinja filters doc topic + +- **PR** `#42087`_: (*abulford*) Make result=true if Docker volume already exists + @ *2017-07-17T18:41:47Z* + + - **ISSUE** `#42076`_: (*abulford*) dockerng.volume_present test looks as though it would cause a change + | refs: `#42086`_ `#42086`_ `#42087`_ `#42087`_ + - **PR** `#42086`_: (*abulford*) Make result=true if Docker volume already exists + | refs: `#42087`_ + * 8dbb93851d Merge pull request `#42087`_ from redmatter/fix-dockerng-volume-present-result-2017.7 + * 2e1dc95500 Make result=true if Docker volume already exists + +- **PR** `#42186`_: (*rallytime*) Use long_range function for IPv6Network hosts() function + @ *2017-07-17T18:39:35Z* + + - **ISSUE** `#42166`_: (*sjorge*) [2017.7.0rc1] jinja filter network_hosts fails on large IPv6 networks + | refs: `#42186`_ + * c84d6db548 Merge pull request `#42186`_ from rallytime/`fix-42166`_ + * b8bcc0d599 Add note to various network_hosts docs about long_run for IPv6 networks + + * 11862743c2 Use long_range function for IPv6Network hosts() function + +- **PR** `#42210`_: (*terminalmage*) Only pass a saltenv in orchestration if one was explicitly passed (2017.7) + @ *2017-07-17T18:22:39Z* + + * e7b79e0fd2 Merge pull request `#42210`_ from terminalmage/issue40928-2017.7 + * 771ade5d73 Only pass a saltenv in orchestration if one was explicitly passed (2017.7) + +- **PR** `#42236`_: (*mirceaulinic*) New option for napalm proxy/minion: provider + @ *2017-07-17T18:19:56Z* + + * 0e49021b0e Merge pull request `#42236`_ from cloudflare/napalm-provider + * 1ac69bd737 Document the provider option and rearrange the doc + + * 4bf4b14161 New option for napalm proxy/minion: provider + +- **PR** `#42257`_: (*twangboy*) Fix `unit.pillar.test_git` for Windows + @ *2017-07-17T17:51:42Z* + + * 3ec5bb1c2f Merge pull request `#42257`_ from twangboy/win_unit_pillar_test_git + * 45be32666a Add error-handling function to shutil.rmtree + +- **PR** `#42258`_: (*twangboy*) Fix `unit.states.test_environ` for Windows + @ *2017-07-17T17:50:38Z* + + * 36395625c2 Merge pull request `#42258`_ from twangboy/win_unit_states_tests_environ + * 55b278c478 Mock the reg.read_value function + +- **PR** `#42265`_: (*rallytime*) Gate boto_elb tests if proper version of moto isn't installed + @ *2017-07-17T17:47:52Z* + + * 894bdd2b19 Merge pull request `#42265`_ from rallytime/gate-moto-version + * 78cdee51d5 Gate boto_elb tests if proper version of moto isn't installed + +- **PR** `#42277`_: (*twangboy*) Fix `unit.states.test_winrepo` for Windows + @ *2017-07-17T17:37:07Z* + + * baf04f2a2d Merge pull request `#42277`_ from twangboy/win_unit_states_test_winrepo + * ed89cd0b93 Use os.sep for path seps + +- **PR** `#42309`_: (*terminalmage*) Change "TBD" in versionadded to "2017.7.0" + @ *2017-07-17T17:11:45Z* + + * be6b211683 Merge pull request `#42309`_ from terminalmage/fix-versionadded + * 603f5b7de6 Change "TBD" in versionadded to "2017.7.0" + +- **PR** `#42206`_: (*rallytime*) [PY3] Fix test that is flaky in Python 3 + | refs: `#42783`_ + @ *2017-07-17T17:09:53Z* + + * acd29f9b38 Merge pull request `#42206`_ from rallytime/fix-flaky-test + * 2be4865f48 [PY3] Fix test that is flaky in Python 3 + +- **PR** `#42126`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-17T17:07:19Z* + + * 8f1cb287cf Merge pull request `#42126`_ from rallytime/merge-2017.7 +* 8b35b367b3 Merge branch '2016.11' into '2017.7' + + +- **PR** `#42078`_: (*damon-atkins*) pkg.install and pkg.remove fix version number input. + @ *2017-07-05T06:04:57Z* + + * 4780d7830a Merge pull request `#42078`_ from damon-atkins/fix_convert_flt_str_version_on_cmd_line + * 09d37dd892 Fix comment typo + + * 7167549425 Handle version=None when converted to a string it becomes 'None' parm should default to empty string rather than None, it would fix better with existing code. + + * 4fb2bb1856 Fix typo + + * cf55c3361c pkg.install and pkg.remove on the command line take number version numbers, store them within a float. However version is a string, to support versions numbers like 1.3.4 + +- **PR** `#42105`_: (*Ch3LL*) Update releasecanddiate doc with new 2017.7.0rc1 Release + @ *2017-07-04T03:14:42Z* + + * 46d575acbc Merge pull request `#42105`_ from Ch3LL/update_rc + * d4e7b91608 Update releasecanddiate doc with new 2017.7.0rc1 Release + +- **PR** `#42099`_: (*rallytime*) Remove references in docs to pip install salt-cloud + @ *2017-07-03T22:13:44Z* + + - **ISSUE** `#41885`_: (*astronouth7303*) Recommended pip installation outdated? + | refs: `#42099`_ + * d38548bbbd Merge pull request `#42099`_ from rallytime/`fix-41885`_ + * c2822e05ad Remove references in docs to pip install salt-cloud + +- **PR** `#42086`_: (*abulford*) Make result=true if Docker volume already exists + | refs: `#42087`_ + @ *2017-07-03T15:48:33Z* + + - **ISSUE** `#42076`_: (*abulford*) dockerng.volume_present test looks as though it would cause a change + | refs: `#42086`_ `#42086`_ `#42087`_ `#42087`_ + * 81d606a8cb Merge pull request `#42086`_ from redmatter/fix-dockerng-volume-present-result + * 8d549685a7 Make result=true if Docker volume already exists + +- **PR** `#42021`_: (*gtmanfred*) Set concurrent to True when running states with sudo + @ *2017-06-30T21:02:15Z* + + - **ISSUE** `#25842`_: (*shikhartanwar*) Running salt-minion as non-root user to execute sudo commands always returns an error + | refs: `#42021`_ + * 7160697123 Merge pull request `#42021`_ from gtmanfred/2016.11 + * 26beb18aa5 Set concurrent to True when running states with sudo + +- **PR** `#42029`_: (*terminalmage*) Mock socket.getaddrinfo in unit.utils.network_test.NetworkTestCase.test_host_to_ips + @ *2017-06-30T20:58:56Z* + + * b784fbbdf8 Merge pull request `#42029`_ from terminalmage/host_to_ips + * 26f848e111 Mock socket.getaddrinfo in unit.utils.network_test.NetworkTestCase.test_host_to_ips + +- **PR** `#42055`_: (*dmurphy18*) Upgrade support for gnupg v2.1 and higher + @ *2017-06-30T20:54:02Z* + + * e067020b9b Merge pull request `#42055`_ from dmurphy18/handle_gnupgv21 + * e20cea6350 Upgrade support for gnupg v2.1 and higher + +- **PR** `#42048`_: (*Ch3LL*) Add initial 2016.11.7 Release Notes + @ *2017-06-30T16:00:05Z* + + * 74ba2abc48 Merge pull request `#42048`_ from Ch3LL/add_11.7 + * 1de5e008a0 Add initial 2016.11.7 Release Notes + + +.. _`#1`: https://github.com/saltstack/salt/issues/1 +.. _`#1036125`: https://github.com/saltstack/salt/issues/1036125 +.. _`#12587`: https://github.com/saltstack/salt/issues/12587 +.. _`#15171`: https://github.com/saltstack/salt/issues/15171 +.. _`#2`: https://github.com/saltstack/salt/issues/2 +.. _`#23516`: https://github.com/saltstack/salt/issues/23516 +.. _`#25842`: https://github.com/saltstack/salt/issues/25842 +.. _`#26995`: https://github.com/saltstack/salt/issues/26995 +.. _`#32400`: https://github.com/saltstack/salt/issues/32400 +.. _`#33708`: https://github.com/saltstack/salt/issues/33708 +.. _`#33806`: https://github.com/saltstack/salt/pull/33806 +.. _`#34245`: https://github.com/saltstack/salt/issues/34245 +.. _`#34280`: https://github.com/saltstack/salt/pull/34280 +.. _`#37312`: https://github.com/saltstack/salt/issues/37312 +.. _`#37424`: https://github.com/saltstack/salt/pull/37424 +.. _`#38063`: https://github.com/saltstack/salt/pull/38063 +.. _`#38839`: https://github.com/saltstack/salt/issues/38839 +.. _`#38878`: https://github.com/saltstack/salt/issues/38878 +.. _`#38898`: https://github.com/saltstack/salt/pull/38898 +.. _`#38965`: https://github.com/saltstack/salt/pull/38965 +.. _`#39365`: https://github.com/saltstack/salt/issues/39365 +.. _`#39366`: https://github.com/saltstack/salt/pull/39366 +.. _`#39516`: https://github.com/saltstack/salt/pull/39516 +.. _`#396`: https://github.com/saltstack/salt/pull/396 +.. _`#39646`: https://github.com/saltstack/salt/pull/39646 +.. _`#39773`: https://github.com/saltstack/salt/pull/39773 +.. _`#40354`: https://github.com/saltstack/salt/issues/40354 +.. _`#40490`: https://github.com/saltstack/salt/issues/40490 +.. _`#41116`: https://github.com/saltstack/salt/issues/41116 +.. _`#41433`: https://github.com/saltstack/salt/issues/41433 +.. _`#41543`: https://github.com/saltstack/salt/pull/41543 +.. _`#41690`: https://github.com/saltstack/salt/pull/41690 +.. _`#41720`: https://github.com/saltstack/salt/issues/41720 +.. _`#41721`: https://github.com/saltstack/salt/issues/41721 +.. _`#41770`: https://github.com/saltstack/salt/issues/41770 +.. _`#41885`: https://github.com/saltstack/salt/issues/41885 +.. _`#41936`: https://github.com/saltstack/salt/issues/41936 +.. _`#41945`: https://github.com/saltstack/salt/pull/41945 +.. _`#41949`: https://github.com/saltstack/salt/issues/41949 +.. _`#41955`: https://github.com/saltstack/salt/issues/41955 +.. _`#41968`: https://github.com/saltstack/salt/pull/41968 +.. _`#41973`: https://github.com/saltstack/salt/pull/41973 +.. _`#41976`: https://github.com/saltstack/salt/issues/41976 +.. _`#41977`: https://github.com/saltstack/salt/pull/41977 +.. _`#41982`: https://github.com/saltstack/salt/issues/41982 +.. _`#41988`: https://github.com/saltstack/salt/pull/41988 +.. _`#41994`: https://github.com/saltstack/salt/pull/41994 +.. _`#42006`: https://github.com/saltstack/salt/pull/42006 +.. _`#42021`: https://github.com/saltstack/salt/pull/42021 +.. _`#42027`: https://github.com/saltstack/salt/pull/42027 +.. _`#42029`: https://github.com/saltstack/salt/pull/42029 +.. _`#42031`: https://github.com/saltstack/salt/pull/42031 +.. _`#42041`: https://github.com/saltstack/salt/issues/42041 +.. _`#42045`: https://github.com/saltstack/salt/pull/42045 +.. _`#42048`: https://github.com/saltstack/salt/pull/42048 +.. _`#42055`: https://github.com/saltstack/salt/pull/42055 +.. _`#42067`: https://github.com/saltstack/salt/pull/42067 +.. _`#42076`: https://github.com/saltstack/salt/issues/42076 +.. _`#42077`: https://github.com/saltstack/salt/pull/42077 +.. _`#42078`: https://github.com/saltstack/salt/pull/42078 +.. _`#42086`: https://github.com/saltstack/salt/pull/42086 +.. _`#42087`: https://github.com/saltstack/salt/pull/42087 +.. _`#42094`: https://github.com/saltstack/salt/pull/42094 +.. _`#42095`: https://github.com/saltstack/salt/pull/42095 +.. _`#42097`: https://github.com/saltstack/salt/pull/42097 +.. _`#42098`: https://github.com/saltstack/salt/pull/42098 +.. _`#42099`: https://github.com/saltstack/salt/pull/42099 +.. _`#42105`: https://github.com/saltstack/salt/pull/42105 +.. _`#42107`: https://github.com/saltstack/salt/pull/42107 +.. _`#42109`: https://github.com/saltstack/salt/pull/42109 +.. _`#42114`: https://github.com/saltstack/salt/issues/42114 +.. _`#42115`: https://github.com/saltstack/salt/issues/42115 +.. _`#42116`: https://github.com/saltstack/salt/issues/42116 +.. _`#42119`: https://github.com/saltstack/salt/pull/42119 +.. _`#42121`: https://github.com/saltstack/salt/pull/42121 +.. _`#42123`: https://github.com/saltstack/salt/pull/42123 +.. _`#42126`: https://github.com/saltstack/salt/pull/42126 +.. _`#42137`: https://github.com/saltstack/salt/issues/42137 +.. _`#42140`: https://github.com/saltstack/salt/pull/42140 +.. _`#42141`: https://github.com/saltstack/salt/pull/42141 +.. _`#42142`: https://github.com/saltstack/salt/pull/42142 +.. _`#42150`: https://github.com/saltstack/salt/pull/42150 +.. _`#42151`: https://github.com/saltstack/salt/issues/42151 +.. _`#42152`: https://github.com/saltstack/salt/issues/42152 +.. _`#42154`: https://github.com/saltstack/salt/pull/42154 +.. _`#42155`: https://github.com/saltstack/salt/pull/42155 +.. _`#42163`: https://github.com/saltstack/salt/pull/42163 +.. _`#42164`: https://github.com/saltstack/salt/pull/42164 +.. _`#42166`: https://github.com/saltstack/salt/issues/42166 +.. _`#42172`: https://github.com/saltstack/salt/pull/42172 +.. _`#42173`: https://github.com/saltstack/salt/pull/42173 +.. _`#42174`: https://github.com/saltstack/salt/pull/42174 +.. _`#42175`: https://github.com/saltstack/salt/pull/42175 +.. _`#42176`: https://github.com/saltstack/salt/pull/42176 +.. _`#42179`: https://github.com/saltstack/salt/pull/42179 +.. _`#42180`: https://github.com/saltstack/salt/pull/42180 +.. _`#42181`: https://github.com/saltstack/salt/pull/42181 +.. _`#42182`: https://github.com/saltstack/salt/pull/42182 +.. _`#42186`: https://github.com/saltstack/salt/pull/42186 +.. _`#42194`: https://github.com/saltstack/salt/issues/42194 +.. _`#42198`: https://github.com/saltstack/salt/issues/42198 +.. _`#42200`: https://github.com/saltstack/salt/pull/42200 +.. _`#42206`: https://github.com/saltstack/salt/pull/42206 +.. _`#42210`: https://github.com/saltstack/salt/pull/42210 +.. _`#42211`: https://github.com/saltstack/salt/pull/42211 +.. _`#42215`: https://github.com/saltstack/salt/pull/42215 +.. _`#42224`: https://github.com/saltstack/salt/pull/42224 +.. _`#42232`: https://github.com/saltstack/salt/issues/42232 +.. _`#42235`: https://github.com/saltstack/salt/pull/42235 +.. _`#42236`: https://github.com/saltstack/salt/pull/42236 +.. _`#42240`: https://github.com/saltstack/salt/issues/42240 +.. _`#42251`: https://github.com/saltstack/salt/pull/42251 +.. _`#42252`: https://github.com/saltstack/salt/pull/42252 +.. _`#42253`: https://github.com/saltstack/salt/pull/42253 +.. _`#42255`: https://github.com/saltstack/salt/pull/42255 +.. _`#42257`: https://github.com/saltstack/salt/pull/42257 +.. _`#42258`: https://github.com/saltstack/salt/pull/42258 +.. _`#42261`: https://github.com/saltstack/salt/pull/42261 +.. _`#42262`: https://github.com/saltstack/salt/pull/42262 +.. _`#42264`: https://github.com/saltstack/salt/pull/42264 +.. _`#42265`: https://github.com/saltstack/salt/pull/42265 +.. _`#42266`: https://github.com/saltstack/salt/pull/42266 +.. _`#42267`: https://github.com/saltstack/salt/issues/42267 +.. _`#42269`: https://github.com/saltstack/salt/pull/42269 +.. _`#42270`: https://github.com/saltstack/salt/issues/42270 +.. _`#42275`: https://github.com/saltstack/salt/pull/42275 +.. _`#42277`: https://github.com/saltstack/salt/pull/42277 +.. _`#42279`: https://github.com/saltstack/salt/issues/42279 +.. _`#42282`: https://github.com/saltstack/salt/pull/42282 +.. _`#42289`: https://github.com/saltstack/salt/pull/42289 +.. _`#42290`: https://github.com/saltstack/salt/pull/42290 +.. _`#42291`: https://github.com/saltstack/salt/pull/42291 +.. _`#42295`: https://github.com/saltstack/salt/issues/42295 +.. _`#42308`: https://github.com/saltstack/salt/pull/42308 +.. _`#42309`: https://github.com/saltstack/salt/pull/42309 +.. _`#42314`: https://github.com/saltstack/salt/pull/42314 +.. _`#42319`: https://github.com/saltstack/salt/pull/42319 +.. _`#42327`: https://github.com/saltstack/salt/pull/42327 +.. _`#42329`: https://github.com/saltstack/salt/issues/42329 +.. _`#42333`: https://github.com/saltstack/salt/issues/42333 +.. _`#42339`: https://github.com/saltstack/salt/pull/42339 +.. _`#42340`: https://github.com/saltstack/salt/pull/42340 +.. _`#42347`: https://github.com/saltstack/salt/pull/42347 +.. _`#42350`: https://github.com/saltstack/salt/pull/42350 +.. _`#42352`: https://github.com/saltstack/salt/pull/42352 +.. _`#42353`: https://github.com/saltstack/salt/pull/42353 +.. _`#42356`: https://github.com/saltstack/salt/pull/42356 +.. _`#42357`: https://github.com/saltstack/salt/issues/42357 +.. _`#42359`: https://github.com/saltstack/salt/pull/42359 +.. _`#42360`: https://github.com/saltstack/salt/pull/42360 +.. _`#42361`: https://github.com/saltstack/salt/pull/42361 +.. _`#42363`: https://github.com/saltstack/salt/pull/42363 +.. _`#42364`: https://github.com/saltstack/salt/pull/42364 +.. _`#42366`: https://github.com/saltstack/salt/pull/42366 +.. _`#42368`: https://github.com/saltstack/salt/pull/42368 +.. _`#42370`: https://github.com/saltstack/salt/pull/42370 +.. _`#42371`: https://github.com/saltstack/salt/issues/42371 +.. _`#42373`: https://github.com/saltstack/salt/pull/42373 +.. _`#42374`: https://github.com/saltstack/salt/issues/42374 +.. _`#42375`: https://github.com/saltstack/salt/issues/42375 +.. _`#42381`: https://github.com/saltstack/salt/issues/42381 +.. _`#42387`: https://github.com/saltstack/salt/pull/42387 +.. _`#42388`: https://github.com/saltstack/salt/pull/42388 +.. _`#42399`: https://github.com/saltstack/salt/pull/42399 +.. _`#42400`: https://github.com/saltstack/salt/issues/42400 +.. _`#42403`: https://github.com/saltstack/salt/issues/42403 +.. _`#42404`: https://github.com/saltstack/salt/issues/42404 +.. _`#42405`: https://github.com/saltstack/salt/issues/42405 +.. _`#42408`: https://github.com/saltstack/salt/pull/42408 +.. _`#42409`: https://github.com/saltstack/salt/pull/42409 +.. _`#42411`: https://github.com/saltstack/salt/pull/42411 +.. _`#42413`: https://github.com/saltstack/salt/issues/42413 +.. _`#42414`: https://github.com/saltstack/salt/pull/42414 +.. _`#42417`: https://github.com/saltstack/salt/issues/42417 +.. _`#42421`: https://github.com/saltstack/salt/issues/42421 +.. _`#42424`: https://github.com/saltstack/salt/pull/42424 +.. _`#42425`: https://github.com/saltstack/salt/pull/42425 +.. _`#42427`: https://github.com/saltstack/salt/issues/42427 +.. _`#42433`: https://github.com/saltstack/salt/pull/42433 +.. _`#42435`: https://github.com/saltstack/salt/pull/42435 +.. _`#42436`: https://github.com/saltstack/salt/pull/42436 +.. _`#42443`: https://github.com/saltstack/salt/pull/42443 +.. _`#42444`: https://github.com/saltstack/salt/pull/42444 +.. _`#42452`: https://github.com/saltstack/salt/pull/42452 +.. _`#42453`: https://github.com/saltstack/salt/pull/42453 +.. _`#42454`: https://github.com/saltstack/salt/pull/42454 +.. _`#42456`: https://github.com/saltstack/salt/issues/42456 +.. _`#42459`: https://github.com/saltstack/salt/issues/42459 +.. _`#42461`: https://github.com/saltstack/salt/pull/42461 +.. _`#42464`: https://github.com/saltstack/salt/pull/42464 +.. _`#42465`: https://github.com/saltstack/salt/pull/42465 +.. _`#42474`: https://github.com/saltstack/salt/pull/42474 +.. _`#42477`: https://github.com/saltstack/salt/issues/42477 +.. _`#42479`: https://github.com/saltstack/salt/pull/42479 +.. _`#42481`: https://github.com/saltstack/salt/pull/42481 +.. _`#42484`: https://github.com/saltstack/salt/pull/42484 +.. _`#42502`: https://github.com/saltstack/salt/pull/42502 +.. _`#42505`: https://github.com/saltstack/salt/issues/42505 +.. _`#42506`: https://github.com/saltstack/salt/pull/42506 +.. _`#42509`: https://github.com/saltstack/salt/pull/42509 +.. _`#42514`: https://github.com/saltstack/salt/issues/42514 +.. _`#42515`: https://github.com/saltstack/salt/pull/42515 +.. _`#42516`: https://github.com/saltstack/salt/pull/42516 +.. _`#42521`: https://github.com/saltstack/salt/issues/42521 +.. _`#42523`: https://github.com/saltstack/salt/pull/42523 +.. _`#42524`: https://github.com/saltstack/salt/pull/42524 +.. _`#42527`: https://github.com/saltstack/salt/pull/42527 +.. _`#42528`: https://github.com/saltstack/salt/pull/42528 +.. _`#42529`: https://github.com/saltstack/salt/pull/42529 +.. _`#42534`: https://github.com/saltstack/salt/pull/42534 +.. _`#42538`: https://github.com/saltstack/salt/issues/42538 +.. _`#42541`: https://github.com/saltstack/salt/pull/42541 +.. _`#42545`: https://github.com/saltstack/salt/issues/42545 +.. _`#42547`: https://github.com/saltstack/salt/pull/42547 +.. _`#42551`: https://github.com/saltstack/salt/pull/42551 +.. _`#42552`: https://github.com/saltstack/salt/pull/42552 +.. _`#42555`: https://github.com/saltstack/salt/pull/42555 +.. _`#42557`: https://github.com/saltstack/salt/pull/42557 +.. _`#42567`: https://github.com/saltstack/salt/pull/42567 +.. _`#42571`: https://github.com/saltstack/salt/pull/42571 +.. _`#42573`: https://github.com/saltstack/salt/pull/42573 +.. _`#42574`: https://github.com/saltstack/salt/pull/42574 +.. _`#42575`: https://github.com/saltstack/salt/pull/42575 +.. _`#42577`: https://github.com/saltstack/salt/pull/42577 +.. _`#42586`: https://github.com/saltstack/salt/pull/42586 +.. _`#42588`: https://github.com/saltstack/salt/issues/42588 +.. _`#42589`: https://github.com/saltstack/salt/pull/42589 +.. _`#42600`: https://github.com/saltstack/salt/issues/42600 +.. _`#42601`: https://github.com/saltstack/salt/pull/42601 +.. _`#42602`: https://github.com/saltstack/salt/pull/42602 +.. _`#42603`: https://github.com/saltstack/salt/pull/42603 +.. _`#42611`: https://github.com/saltstack/salt/issues/42611 +.. _`#42612`: https://github.com/saltstack/salt/pull/42612 +.. _`#42616`: https://github.com/saltstack/salt/pull/42616 +.. _`#42618`: https://github.com/saltstack/salt/pull/42618 +.. _`#42619`: https://github.com/saltstack/salt/pull/42619 +.. _`#42621`: https://github.com/saltstack/salt/pull/42621 +.. _`#42623`: https://github.com/saltstack/salt/pull/42623 +.. _`#42625`: https://github.com/saltstack/salt/pull/42625 +.. _`#42627`: https://github.com/saltstack/salt/issues/42627 +.. _`#42629`: https://github.com/saltstack/salt/pull/42629 +.. _`#42639`: https://github.com/saltstack/salt/issues/42639 +.. _`#42642`: https://github.com/saltstack/salt/issues/42642 +.. _`#42644`: https://github.com/saltstack/salt/issues/42644 +.. _`#42646`: https://github.com/saltstack/salt/issues/42646 +.. _`#42649`: https://github.com/saltstack/salt/issues/42649 +.. _`#42651`: https://github.com/saltstack/salt/pull/42651 +.. _`#42654`: https://github.com/saltstack/salt/pull/42654 +.. _`#42655`: https://github.com/saltstack/salt/pull/42655 +.. _`#42657`: https://github.com/saltstack/salt/pull/42657 +.. _`#42663`: https://github.com/saltstack/salt/pull/42663 +.. _`#42668`: https://github.com/saltstack/salt/issues/42668 +.. _`#42669`: https://github.com/saltstack/salt/pull/42669 +.. _`#42670`: https://github.com/saltstack/salt/pull/42670 +.. _`#42678`: https://github.com/saltstack/salt/pull/42678 +.. _`#42679`: https://github.com/saltstack/salt/pull/42679 +.. _`#42683`: https://github.com/saltstack/salt/issues/42683 +.. _`#42686`: https://github.com/saltstack/salt/issues/42686 +.. _`#42688`: https://github.com/saltstack/salt/issues/42688 +.. _`#42689`: https://github.com/saltstack/salt/pull/42689 +.. _`#42690`: https://github.com/saltstack/salt/issues/42690 +.. _`#42693`: https://github.com/saltstack/salt/pull/42693 +.. _`#42694`: https://github.com/saltstack/salt/pull/42694 +.. _`#42697`: https://github.com/saltstack/salt/issues/42697 +.. _`#42704`: https://github.com/saltstack/salt/pull/42704 +.. _`#42705`: https://github.com/saltstack/salt/issues/42705 +.. _`#42707`: https://github.com/saltstack/salt/issues/42707 +.. _`#42708`: https://github.com/saltstack/salt/pull/42708 +.. _`#42709`: https://github.com/saltstack/salt/pull/42709 +.. _`#42710`: https://github.com/saltstack/salt/pull/42710 +.. _`#42712`: https://github.com/saltstack/salt/pull/42712 +.. _`#42714`: https://github.com/saltstack/salt/pull/42714 +.. _`#42721`: https://github.com/saltstack/salt/pull/42721 +.. _`#42731`: https://github.com/saltstack/salt/issues/42731 +.. _`#42741`: https://github.com/saltstack/salt/issues/42741 +.. _`#42743`: https://github.com/saltstack/salt/pull/42743 +.. _`#42744`: https://github.com/saltstack/salt/pull/42744 +.. _`#42745`: https://github.com/saltstack/salt/pull/42745 +.. _`#42747`: https://github.com/saltstack/salt/issues/42747 +.. _`#42748`: https://github.com/saltstack/salt/pull/42748 +.. _`#42753`: https://github.com/saltstack/salt/issues/42753 +.. _`#42760`: https://github.com/saltstack/salt/pull/42760 +.. _`#42764`: https://github.com/saltstack/salt/pull/42764 +.. _`#42768`: https://github.com/saltstack/salt/pull/42768 +.. _`#42769`: https://github.com/saltstack/salt/pull/42769 +.. _`#42770`: https://github.com/saltstack/salt/pull/42770 +.. _`#42774`: https://github.com/saltstack/salt/issues/42774 +.. _`#42778`: https://github.com/saltstack/salt/pull/42778 +.. _`#42782`: https://github.com/saltstack/salt/pull/42782 +.. _`#42783`: https://github.com/saltstack/salt/pull/42783 +.. _`#42784`: https://github.com/saltstack/salt/pull/42784 +.. _`#42786`: https://github.com/saltstack/salt/pull/42786 +.. _`#42788`: https://github.com/saltstack/salt/pull/42788 +.. _`#42794`: https://github.com/saltstack/salt/pull/42794 +.. _`#42795`: https://github.com/saltstack/salt/pull/42795 +.. _`#42798`: https://github.com/saltstack/salt/pull/42798 +.. _`#42800`: https://github.com/saltstack/salt/pull/42800 +.. _`#42803`: https://github.com/saltstack/salt/issues/42803 +.. _`#42804`: https://github.com/saltstack/salt/pull/42804 +.. _`#42805`: https://github.com/saltstack/salt/pull/42805 +.. _`#42806`: https://github.com/saltstack/salt/pull/42806 +.. _`#42807`: https://github.com/saltstack/salt/pull/42807 +.. _`#42808`: https://github.com/saltstack/salt/pull/42808 +.. _`#42810`: https://github.com/saltstack/salt/pull/42810 +.. _`#42812`: https://github.com/saltstack/salt/pull/42812 +.. _`#42818`: https://github.com/saltstack/salt/issues/42818 +.. _`#42826`: https://github.com/saltstack/salt/pull/42826 +.. _`#42829`: https://github.com/saltstack/salt/pull/42829 +.. _`#42835`: https://github.com/saltstack/salt/pull/42835 +.. _`#42836`: https://github.com/saltstack/salt/pull/42836 +.. _`#42838`: https://github.com/saltstack/salt/pull/42838 +.. _`#42841`: https://github.com/saltstack/salt/pull/42841 +.. _`#42842`: https://github.com/saltstack/salt/issues/42842 +.. _`#42843`: https://github.com/saltstack/salt/issues/42843 +.. _`#42845`: https://github.com/saltstack/salt/pull/42845 +.. _`#42848`: https://github.com/saltstack/salt/pull/42848 +.. _`#42851`: https://github.com/saltstack/salt/pull/42851 +.. _`#42855`: https://github.com/saltstack/salt/pull/42855 +.. _`#42856`: https://github.com/saltstack/salt/pull/42856 +.. _`#42857`: https://github.com/saltstack/salt/pull/42857 +.. _`#42859`: https://github.com/saltstack/salt/pull/42859 +.. _`#42860`: https://github.com/saltstack/salt/pull/42860 +.. _`#42861`: https://github.com/saltstack/salt/pull/42861 +.. _`#42864`: https://github.com/saltstack/salt/pull/42864 +.. _`#42866`: https://github.com/saltstack/salt/pull/42866 +.. _`#42868`: https://github.com/saltstack/salt/pull/42868 +.. _`#42869`: https://github.com/saltstack/salt/issues/42869 +.. _`#42870`: https://github.com/saltstack/salt/issues/42870 +.. _`#42871`: https://github.com/saltstack/salt/pull/42871 +.. _`#42873`: https://github.com/saltstack/salt/issues/42873 +.. _`#42877`: https://github.com/saltstack/salt/pull/42877 +.. _`#42881`: https://github.com/saltstack/salt/pull/42881 +.. _`#42882`: https://github.com/saltstack/salt/pull/42882 +.. _`#42883`: https://github.com/saltstack/salt/pull/42883 +.. _`#42884`: https://github.com/saltstack/salt/pull/42884 +.. _`#42885`: https://github.com/saltstack/salt/pull/42885 +.. _`#42886`: https://github.com/saltstack/salt/pull/42886 +.. _`#42887`: https://github.com/saltstack/salt/pull/42887 +.. _`#42889`: https://github.com/saltstack/salt/pull/42889 +.. _`#42890`: https://github.com/saltstack/salt/pull/42890 +.. _`#42898`: https://github.com/saltstack/salt/pull/42898 +.. _`#42911`: https://github.com/saltstack/salt/pull/42911 +.. _`#42913`: https://github.com/saltstack/salt/pull/42913 +.. _`#42918`: https://github.com/saltstack/salt/pull/42918 +.. _`#42919`: https://github.com/saltstack/salt/pull/42919 +.. _`#42920`: https://github.com/saltstack/salt/pull/42920 +.. _`#42925`: https://github.com/saltstack/salt/pull/42925 +.. _`#42933`: https://github.com/saltstack/salt/pull/42933 +.. _`#42936`: https://github.com/saltstack/salt/issues/42936 +.. _`#42940`: https://github.com/saltstack/salt/pull/42940 +.. _`#42941`: https://github.com/saltstack/salt/issues/42941 +.. _`#42942`: https://github.com/saltstack/salt/pull/42942 +.. _`#42943`: https://github.com/saltstack/salt/issues/42943 +.. _`#42944`: https://github.com/saltstack/salt/pull/42944 +.. _`#42945`: https://github.com/saltstack/salt/pull/42945 +.. _`#42946`: https://github.com/saltstack/salt/pull/42946 +.. _`#42948`: https://github.com/saltstack/salt/pull/42948 +.. _`#42949`: https://github.com/saltstack/salt/pull/42949 +.. _`#42950`: https://github.com/saltstack/salt/pull/42950 +.. _`#42951`: https://github.com/saltstack/salt/pull/42951 +.. _`#42952`: https://github.com/saltstack/salt/pull/42952 +.. _`#42953`: https://github.com/saltstack/salt/pull/42953 +.. _`#42954`: https://github.com/saltstack/salt/pull/42954 +.. _`#42958`: https://github.com/saltstack/salt/pull/42958 +.. _`#42959`: https://github.com/saltstack/salt/pull/42959 +.. _`#42962`: https://github.com/saltstack/salt/pull/42962 +.. _`#42963`: https://github.com/saltstack/salt/pull/42963 +.. _`#42964`: https://github.com/saltstack/salt/pull/42964 +.. _`#42967`: https://github.com/saltstack/salt/pull/42967 +.. _`#42968`: https://github.com/saltstack/salt/pull/42968 +.. _`#42969`: https://github.com/saltstack/salt/pull/42969 +.. _`#42985`: https://github.com/saltstack/salt/pull/42985 +.. _`#42986`: https://github.com/saltstack/salt/pull/42986 +.. _`#42988`: https://github.com/saltstack/salt/pull/42988 +.. _`#42989`: https://github.com/saltstack/salt/issues/42989 +.. _`#42992`: https://github.com/saltstack/salt/issues/42992 +.. _`#42993`: https://github.com/saltstack/salt/pull/42993 +.. _`#42995`: https://github.com/saltstack/salt/pull/42995 +.. _`#42996`: https://github.com/saltstack/salt/pull/42996 +.. _`#42997`: https://github.com/saltstack/salt/pull/42997 +.. _`#42999`: https://github.com/saltstack/salt/pull/42999 +.. _`#43002`: https://github.com/saltstack/salt/pull/43002 +.. _`#43006`: https://github.com/saltstack/salt/pull/43006 +.. _`#43008`: https://github.com/saltstack/salt/issues/43008 +.. _`#43009`: https://github.com/saltstack/salt/pull/43009 +.. _`#43010`: https://github.com/saltstack/salt/pull/43010 +.. _`#43014`: https://github.com/saltstack/salt/pull/43014 +.. _`#43016`: https://github.com/saltstack/salt/pull/43016 +.. _`#43019`: https://github.com/saltstack/salt/pull/43019 +.. _`#43020`: https://github.com/saltstack/salt/pull/43020 +.. _`#43021`: https://github.com/saltstack/salt/pull/43021 +.. _`#43023`: https://github.com/saltstack/salt/pull/43023 +.. _`#43024`: https://github.com/saltstack/salt/pull/43024 +.. _`#43026`: https://github.com/saltstack/salt/pull/43026 +.. _`#43027`: https://github.com/saltstack/salt/pull/43027 +.. _`#43029`: https://github.com/saltstack/salt/pull/43029 +.. _`#43030`: https://github.com/saltstack/salt/pull/43030 +.. _`#43031`: https://github.com/saltstack/salt/pull/43031 +.. _`#43032`: https://github.com/saltstack/salt/pull/43032 +.. _`#43033`: https://github.com/saltstack/salt/pull/43033 +.. _`#43034`: https://github.com/saltstack/salt/pull/43034 +.. _`#43035`: https://github.com/saltstack/salt/pull/43035 +.. _`#43036`: https://github.com/saltstack/salt/issues/43036 +.. _`#43037`: https://github.com/saltstack/salt/pull/43037 +.. _`#43038`: https://github.com/saltstack/salt/pull/43038 +.. _`#43039`: https://github.com/saltstack/salt/pull/43039 +.. _`#43040`: https://github.com/saltstack/salt/issues/43040 +.. _`#43041`: https://github.com/saltstack/salt/pull/43041 +.. _`#43043`: https://github.com/saltstack/salt/issues/43043 +.. _`#43048`: https://github.com/saltstack/salt/pull/43048 +.. _`#43051`: https://github.com/saltstack/salt/pull/43051 +.. _`#43054`: https://github.com/saltstack/salt/pull/43054 +.. _`#43056`: https://github.com/saltstack/salt/pull/43056 +.. _`#43058`: https://github.com/saltstack/salt/pull/43058 +.. _`#43060`: https://github.com/saltstack/salt/pull/43060 +.. _`#43061`: https://github.com/saltstack/salt/pull/43061 +.. _`#43064`: https://github.com/saltstack/salt/pull/43064 +.. _`#43068`: https://github.com/saltstack/salt/pull/43068 +.. _`#43073`: https://github.com/saltstack/salt/pull/43073 +.. _`#43077`: https://github.com/saltstack/salt/issues/43077 +.. _`#43085`: https://github.com/saltstack/salt/issues/43085 +.. _`#43087`: https://github.com/saltstack/salt/pull/43087 +.. _`#43088`: https://github.com/saltstack/salt/pull/43088 +.. _`#43091`: https://github.com/saltstack/salt/pull/43091 +.. _`#43092`: https://github.com/saltstack/salt/pull/43092 +.. _`#43093`: https://github.com/saltstack/salt/pull/43093 +.. _`#43097`: https://github.com/saltstack/salt/pull/43097 +.. _`#43100`: https://github.com/saltstack/salt/pull/43100 +.. _`#43101`: https://github.com/saltstack/salt/issues/43101 +.. _`#43103`: https://github.com/saltstack/salt/pull/43103 +.. _`#43107`: https://github.com/saltstack/salt/pull/43107 +.. _`#43108`: https://github.com/saltstack/salt/pull/43108 +.. _`#43110`: https://github.com/saltstack/salt/issues/43110 +.. _`#43115`: https://github.com/saltstack/salt/pull/43115 +.. _`#43116`: https://github.com/saltstack/salt/pull/43116 +.. _`#43123`: https://github.com/saltstack/salt/pull/43123 +.. _`#43142`: https://github.com/saltstack/salt/pull/43142 +.. _`#43143`: https://github.com/saltstack/salt/issues/43143 +.. _`#43146`: https://github.com/saltstack/salt/pull/43146 +.. _`#43151`: https://github.com/saltstack/salt/pull/43151 +.. _`#43155`: https://github.com/saltstack/salt/pull/43155 +.. _`#43156`: https://github.com/saltstack/salt/pull/43156 +.. _`#43162`: https://github.com/saltstack/salt/issues/43162 +.. _`#43165`: https://github.com/saltstack/salt/pull/43165 +.. _`#43166`: https://github.com/saltstack/salt/pull/43166 +.. _`#43168`: https://github.com/saltstack/salt/pull/43168 +.. _`#43170`: https://github.com/saltstack/salt/pull/43170 +.. _`#43171`: https://github.com/saltstack/salt/pull/43171 +.. _`#43172`: https://github.com/saltstack/salt/pull/43172 +.. _`#43173`: https://github.com/saltstack/salt/pull/43173 +.. _`#43178`: https://github.com/saltstack/salt/pull/43178 +.. _`#43179`: https://github.com/saltstack/salt/pull/43179 +.. _`#43184`: https://github.com/saltstack/salt/pull/43184 +.. _`#43193`: https://github.com/saltstack/salt/pull/43193 +.. _`#43196`: https://github.com/saltstack/salt/pull/43196 +.. _`#43198`: https://github.com/saltstack/salt/issues/43198 +.. _`#43199`: https://github.com/saltstack/salt/pull/43199 +.. _`#43201`: https://github.com/saltstack/salt/pull/43201 +.. _`#43202`: https://github.com/saltstack/salt/pull/43202 +.. _`#43217`: https://github.com/saltstack/salt/pull/43217 +.. _`#43226`: https://github.com/saltstack/salt/pull/43226 +.. _`#43227`: https://github.com/saltstack/salt/pull/43227 +.. _`#43228`: https://github.com/saltstack/salt/pull/43228 +.. _`#43229`: https://github.com/saltstack/salt/pull/43229 +.. _`#43241`: https://github.com/saltstack/salt/issues/43241 +.. _`#43251`: https://github.com/saltstack/salt/pull/43251 +.. _`#43254`: https://github.com/saltstack/salt/pull/43254 +.. _`#43255`: https://github.com/saltstack/salt/pull/43255 +.. _`#43256`: https://github.com/saltstack/salt/pull/43256 +.. _`#43259`: https://github.com/saltstack/salt/issues/43259 +.. _`#43266`: https://github.com/saltstack/salt/pull/43266 +.. _`#43283`: https://github.com/saltstack/salt/pull/43283 +.. _`#43315`: https://github.com/saltstack/salt/pull/43315 +.. _`#43330`: https://github.com/saltstack/salt/pull/43330 +.. _`#43333`: https://github.com/saltstack/salt/pull/43333 +.. _`#43377`: https://github.com/saltstack/salt/pull/43377 +.. _`#43421`: https://github.com/saltstack/salt/pull/43421 +.. _`#43440`: https://github.com/saltstack/salt/pull/43440 +.. _`#43447`: https://github.com/saltstack/salt/issues/43447 +.. _`#43509`: https://github.com/saltstack/salt/pull/43509 +.. _`#43526`: https://github.com/saltstack/salt/pull/43526 +.. _`#43551`: https://github.com/saltstack/salt/pull/43551 +.. _`#43585`: https://github.com/saltstack/salt/pull/43585 +.. _`#43586`: https://github.com/saltstack/salt/pull/43586 +.. _`#475`: https://github.com/saltstack/salt/issues/475 +.. _`#480`: https://github.com/saltstack/salt/issues/480 +.. _`#495`: https://github.com/saltstack/salt/issues/495 +.. _`bp-37424`: https://github.com/saltstack/salt/pull/37424 +.. _`bp-39366`: https://github.com/saltstack/salt/pull/39366 +.. _`bp-41543`: https://github.com/saltstack/salt/pull/41543 +.. _`bp-41690`: https://github.com/saltstack/salt/pull/41690 +.. _`bp-42067`: https://github.com/saltstack/salt/pull/42067 +.. _`bp-42097`: https://github.com/saltstack/salt/pull/42097 +.. _`bp-42098`: https://github.com/saltstack/salt/pull/42098 +.. _`bp-42109`: https://github.com/saltstack/salt/pull/42109 +.. _`bp-42174`: https://github.com/saltstack/salt/pull/42174 +.. _`bp-42224`: https://github.com/saltstack/salt/pull/42224 +.. _`bp-42433`: https://github.com/saltstack/salt/pull/42433 +.. _`bp-42547`: https://github.com/saltstack/salt/pull/42547 +.. _`bp-42552`: https://github.com/saltstack/salt/pull/42552 +.. _`bp-42589`: https://github.com/saltstack/salt/pull/42589 +.. _`bp-42651`: https://github.com/saltstack/salt/pull/42651 +.. _`bp-42744`: https://github.com/saltstack/salt/pull/42744 +.. _`bp-42760`: https://github.com/saltstack/salt/pull/42760 +.. _`bp-42784`: https://github.com/saltstack/salt/pull/42784 +.. _`bp-42848`: https://github.com/saltstack/salt/pull/42848 +.. _`bp-42871`: https://github.com/saltstack/salt/pull/42871 +.. _`bp-42883`: https://github.com/saltstack/salt/pull/42883 +.. _`bp-42988`: https://github.com/saltstack/salt/pull/42988 +.. _`bp-43002`: https://github.com/saltstack/salt/pull/43002 +.. _`bp-43020`: https://github.com/saltstack/salt/pull/43020 +.. _`bp-43031`: https://github.com/saltstack/salt/pull/43031 +.. _`bp-43041`: https://github.com/saltstack/salt/pull/43041 +.. _`bp-43068`: https://github.com/saltstack/salt/pull/43068 +.. _`bp-43116`: https://github.com/saltstack/salt/pull/43116 +.. _`bp-43193`: https://github.com/saltstack/salt/pull/43193 +.. _`bp-43283`: https://github.com/saltstack/salt/pull/43283 +.. _`bp-43330`: https://github.com/saltstack/salt/pull/43330 +.. _`bp-43333`: https://github.com/saltstack/salt/pull/43333 +.. _`bp-43421`: https://github.com/saltstack/salt/pull/43421 +.. _`bp-43526`: https://github.com/saltstack/salt/pull/43526 +.. _`fix-38839`: https://github.com/saltstack/salt/issues/38839 +.. _`fix-41116`: https://github.com/saltstack/salt/issues/41116 +.. _`fix-41721`: https://github.com/saltstack/salt/issues/41721 +.. _`fix-41885`: https://github.com/saltstack/salt/issues/41885 +.. _`fix-42115`: https://github.com/saltstack/salt/issues/42115 +.. _`fix-42151`: https://github.com/saltstack/salt/issues/42151 +.. _`fix-42152`: https://github.com/saltstack/salt/issues/42152 +.. _`fix-42166`: https://github.com/saltstack/salt/issues/42166 +.. _`fix-42267`: https://github.com/saltstack/salt/issues/42267 +.. _`fix-42375`: https://github.com/saltstack/salt/issues/42375 +.. _`fix-42381`: https://github.com/saltstack/salt/issues/42381 +.. _`fix-42405`: https://github.com/saltstack/salt/issues/42405 +.. _`fix-42417`: https://github.com/saltstack/salt/issues/42417 +.. _`fix-42639`: https://github.com/saltstack/salt/issues/42639 +.. _`fix-42683`: https://github.com/saltstack/salt/issues/42683 +.. _`fix-42697`: https://github.com/saltstack/salt/issues/42697 +.. _`fix-42870`: https://github.com/saltstack/salt/issues/42870 From caf5795856443ed7b2240038fe8a2ba85cef67e5 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Tue, 26 Sep 2017 17:27:29 -0400 Subject: [PATCH 306/633] add mac patch notes --- doc/topics/releases/2017.7.2.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/topics/releases/2017.7.2.rst b/doc/topics/releases/2017.7.2.rst index 7ef2d4363a..1f823d7417 100644 --- a/doc/topics/releases/2017.7.2.rst +++ b/doc/topics/releases/2017.7.2.rst @@ -3148,3 +3148,13 @@ Changes: .. _`fix-42683`: https://github.com/saltstack/salt/issues/42683 .. _`fix-42697`: https://github.com/saltstack/salt/issues/42697 .. _`fix-42870`: https://github.com/saltstack/salt/issues/42870 + +Build Notes +=========== + +Mac Installer Packages +-------------------------- + +Mac Installer packages have been patched with the following PR: 43756_ + +.. _43756: https://github.com/saltstack/salt/pull/43756 From 26b23b37bcb0414dc3595edeed17e3bce8c5b58f Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 26 Sep 2017 15:51:22 -0600 Subject: [PATCH 307/633] Skip test if missing binaries --- tests/unit/modules/test_disk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/test_disk.py b/tests/unit/modules/test_disk.py index 1c5459a530..7ff2fef60e 100644 --- a/tests/unit/modules/test_disk.py +++ b/tests/unit/modules/test_disk.py @@ -152,6 +152,7 @@ class DiskTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(disk.__salt__, {'cmd.retcode': mock}): self.assertEqual(disk.format_(device), True) + @skipIf(not salt.utils.which('lsblk') and not salt.utils.which('df'), 'lsblk or df not found') def test_fstype(self): ''' unit tests for disk.fstype From 35505ac966a7956bf41627a31b3df53b8522ed19 Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 26 Sep 2017 15:52:04 -0600 Subject: [PATCH 308/633] Honor 80 char limit --- tests/unit/modules/test_disk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/modules/test_disk.py b/tests/unit/modules/test_disk.py index 7ff2fef60e..d5db253071 100644 --- a/tests/unit/modules/test_disk.py +++ b/tests/unit/modules/test_disk.py @@ -152,7 +152,8 @@ class DiskTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(disk.__salt__, {'cmd.retcode': mock}): self.assertEqual(disk.format_(device), True) - @skipIf(not salt.utils.which('lsblk') and not salt.utils.which('df'), 'lsblk or df not found') + @skipIf(not salt.utils.which('lsblk') and not salt.utils.which('df'), + 'lsblk or df not found') def test_fstype(self): ''' unit tests for disk.fstype From 5c41268dd74171befb03b0f0343de8047d537ba3 Mon Sep 17 00:00:00 2001 From: Benjamin Schiborr Date: Tue, 26 Sep 2017 15:32:03 -0700 Subject: [PATCH 309/633] Fix return code of puppet module Fixes #43762. Successful puppet return codes are 0 and 2. When return code is 2 salt will fail. puppet.py intercepted that for the json return, however, the salt job will still fail, because it only parses the return code of the actual process. This commit changes the actual process to return 0 for 0 and 2. --- salt/modules/puppet.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/salt/modules/puppet.py b/salt/modules/puppet.py index 58b3963c8c..0462152e03 100644 --- a/salt/modules/puppet.py +++ b/salt/modules/puppet.py @@ -68,9 +68,7 @@ class _Puppet(object): self.vardir = 'C:\\ProgramData\\PuppetLabs\\puppet\\var' self.rundir = 'C:\\ProgramData\\PuppetLabs\\puppet\\run' self.confdir = 'C:\\ProgramData\\PuppetLabs\\puppet\\etc' - self.useshell = True else: - self.useshell = False self.puppet_version = __salt__['cmd.run']('puppet --version') if 'Enterprise' in self.puppet_version: self.vardir = '/var/opt/lib/pe-puppet' @@ -106,7 +104,10 @@ class _Puppet(object): ' --{0} {1}'.format(k, v) for k, v in six.iteritems(self.kwargs)] ) - return '{0} {1}'.format(cmd, args) + # Ensure that the puppet call will return 0 in case of exit code 2 + if salt.utils.platform.is_windows(): + return 'cmd /V:ON /c {0} {1} ^& if !ERRORLEVEL! EQU 2 (EXIT 0) ELSE (EXIT /B)'.format(cmd, args) + return '({0} {1}) || test $? -eq 2'.format(cmd, args) def arguments(self, args=None): ''' @@ -169,12 +170,7 @@ def run(*args, **kwargs): puppet.kwargs.update(salt.utils.args.clean_kwargs(**kwargs)) - ret = __salt__['cmd.run_all'](repr(puppet), python_shell=puppet.useshell) - if ret['retcode'] in [0, 2]: - ret['retcode'] = 0 - else: - ret['retcode'] = 1 - + ret = __salt__['cmd.run_all'](repr(puppet), python_shell=True) return ret From b41b9c83782ade7b8866a9dfca73882f0a187172 Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 26 Sep 2017 17:12:37 -0600 Subject: [PATCH 310/633] Skip snapper tests on Windows --- tests/unit/modules/test_snapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/test_snapper.py b/tests/unit/modules/test_snapper.py index f3c4e0fa2d..c95d343bbb 100644 --- a/tests/unit/modules/test_snapper.py +++ b/tests/unit/modules/test_snapper.py @@ -141,6 +141,7 @@ MODULE_RET = { } +@skipIf(sys.platform.startswith('win'), 'Snapper not available on Windows') @skipIf(NO_MOCK, NO_MOCK_REASON) class SnapperTestCase(TestCase, LoaderModuleMockMixin): From 651ed16ad38e9681d362a8b2fa142a23d6a03073 Mon Sep 17 00:00:00 2001 From: Denys Havrysh Date: Wed, 27 Sep 2017 10:38:47 +0300 Subject: [PATCH 311/633] Fix Pylint deprecated option warnings --- .pylintrc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index 219a8575ec..955c13948e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -255,8 +255,8 @@ ignore-imports=no [BASIC] -# Required attributes for module, separated by a comma -required-attributes= +# Required attributes for module, separated by a comma (will be removed in Pylint 2.0) +#required-attributes= # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input @@ -362,7 +362,8 @@ spelling-store-unknown-words=no [CLASSES] # List of interface methods to ignore, separated by a comma. This is used for # instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by +# Will be removed in Pylint 2.0 +#ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp From 46203c630c8f06b2a8d151ec1fb498fb92b9437f Mon Sep 17 00:00:00 2001 From: assaf shapira Date: Wed, 27 Sep 2017 15:28:46 +0300 Subject: [PATCH 312/633] ignore_ssl returned to _get_session --- salt/cloud/clouds/xen.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/salt/cloud/clouds/xen.py b/salt/cloud/clouds/xen.py index 7359796c20..959688ac84 100644 --- a/salt/cloud/clouds/xen.py +++ b/salt/cloud/clouds/xen.py @@ -151,8 +151,15 @@ def _get_session(): __opts__, search_global=False ) + ignore_ssl = config.get_cloud_config_value( + 'ignore_ssl', + get_configured_provider(), + __opts__, + default=False, + search_global=False + ) try: - session = XenAPI.Session(url) + session = XenAPI.Session(url, ignore_ssl=ignore_ssl) log.debug('url: {} user: {} password: {}, originator: {}'.format( url, user, From 5e4b122b56418e66f2feeedc4cfef777dbdec1c9 Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Wed, 27 Sep 2017 06:24:52 -0700 Subject: [PATCH 313/633] Fix ident issue to ensure code block ends correctly --- salt/modules/purefa.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/salt/modules/purefa.py b/salt/modules/purefa.py index aeb4104ee7..8bcf06fbe8 100644 --- a/salt/modules/purefa.py +++ b/salt/modules/purefa.py @@ -30,7 +30,7 @@ Installation Prerequisites - Configure Pure Storage FlashArray authentication. Use one of the following three methods. - 1) From the minion config + 1) From the minion config .. code-block:: yaml pure_tags: @@ -38,8 +38,8 @@ Installation Prerequisites san_ip: management vip or hostname for the FlashArray api_token: A valid api token for the FlashArray being managed - 2) From environment (PUREFA_IP and PUREFA_API) - 3) From the pillar (PUREFA_IP and PUREFA_API) + 2) From environment (PUREFA_IP and PUREFA_API) + 3) From the pillar (PUREFA_IP and PUREFA_API) :maintainer: Simon Dodsley (simon@purestorage.com) :maturity: new From 0194c6096089d3caf398708adc032826b4cac8aa Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Wed, 27 Sep 2017 08:27:44 -0600 Subject: [PATCH 314/633] dont print Minion not responding with quiet --- salt/cli/batch.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/salt/cli/batch.py b/salt/cli/batch.py index cc3121aa95..ffaf1d9f4a 100644 --- a/salt/cli/batch.py +++ b/salt/cli/batch.py @@ -140,10 +140,11 @@ class Batch(object): # sure that the main while loop finishes even with unresp minions minion_tracker = {} - # We already know some minions didn't respond to the ping, so inform - # the user we won't be attempting to run a job on them - for down_minion in self.down_minions: - print_cli('Minion {0} did not respond. No job will be sent.'.format(down_minion)) + if not self.quiet: + # We already know some minions didn't respond to the ping, so inform + # the user we won't be attempting to run a job on them + for down_minion in self.down_minions: + print_cli('Minion {0} did not respond. No job will be sent.'.format(down_minion)) # Iterate while we still have things to execute while len(ret) < len(self.minions): From 1de6791069552f80812dc4cab4c0ded0762030d3 Mon Sep 17 00:00:00 2001 From: Kees Bos Date: Thu, 21 Sep 2017 08:43:48 +0200 Subject: [PATCH 315/633] Fix git-pillar ext_pillar for __env__ usage The env must be mapped from '__env__' before validation of the env is done. Otherwise it will (naturally) fail, since __env__ in itself will never be a valid branch name. --- salt/pillar/git_pillar.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/salt/pillar/git_pillar.py b/salt/pillar/git_pillar.py index 1c0f7b700f..12bab065d8 100644 --- a/salt/pillar/git_pillar.py +++ b/salt/pillar/git_pillar.py @@ -398,6 +398,13 @@ def ext_pillar(minion_id, pillar, *repos): # pylint: disable=unused-argument False ) for pillar_dir, env in six.iteritems(git_pillar.pillar_dirs): + # Map env if env == '__env__' before checking the env value + if env == '__env__': + env = opts.get('pillarenv') \ + or opts.get('environment') \ + or opts.get('git_pillar_base') + log.debug('__env__ maps to %s', env) + # If pillarenv is set, only grab pillars with that match pillarenv if opts['pillarenv'] and env != opts['pillarenv']: log.debug( @@ -418,12 +425,6 @@ def ext_pillar(minion_id, pillar, *repos): # pylint: disable=unused-argument 'env \'%s\'', pillar_dir, env ) - if env == '__env__': - env = opts.get('pillarenv') \ - or opts.get('environment') \ - or opts.get('git_pillar_base') - log.debug('__env__ maps to %s', env) - pillar_roots = [pillar_dir] if __opts__['git_pillar_includes']: From 84bbe85e60081304388ca2e61f74c401cdd5bf7f Mon Sep 17 00:00:00 2001 From: Ronald van Zantvoort Date: Wed, 27 Sep 2017 17:59:41 +0200 Subject: [PATCH 316/633] typo fix aka what is a 'masterarpi' --- salt/daemons/masterapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index f501f41938..bb6d3dd452 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -270,12 +270,12 @@ def access_keys(opts): # Check other users matching ACL patterns if opts['client_acl_verify'] and HAS_PWD: - log.profile('Beginning pwd.getpwall() call in masterarpi access_keys function') + log.profile('Beginning pwd.getpwall() call in masterapi access_keys function') for user in pwd.getpwall(): user = user.pw_name if user not in keys and salt.utils.check_whitelist_blacklist(user, whitelist=acl_users): keys[user] = mk_key(opts, user) - log.profile('End pwd.getpwall() call in masterarpi access_keys function') + log.profile('End pwd.getpwall() call in masterapi access_keys function') return keys From f72bc000000d6c580feec5f7fa7e71e4a12f8294 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 27 Sep 2017 12:24:09 -0400 Subject: [PATCH 317/633] [2016.11] Bump latest and previous versions --- doc/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 0510bf8d50..adc0ff0234 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -240,8 +240,8 @@ on_saltstack = 'SALT_ON_SALTSTACK' in os.environ project = 'Salt' version = salt.version.__version__ -latest_release = '2017.7.1' # latest release -previous_release = '2016.11.7' # latest release from previous branch +latest_release = '2017.7.2' # latest release +previous_release = '2016.11.8' # latest release from previous branch previous_release_dir = '2016.11' # path on web server for previous branch next_release = '' # next release next_release_dir = '' # path on web server for next release branch From 410c624f7a0654ab8f9cad581ddd77097a83f3fb Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 27 Sep 2017 12:25:30 -0400 Subject: [PATCH 318/633] [2017.7] Bump latest and previous versions --- doc/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index de8db8ea90..4facdc0176 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -245,8 +245,8 @@ on_saltstack = 'SALT_ON_SALTSTACK' in os.environ project = 'Salt' version = salt.version.__version__ -latest_release = '2017.7.1' # latest release -previous_release = '2016.11.7' # latest release from previous branch +latest_release = '2017.7.2' # latest release +previous_release = '2016.11.8' # latest release from previous branch previous_release_dir = '2016.11' # path on web server for previous branch next_release = '' # next release next_release_dir = '' # path on web server for next release branch From 9ba51646e04edf586fd534ba9bc8b6e77e09c1a8 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 27 Sep 2017 12:31:20 -0400 Subject: [PATCH 319/633] [develop] Bump latest and previous versions --- doc/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index f7c329db11..99eb9a50ad 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -245,8 +245,8 @@ on_saltstack = 'SALT_ON_SALTSTACK' in os.environ project = 'Salt' version = salt.version.__version__ -latest_release = '2017.7.1' # latest release -previous_release = '2016.11.7' # latest release from previous branch +latest_release = '2017.7.2' # latest release +previous_release = '2016.11.8' # latest release from previous branch previous_release_dir = '2016.11' # path on web server for previous branch next_release = '' # next release next_release_dir = '' # path on web server for next release branch From 5c3109ff071a7e1b18680a9217b0539d2c9ae4e1 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Wed, 27 Sep 2017 12:55:46 -0400 Subject: [PATCH 320/633] Removed commented imports --- salt/utils/vmware.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 018bb10417..b0552996e3 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- ''' -import sys -import ssl Connection library for VMware .. versionadded:: 2015.8.2 From 5fb3f5f6b197ff58b2981b3e344c9c26e7791533 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 27 Sep 2017 14:52:29 -0400 Subject: [PATCH 321/633] Add Security Notes to 2016.3.8 Release Notes --- doc/topics/releases/2016.3.8.rst | 22 ++++------------------ doc/topics/releases/2016.3.9.rst | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 doc/topics/releases/2016.3.9.rst diff --git a/doc/topics/releases/2016.3.8.rst b/doc/topics/releases/2016.3.8.rst index c5f0c01da8..4d729cca34 100644 --- a/doc/topics/releases/2016.3.8.rst +++ b/doc/topics/releases/2016.3.8.rst @@ -7,23 +7,9 @@ Version 2016.3.8 is a bugfix release for :ref:`2016.3.0 `. Changes for v2016.3.7..v2016.3.8 -------------------------------- -New master configuration option `allow_minion_key_revoke`, defaults to True. This option -controls whether a minion can request that the master revoke its key. When True, a minion -can request a key revocation and the master will comply. If it is False, the key will not -be revoked by the msater. +Security Fix +============ -New master configuration option `require_minion_sign_messages` -This requires that minions cryptographically sign the messages they -publish to the master. If minions are not signing, then log this information -at loglevel 'INFO' and drop the message without acting on it. +CVE-2017-14695 Directory traversal vulnerability in minion id validation in SaltStack. Allows remote minions with incorrect credentials to authenticate to a master via a crafted minion ID. Credit for discovering the security flaw goes to: Julian Brost (julian@0x4a42.net) -New master configuration option `drop_messages_signature_fail` -Drop messages from minions when their signatures do not validate. -Note that when this option is False but `require_minion_sign_messages` is True -minions MUST sign their messages but the validity of their signatures -is ignored. - -New minion configuration option `minion_sign_messages` -Causes the minion to cryptographically sign the payload of messages it places -on the event bus for the master. The payloads are signed with the minion's -private key so the master can verify the signature with its public key. +CVE-2017-14696 Remote Denial of Service with a specially crafted authentication request. Credit for discovering the security flaw goes to: Julian Brost (julian@0x4a42.net) diff --git a/doc/topics/releases/2016.3.9.rst b/doc/topics/releases/2016.3.9.rst new file mode 100644 index 0000000000..630801cbe5 --- /dev/null +++ b/doc/topics/releases/2016.3.9.rst @@ -0,0 +1,29 @@ +=========================== +Salt 2016.3.9 Release Notes +=========================== + +Version 2016.3.9 is a bugfix release for :ref:`2016.3.0 `. + +Changes for v2016.3.7..v2016.3.9 +-------------------------------- + +New master configuration option `allow_minion_key_revoke`, defaults to True. This option +controls whether a minion can request that the master revoke its key. When True, a minion +can request a key revocation and the master will comply. If it is False, the key will not +be revoked by the msater. + +New master configuration option `require_minion_sign_messages` +This requires that minions cryptographically sign the messages they +publish to the master. If minions are not signing, then log this information +at loglevel 'INFO' and drop the message without acting on it. + +New master configuration option `drop_messages_signature_fail` +Drop messages from minions when their signatures do not validate. +Note that when this option is False but `require_minion_sign_messages` is True +minions MUST sign their messages but the validity of their signatures +is ignored. + +New minion configuration option `minion_sign_messages` +Causes the minion to cryptographically sign the payload of messages it places +on the event bus for the master. The payloads are signed with the minion's +private key so the master can verify the signature with its public key. From a64fe75816f893a90f2c1f68e2ca7f0522e6e2d9 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 27 Sep 2017 14:16:39 -0600 Subject: [PATCH 322/633] Use os agnostic paths --- tests/unit/modules/test_state.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/tests/unit/modules/test_state.py b/tests/unit/modules/test_state.py index 7f4f361c26..a758d73486 100644 --- a/tests/unit/modules/test_state.py +++ b/tests/unit/modules/test_state.py @@ -279,7 +279,7 @@ class MockTarFile(object): ''' Mock tarfile class ''' - path = "/tmp" + path = os.sep + "tmp" def __init__(self): pass @@ -952,30 +952,27 @@ class StateTestCase(TestCase, LoaderModuleMockMixin): ''' Test to execute a packaged state run ''' + tar_file = os.sep + os.path.join('tmp', 'state_pkg.tgz') mock = MagicMock(side_effect=[False, True, True, True, True, True]) with patch.object(os.path, 'isfile', mock), \ patch('salt.modules.state.tarfile', MockTarFile), \ patch('salt.modules.state.json', MockJson()): - self.assertEqual(state.pkg("/tmp/state_pkg.tgz", "", "md5"), {}) + self.assertEqual(state.pkg(tar_file, "", "md5"), {}) mock = MagicMock(side_effect=[False, 0, 0, 0, 0]) with patch.object(salt.utils, 'get_hash', mock): - self.assertDictEqual(state.pkg("/tmp/state_pkg.tgz", "", "md5"), - {}) + # Verify hash + self.assertDictEqual(state.pkg(tar_file, "", "md5"), {}) - self.assertDictEqual(state.pkg("/tmp/state_pkg.tgz", 0, "md5"), - {}) + # Verify file outside intended root + self.assertDictEqual(state.pkg(tar_file, 0, "md5"), {}) MockTarFile.path = "" MockJson.flag = True with patch('salt.utils.fopen', mock_open()): - self.assertListEqual(state.pkg("/tmp/state_pkg.tgz", - 0, - "md5"), - [True]) + self.assertListEqual(state.pkg(tar_file, 0, "md5"), [True]) MockTarFile.path = "" MockJson.flag = False with patch('salt.utils.fopen', mock_open()): - self.assertTrue(state.pkg("/tmp/state_pkg.tgz", - 0, "md5")) + self.assertTrue(state.pkg(tar_file, 0, "md5")) From 52acfd980d5365c95ff33829b8445e557b013701 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Wed, 27 Sep 2017 14:26:32 -0600 Subject: [PATCH 323/633] merge upstream develop --- .github/CODEOWNERS | 60 + .github/stale.yml | 4 +- conf/cloud.providers | 2 +- conf/cloud.providers.d/digitalocean.conf | 2 +- conf/minion | 2 +- doc/man/salt.7 | 1 + doc/ref/beacons/all/index.rst | 1 + .../all/salt.beacons.napalm_beacon.rst | 6 + doc/ref/cli/salt-cloud.rst | 2 +- doc/ref/clouds/all/index.rst | 2 +- .../all/salt.cloud.clouds.digital_ocean.rst | 4 +- doc/ref/configuration/master.rst | 4 +- doc/ref/configuration/minion.rst | 44 +- doc/ref/configuration/proxy.rst | 50 + doc/ref/modules/all/index.rst | 3 + .../all/salt.modules.boto_cloudfront.rst | 6 + doc/ref/modules/all/salt.modules.purefa.rst | 6 + .../modules/all/salt.modules.textfsm_mod.rst | 5 + .../runners/all/salt.runners.digicertapi.rst | 4 +- .../runners/all/salt.runners.mattermost.rst | 10 +- doc/ref/runners/all/salt.runners.vault.rst | 4 +- .../runners/all/salt.runners.venafiapi.rst | 4 +- doc/ref/states/aggregate.rst | 2 +- doc/ref/states/all/index.rst | 2 + .../all/salt.states.boto_cloudfront.rst | 6 + doc/ref/states/all/salt.states.nfs_export.rst | 6 + doc/ref/states/writing.rst | 7 +- doc/topics/beacons/index.rst | 5 +- doc/topics/cloud/cloud.rst | 2 +- doc/topics/cloud/config.rst | 2 +- doc/topics/cloud/digitalocean.rst | 10 +- doc/topics/development/contributing.rst | 12 +- doc/topics/development/tests/index.rst | 4 +- doc/topics/development/tests/integration.rst | 6 +- doc/topics/engines/index.rst | 7 +- doc/topics/installation/debian.rst | 2 +- doc/topics/reactor/index.rst | 553 +++++--- doc/topics/releases/oxygen.rst | 33 +- doc/topics/tutorials/libcloud.rst | 2 +- .../windows/windows-package-manager.rst | 14 +- pkg/osx/pkg-scripts/preinstall | 2 +- pkg/salt.bash | 5 +- pkg/windows/build.bat | 2 +- pkg/windows/build_env_2.ps1 | 24 +- pkg/windows/build_env_3.ps1 | 8 +- pkg/windows/build_pkg.bat | 2 +- pkg/windows/clean_env.bat | 18 +- pkg/windows/installer/Salt-Minion-Setup.nsi | 420 +++--- pkg/windows/modules/get-settings.psm1 | 6 +- salt/auth/__init__.py | 130 +- salt/auth/ldap.py | 4 +- salt/beacons/__init__.py | 71 +- salt/beacons/btmp.py | 131 +- salt/beacons/inotify.py | 2 + salt/beacons/napalm_beacon.py | 353 +++++ salt/beacons/wtmp.py | 131 +- salt/cache/__init__.py | 2 +- salt/cache/consul.py | 9 +- salt/cli/caller.py | 2 +- salt/cli/cp.py | 3 +- salt/cli/spm.py | 6 +- salt/client/__init__.py | 10 +- salt/client/api.py | 11 +- salt/client/mixins.py | 34 +- .../{digital_ocean.py => digitalocean.py} | 33 +- salt/cloud/clouds/gce.py | 2 +- salt/cloud/clouds/libvirt.py | 24 +- salt/cloud/clouds/linode.py | 7 +- salt/cloud/clouds/virtualbox.py | 17 +- salt/config/__init__.py | 125 +- salt/config/schemas/esxcluster.py | 215 +++ salt/config/schemas/vcenter.py | 33 + salt/daemons/flo/core.py | 2 +- salt/daemons/masterapi.py | 160 ++- salt/engines/slack.py | 1120 ++++++++------- salt/exceptions.py | 20 +- salt/ext/vsan/__init__.py | 7 + salt/ext/vsan/vsanapiutils.py | 165 +++ salt/ext/vsan/vsanmgmtObjects.py | 143 ++ salt/fileclient.py | 59 +- salt/fileserver/__init__.py | 57 +- salt/fileserver/hgfs.py | 35 +- salt/fileserver/minionfs.py | 21 +- salt/fileserver/roots.py | 35 +- salt/fileserver/s3fs.py | 35 +- salt/fileserver/svnfs.py | 21 +- salt/grains/cimc.py | 38 + salt/grains/core.py | 48 +- salt/grains/extra.py | 10 +- salt/grains/metadata.py | 19 +- salt/grains/napalm.py | 4 +- salt/key.py | 37 +- salt/loader.py | 2 +- salt/log/handlers/fluent_mod.py | 197 ++- salt/master.py | 175 ++- salt/minion.py | 109 +- salt/modules/alternatives.py | 3 +- salt/modules/apache.py | 16 +- salt/modules/aptpkg.py | 12 +- salt/modules/archive.py | 31 +- salt/modules/augeas_cfg.py | 2 +- salt/modules/beacons.py | 142 +- salt/modules/boto_cloudfront.py | 462 ++++++ salt/modules/boto_elb.py | 75 +- salt/modules/boto_sqs.py | 2 +- salt/modules/boto_vpc.py | 7 +- salt/modules/cimc.py | 710 ++++++++++ salt/modules/cmdmod.py | 14 +- salt/modules/cp.py | 28 +- salt/modules/debconfmod.py | 7 +- salt/modules/debian_ip.py | 9 +- salt/modules/dockermod.py | 42 +- salt/modules/ebuild.py | 30 +- salt/modules/elasticsearch.py | 4 + salt/modules/esxcluster.py | 29 + salt/modules/file.py | 79 +- salt/modules/genesis.py | 56 +- salt/modules/groupadd.py | 42 +- salt/modules/ini_manage.py | 9 +- salt/modules/inspectlib/collector.py | 3 +- salt/modules/inspectlib/kiwiproc.py | 7 +- salt/modules/iptables.py | 2 + salt/modules/junos.py | 5 + salt/modules/kubernetes.py | 45 +- salt/modules/linux_acl.py | 3 + salt/modules/linux_lvm.py | 7 +- salt/modules/mdadm.py | 37 + salt/modules/mine.py | 2 +- salt/modules/mount.py | 89 ++ salt/modules/mysql.py | 20 +- salt/modules/napalm_network.py | 9 +- salt/modules/nfs3.py | 4 +- salt/modules/nilrt_ip.py | 34 +- salt/modules/panos.py | 152 +- salt/modules/pkg_resource.py | 26 +- salt/modules/portage_config.py | 28 +- salt/modules/purefa.py | 1256 +++++++++++++++++ salt/modules/redismod.py | 10 +- salt/modules/rh_ip.py | 5 +- salt/modules/saltcheck.py | 602 ++++++++ salt/modules/selinux.py | 108 +- salt/modules/ssh.py | 11 +- salt/modules/state.py | 49 +- salt/modules/sysmod.py | 34 +- salt/modules/system.py | 5 +- salt/modules/textfsm_mod.py | 459 ++++++ salt/modules/virtualenv_mod.py | 8 +- salt/modules/vsphere.py | 893 +++++++++++- salt/modules/win_iis.py | 14 + salt/modules/win_path.py | 2 +- salt/modules/win_pkg.py | 60 +- salt/modules/win_pki.py | 6 +- salt/modules/yumpkg.py | 25 +- salt/modules/zk_concurrency.py | 8 +- salt/modules/zypper.py | 14 +- salt/netapi/rest_cherrypy/__init__.py | 2 +- salt/netapi/rest_tornado/__init__.py | 2 +- salt/output/highstate.py | 40 +- salt/output/key.py | 4 +- salt/output/nested.py | 4 +- salt/output/no_return.py | 4 +- salt/output/overstatestage.py | 4 +- salt/output/table_out.py | 10 +- salt/pillar/__init__.py | 153 +- salt/pillar/consul_pillar.py | 3 +- salt/pillar/file_tree.py | 3 +- salt/pillar/nacl.py | 29 + salt/pillar/nodegroups.py | 3 +- salt/proxy/cimc.py | 290 ++++ salt/proxy/esxcluster.py | 310 ++++ salt/proxy/esxdatacenter.py | 23 +- salt/proxy/junos.py | 41 +- salt/proxy/panos.py | 2 +- salt/renderers/nacl.py | 7 +- salt/returners/carbon_return.py | 2 +- salt/returners/cassandra_cql_return.py | 2 +- salt/returners/cassandra_return.py | 2 +- salt/returners/couchbase_return.py | 9 +- salt/returners/couchdb_return.py | 2 +- salt/returners/django_return.py | 2 +- salt/returners/elasticsearch_return.py | 2 +- salt/returners/etcd_return.py | 2 +- salt/returners/influxdb_return.py | 2 +- salt/returners/local_cache.py | 10 +- salt/returners/memcache_return.py | 2 +- salt/returners/mongo_future_return.py | 2 +- salt/returners/mongo_return.py | 2 +- salt/returners/mysql.py | 2 +- salt/returners/odbc.py | 2 +- salt/returners/pgjsonb.py | 2 +- salt/returners/postgres.py | 2 +- salt/returners/postgres_local_cache.py | 2 +- salt/returners/redis_return.py | 2 +- salt/returners/sentry_return.py | 2 +- salt/returners/smtp_return.py | 2 +- salt/returners/sqlite3_return.py | 2 +- salt/returners/syslog_return.py | 2 +- salt/returners/zabbix_return.py | 17 +- salt/roster/cache.py | 3 +- salt/roster/sshconfig.py | 146 ++ salt/runners/git_pillar.py | 5 + salt/runners/mattermost.py | 5 +- salt/runners/state.py | 2 +- salt/spm/__init__.py | 29 +- salt/state.py | 73 +- salt/states/acme.py | 7 +- salt/states/archive.py | 234 ++- salt/states/beacon.py | 21 +- salt/states/boto_cloudfront.py | 229 +++ salt/states/boto_elb.py | 3 +- salt/states/boto_sqs.py | 64 +- salt/states/chocolatey.py | 58 +- salt/states/cimc.py | 211 +++ salt/states/cron.py | 8 +- salt/states/docker_image.py | 80 +- salt/states/elasticsearch.py | 26 +- salt/states/esxcluster.py | 538 +++++++ salt/states/file.py | 421 +++++- salt/states/git.py | 17 + salt/states/iptables.py | 3 +- salt/states/linux_acl.py | 10 +- salt/states/lvm.py | 2 +- salt/states/mdadm.py | 123 +- salt/states/module.py | 4 +- salt/states/mount.py | 62 + salt/states/netconfig.py | 11 +- salt/states/nfs_export.py | 218 +++ salt/states/panos.py | 596 +++++++- salt/states/pkg.py | 3 +- salt/states/saltmod.py | 25 +- salt/states/selinux.py | 65 +- salt/states/ssh_known_hosts.py | 1 + salt/states/win_iis.py | 11 +- salt/syspaths.py | 18 +- salt/template.py | 7 +- salt/templates/rh_ip/rh7_eth.jinja | 2 + salt/transport/zeromq.py | 5 +- salt/utils/__init__.py | 528 +++---- salt/utils/args.py | 69 +- salt/utils/boto.py | 4 +- salt/utils/boto3.py | 14 +- salt/utils/cloud.py | 6 +- salt/utils/color.py | 92 ++ salt/utils/configparser.py | 269 ++++ salt/utils/dictdiffer.py | 307 ++++ salt/utils/files.py | 62 + salt/utils/gitfs.py | 360 ++--- salt/utils/jid.py | 22 +- salt/utils/job.py | 2 +- salt/utils/listdiffer.py | 256 ++++ salt/utils/master.py | 3 +- salt/utils/minions.py | 112 +- salt/utils/mount.py | 67 + salt/utils/network.py | 3 +- salt/utils/parsers.py | 28 +- salt/utils/pydsl.py | 8 +- salt/utils/reactor.py | 232 ++- salt/utils/schedule.py | 184 +-- salt/utils/state.py | 213 +++ salt/utils/stringutils.py | 27 + salt/utils/url.py | 7 +- salt/utils/validate/path.py | 11 + salt/utils/value.py | 19 + salt/utils/vmware.py | 484 ++++++- salt/utils/vsan.py | 212 +++ salt/utils/xmlutil.py | 12 +- salt/wheel/file_roots.py | 3 +- salt/wheel/pillar_roots.py | 3 +- setup.py | 5 + tests/consist.py | 4 +- tests/integration/__init__.py | 5 +- tests/integration/client/test_standard.py | 30 + ..._digital_ocean.py => test_digitalocean.py} | 8 +- tests/integration/cloud/test_cloud.py | 4 +- .../conf/cloud.providers.d/digital_ocean.conf | 2 +- tests/integration/files/conf/master | 1 + .../files/file/base/mysql/select_query.sql | 7 + .../files/file/base/mysql/update_query.sql | 3 + tests/integration/modules/test_groupadd.py | 76 + tests/integration/modules/test_mysql.py | 131 +- tests/integration/states/test_npm.py | 4 +- tests/unit/beacons/test_btmp_beacon.py | 117 ++ tests/unit/beacons/test_wtmp_beacon.py | 119 ++ tests/unit/cloud/clouds/test_ec2.py | 41 +- tests/unit/config/test_api.py | 27 +- tests/unit/config/test_config.py | 53 +- tests/unit/daemons/__init__.py | 1 + tests/unit/daemons/test_masterapi.py | 201 +++ tests/unit/fileserver/test_gitfs.py | 26 +- tests/unit/modules/test_alternatives.py | 18 +- tests/unit/modules/test_beacons.py | 4 + tests/unit/modules/test_chef.py | 6 +- tests/unit/modules/test_dockermod.py | 34 +- tests/unit/modules/test_esxcluster.py | 38 + tests/unit/modules/test_file.py | 8 +- tests/unit/modules/test_gem.py | 3 +- tests/unit/modules/test_genesis.py | 55 +- tests/unit/modules/test_groupadd.py | 31 +- tests/unit/modules/test_hosts.py | 25 +- tests/unit/modules/test_ini_manage.py | 112 +- tests/unit/modules/test_inspect_collector.py | 39 +- tests/unit/modules/test_kubernetes.py | 130 ++ tests/unit/modules/test_mac_group.py | 9 +- tests/unit/modules/test_mac_user.py | 25 +- tests/unit/modules/test_mount.py | 2 +- tests/unit/modules/test_pam.py | 3 +- tests/unit/modules/test_parted.py | 30 +- tests/unit/modules/test_portage_config.py | 99 +- tests/unit/modules/test_pw_group.py | 3 + tests/unit/modules/test_qemu_nbd.py | 17 +- tests/unit/modules/test_rh_ip.py | 2 +- tests/unit/modules/test_saltcheck.py | 335 +++++ tests/unit/modules/test_seed.py | 21 +- tests/unit/modules/test_state.py | 20 +- tests/unit/modules/test_virtualenv.py | 7 +- tests/unit/modules/test_vsphere.py | 306 +++- tests/unit/modules/test_yumpkg.py | 44 +- tests/unit/modules/test_zypper.py | 22 +- tests/unit/pillar/test_nodegroups.py | 6 +- tests/unit/proxy/test_esxcluster.py | 185 +++ tests/unit/proxy/test_esxdatacenter.py | 66 +- tests/unit/renderers/test_nacl.py | 4 +- tests/unit/returners/test_local_cache.py | 5 +- tests/unit/states/test_boto_cloudfront.py | 223 +++ tests/unit/states/test_boto_sqs.py | 18 +- tests/unit/states/test_docker_image.py | 40 +- tests/unit/states/test_file.py | 5 +- tests/unit/states/test_mdadm.py | 138 +- tests/unit/states/test_mount.py | 4 + tests/unit/states/test_saltmod.py | 19 +- tests/unit/test_auth.py | 36 +- tests/unit/test_master.py | 209 +++ tests/unit/test_pillar.py | 411 ++++++ tests/unit/test_pydsl.py | 7 +- tests/unit/test_ssh_config_roster.py | 87 ++ tests/unit/test_state.py | 28 +- tests/unit/utils/test_args.py | 51 +- tests/unit/utils/test_color.py | 27 + tests/unit/utils/test_configparser.py | 268 ++++ tests/unit/utils/test_dictdiffer.py | 95 ++ tests/unit/utils/test_gitfs.py | 2 +- tests/unit/utils/test_listdiffer.py | 88 ++ tests/unit/utils/test_minions.py | 4 +- tests/unit/utils/test_reactor.py | 604 +++++++- tests/unit/utils/test_schema.py | 79 +- tests/unit/utils/test_state.py | 688 +++++++++ tests/unit/utils/test_utils.py | 488 +------ tests/unit/utils/test_verify.py | 42 +- tests/unit/utils/test_vsan.py | 384 +++++ tests/unit/utils/vmware/test_common.py | 50 + tests/unit/utils/vmware/test_license.py | 663 +++++++++ tests/unit/utils/vmware/test_storage.py | 396 ++++++ 352 files changed, 22456 insertions(+), 4369 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 doc/ref/beacons/all/salt.beacons.napalm_beacon.rst create mode 100644 doc/ref/modules/all/salt.modules.boto_cloudfront.rst create mode 100644 doc/ref/modules/all/salt.modules.purefa.rst create mode 100644 doc/ref/modules/all/salt.modules.textfsm_mod.rst create mode 100644 doc/ref/states/all/salt.states.boto_cloudfront.rst create mode 100644 doc/ref/states/all/salt.states.nfs_export.rst create mode 100644 salt/beacons/napalm_beacon.py rename salt/cloud/clouds/{digital_ocean.py => digitalocean.py} (98%) create mode 100644 salt/config/schemas/esxcluster.py create mode 100644 salt/config/schemas/vcenter.py create mode 100644 salt/ext/vsan/__init__.py create mode 100644 salt/ext/vsan/vsanapiutils.py create mode 100644 salt/ext/vsan/vsanmgmtObjects.py create mode 100644 salt/grains/cimc.py create mode 100644 salt/modules/boto_cloudfront.py create mode 100644 salt/modules/cimc.py create mode 100644 salt/modules/esxcluster.py create mode 100644 salt/modules/purefa.py create mode 100644 salt/modules/saltcheck.py create mode 100644 salt/modules/textfsm_mod.py create mode 100644 salt/pillar/nacl.py create mode 100644 salt/proxy/cimc.py create mode 100644 salt/proxy/esxcluster.py create mode 100644 salt/roster/sshconfig.py create mode 100644 salt/states/boto_cloudfront.py create mode 100644 salt/states/cimc.py create mode 100644 salt/states/esxcluster.py create mode 100644 salt/states/nfs_export.py create mode 100644 salt/utils/color.py create mode 100644 salt/utils/configparser.py create mode 100644 salt/utils/listdiffer.py create mode 100644 salt/utils/mount.py create mode 100644 salt/utils/state.py create mode 100644 salt/utils/value.py create mode 100644 salt/utils/vsan.py rename tests/integration/cloud/providers/{test_digital_ocean.py => test_digitalocean.py} (98%) create mode 100644 tests/integration/files/file/base/mysql/select_query.sql create mode 100644 tests/integration/files/file/base/mysql/update_query.sql create mode 100644 tests/unit/beacons/test_btmp_beacon.py create mode 100644 tests/unit/beacons/test_wtmp_beacon.py create mode 100644 tests/unit/daemons/__init__.py create mode 100644 tests/unit/daemons/test_masterapi.py create mode 100644 tests/unit/modules/test_esxcluster.py create mode 100644 tests/unit/modules/test_kubernetes.py create mode 100644 tests/unit/modules/test_saltcheck.py create mode 100644 tests/unit/proxy/test_esxcluster.py create mode 100644 tests/unit/states/test_boto_cloudfront.py create mode 100644 tests/unit/test_master.py create mode 100644 tests/unit/test_ssh_config_roster.py create mode 100644 tests/unit/utils/test_color.py create mode 100644 tests/unit/utils/test_configparser.py create mode 100644 tests/unit/utils/test_dictdiffer.py create mode 100644 tests/unit/utils/test_listdiffer.py create mode 100644 tests/unit/utils/test_state.py create mode 100644 tests/unit/utils/test_vsan.py create mode 100644 tests/unit/utils/vmware/test_license.py create mode 100644 tests/unit/utils/vmware/test_storage.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..29288c6efe --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,60 @@ +# SALTSTACK CODE OWNERS + +# See https://help.github.com/articles/about-codeowners/ +# for more info about CODEOWNERS file + +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# See https://help.github.com/articles/about-codeowners/ +# for more info about the CODEOWNERS file + +# Team Boto +salt/**/*boto* @saltstack/team-boto + +# Team Core +salt/auth/ @saltstack/team-core +salt/cache/ @saltstack/team-core +salt/cli/ @saltstack/team-core +salt/client/* @saltstack/team-core +salt/config/* @saltstack/team-core +salt/daemons/ @saltstack/team-core +salt/pillar/ @saltstack/team-core +salt/loader.py @saltstack/team-core +salt/payload.py @saltstack/team-core +salt/**/master* @saltstack/team-core +salt/**/minion* @saltstack/team-core + +# Team Cloud +salt/cloud/ @saltstack/team-cloud +salt/utils/openstack/ @saltstack/team-cloud +salt/utils/aws.py @saltstack/team-cloud +salt/**/*cloud* @saltstack/team-cloud + +# Team NetAPI +salt/cli/api.py @saltstack/team-netapi +salt/client/netapi.py @saltstack/team-netapi +salt/netapi/ @saltstack/team-netapi + +# Team Network +salt/proxy/ @saltstack/team-proxy + +# Team SPM +salt/cli/spm.py @saltstack/team-spm +salt/spm/ @saltstack/team-spm + +# Team SSH +salt/cli/ssh.py @saltstack/team-ssh +salt/client/ssh/ @saltstack/team-ssh +salt/runners/ssh.py @saltstack/team-ssh +salt/**/thin.py @saltstack/team-ssh + +# Team State +salt/state.py @saltstack/team-state + +# Team Transport +salt/transport/ @saltstack/team-transport +salt/utils/zeromq.py @saltstack/team-transport + +# Team Windows +salt/**/*win* @saltstack/team-windows diff --git a/.github/stale.yml b/.github/stale.yml index 0a5be0ea46..35928803a7 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,8 +1,8 @@ # Probot Stale configuration file # Number of days of inactivity before an issue becomes stale -# 1075 is approximately 2 years and 11 months -daysUntilStale: 1075 +# 1000 is approximately 2 years and 9 months +daysUntilStale: 1000 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 diff --git a/conf/cloud.providers b/conf/cloud.providers index b4879432c4..8e9cc2ccf7 100644 --- a/conf/cloud.providers +++ b/conf/cloud.providers @@ -3,7 +3,7 @@ # directory is identical. #my-digitalocean-config: -# driver: digital_ocean +# driver: digitalocean # client_key: wFGEwgregeqw3435gDger # api_key: GDE43t43REGTrkilg43934t34qT43t4dgegerGEgg # location: New York 1 diff --git a/conf/cloud.providers.d/digitalocean.conf b/conf/cloud.providers.d/digitalocean.conf index 989758f184..da3c13b45d 100644 --- a/conf/cloud.providers.d/digitalocean.conf +++ b/conf/cloud.providers.d/digitalocean.conf @@ -1,5 +1,5 @@ #my-digitalocean-config: -# driver: digital_ocean +# driver: digitalocean # client_key: wFGEwgregeqw3435gDger # api_key: GDE43t43REGTrkilg43934t34qT43t4dgegerGEgg # location: New York 1 diff --git a/conf/minion b/conf/minion index 6cae043295..fa5caf317b 100644 --- a/conf/minion +++ b/conf/minion @@ -373,7 +373,7 @@ # interface: eth0 # cidr: '10.0.0.0/8' -# The number of seconds a mine update runs. +# The number of minutes between mine updates. #mine_interval: 60 # Windows platforms lack posix IPC and must rely on slower TCP based inter- diff --git a/doc/man/salt.7 b/doc/man/salt.7 index d6cfe937a1..86c463b771 100644 --- a/doc/man/salt.7 +++ b/doc/man/salt.7 @@ -10795,6 +10795,7 @@ cmd_whitelist_glob: .UNINDENT .UNINDENT .SS Thread Settings +.SS \fBmultiprocessing\fP .sp Default: \fBTrue\fP .sp diff --git a/doc/ref/beacons/all/index.rst b/doc/ref/beacons/all/index.rst index c0970f4f6c..7fccfc5b15 100644 --- a/doc/ref/beacons/all/index.rst +++ b/doc/ref/beacons/all/index.rst @@ -22,6 +22,7 @@ beacon modules load log memusage + napalm_beacon network_info network_settings pkg diff --git a/doc/ref/beacons/all/salt.beacons.napalm_beacon.rst b/doc/ref/beacons/all/salt.beacons.napalm_beacon.rst new file mode 100644 index 0000000000..ff5bbc4b01 --- /dev/null +++ b/doc/ref/beacons/all/salt.beacons.napalm_beacon.rst @@ -0,0 +1,6 @@ +========================== +salt.beacons.napalm_beacon +========================== + +.. automodule:: salt.beacons.napalm_beacon + :members: diff --git a/doc/ref/cli/salt-cloud.rst b/doc/ref/cli/salt-cloud.rst index a9f3123756..a64c6ba83b 100644 --- a/doc/ref/cli/salt-cloud.rst +++ b/doc/ref/cli/salt-cloud.rst @@ -136,7 +136,7 @@ Query Options .. versionadded:: 2014.7.0 Display a list of configured profiles. Pass in a cloud provider to view - the provider's associated profiles, such as ``digital_ocean``, or pass in + the provider's associated profiles, such as ``digitalocean``, or pass in ``all`` to list all the configured profiles. diff --git a/doc/ref/clouds/all/index.rst b/doc/ref/clouds/all/index.rst index 5c5a3a9f5c..15fb4b1ae3 100644 --- a/doc/ref/clouds/all/index.rst +++ b/doc/ref/clouds/all/index.rst @@ -13,7 +13,7 @@ Full list of Salt Cloud modules aliyun azurearm cloudstack - digital_ocean + digitalocean dimensiondata ec2 gce diff --git a/doc/ref/clouds/all/salt.cloud.clouds.digital_ocean.rst b/doc/ref/clouds/all/salt.cloud.clouds.digital_ocean.rst index 71917c8765..1eeb2b2a41 100644 --- a/doc/ref/clouds/all/salt.cloud.clouds.digital_ocean.rst +++ b/doc/ref/clouds/all/salt.cloud.clouds.digital_ocean.rst @@ -1,6 +1,6 @@ =============================== -salt.cloud.clouds.digital_ocean +salt.cloud.clouds.digitalocean =============================== -.. automodule:: salt.cloud.clouds.digital_ocean +.. automodule:: salt.cloud.clouds.digitalocean :members: \ No newline at end of file diff --git a/doc/ref/configuration/master.rst b/doc/ref/configuration/master.rst index ff6e49d349..0c6ad6f919 100644 --- a/doc/ref/configuration/master.rst +++ b/doc/ref/configuration/master.rst @@ -4175,7 +4175,9 @@ information. .. code-block:: yaml - reactor: [] + reactor: + - 'salt/minion/*/start': + - salt://reactor/startup_tasks.sls .. conf_master:: reactor_refresh_interval diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index 039764c6b2..3438bfca03 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -706,7 +706,7 @@ Note these can be defined in the pillar for a minion as well. Default: ``60`` -The number of seconds a mine update runs. +The number of minutes between mine updates. .. code-block:: yaml @@ -2113,6 +2113,41 @@ It will be interpreted as megabytes. file_recv_max_size: 100 +.. conf_minion:: pass_to_ext_pillars + +``pass_to_ext_pillars`` +----------------------- + +Specify a list of configuration keys whose values are to be passed to +external pillar functions. + +Suboptions can be specified using the ':' notation (i.e. ``option:suboption``) + +The values are merged and included in the ``extra_minion_data`` optional +parameter of the external pillar function. The ``extra_minion_data`` parameter +is passed only to the external pillar functions that have it explicitly +specified in their definition. + +If the config contains + +.. code-block:: yaml + + opt1: value1 + opt2: + subopt1: value2 + subopt2: value3 + + pass_to_ext_pillars: + - opt1 + - opt2: subopt1 + +the ``extra_minion_data`` parameter will be + +.. code-block:: python + + {'opt1': 'value1', + 'opt2': {'subopt1': 'value2'}} + Security Settings ================= @@ -2369,11 +2404,14 @@ Thread Settings .. conf_minion:: multiprocessing +``multiprocessing`` +------- + Default: ``True`` -If `multiprocessing` is enabled when a minion receives a +If ``multiprocessing`` is enabled when a minion receives a publication a new process is spawned and the command is executed therein. -Conversely, if `multiprocessing` is disabled the new publication will be run +Conversely, if ``multiprocessing`` is disabled the new publication will be run executed in a thread. diff --git a/doc/ref/configuration/proxy.rst b/doc/ref/configuration/proxy.rst index 974e0af890..e55f3fc01b 100644 --- a/doc/ref/configuration/proxy.rst +++ b/doc/ref/configuration/proxy.rst @@ -118,3 +118,53 @@ has to be closed after every command. .. code-block:: yaml proxy_always_alive: False + +``proxy_merge_pillar_in_opts`` +------------------------------ + +.. versionadded:: 2017.7.3 + +Default: ``False``. + +Wheter the pillar data to be merged into the proxy configuration options. +As multiple proxies can run on the same server, we may need different +configuration options for each, while there's one single configuration file. +The solution is merging the pillar data of each proxy minion into the opts. + +.. code-block:: yaml + + proxy_merge_pillar_in_opts: True + +``proxy_deep_merge_pillar_in_opts`` +----------------------------------- + +.. versionadded:: 2017.7.3 + +Default: ``False``. + +Deep merge of pillar data into configuration opts. +This option is evaluated only when :conf_proxy:`proxy_merge_pillar_in_opts` is +enabled. + +``proxy_merge_pillar_in_opts_strategy`` +--------------------------------------- + +.. versionadded:: 2017.7.3 + +Default: ``smart``. + +The strategy used when merging pillar configuration into opts. +This option is evaluated only when :conf_proxy:`proxy_merge_pillar_in_opts` is +enabled. + +``proxy_mines_pillar`` +---------------------- + +.. versionadded:: 2017.7.3 + +Default: ``True``. + +Allow enabling mine details using pillar data. This evaluates the mine +configuration under the pillar, for the following regular minion options that +are also equally available on the proxy minion: :conf_minion:`mine_interval`, +and :conf_minion:`mine_functions`. diff --git a/doc/ref/modules/all/index.rst b/doc/ref/modules/all/index.rst index 1365f38453..b9f4b3f35c 100644 --- a/doc/ref/modules/all/index.rst +++ b/doc/ref/modules/all/index.rst @@ -44,6 +44,7 @@ execution modules boto_apigateway boto_asg boto_cfn + boto_cloudfront boto_cloudtrail boto_cloudwatch boto_cloudwatch_event @@ -326,6 +327,7 @@ execution modules ps publish puppet + purefa pushbullet pushover_notify pw_group @@ -417,6 +419,7 @@ execution modules test testinframod test_virtual + textfsm_mod timezone tls tomcat diff --git a/doc/ref/modules/all/salt.modules.boto_cloudfront.rst b/doc/ref/modules/all/salt.modules.boto_cloudfront.rst new file mode 100644 index 0000000000..a76ea991fc --- /dev/null +++ b/doc/ref/modules/all/salt.modules.boto_cloudfront.rst @@ -0,0 +1,6 @@ +============================ +salt.modules.boto_cloudfront +============================ + +.. automodule:: salt.modules.boto_cloudfront + :members: diff --git a/doc/ref/modules/all/salt.modules.purefa.rst b/doc/ref/modules/all/salt.modules.purefa.rst new file mode 100644 index 0000000000..1a1b80ce1e --- /dev/null +++ b/doc/ref/modules/all/salt.modules.purefa.rst @@ -0,0 +1,6 @@ +=================== +salt.modules.purefa +=================== + +.. automodule:: salt.modules.purefa + :members: diff --git a/doc/ref/modules/all/salt.modules.textfsm_mod.rst b/doc/ref/modules/all/salt.modules.textfsm_mod.rst new file mode 100644 index 0000000000..7b2c64b956 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.textfsm_mod.rst @@ -0,0 +1,5 @@ +salt.modules.textfsm_mod module +=============================== + +.. automodule:: salt.modules.textfsm_mod + :members: diff --git a/doc/ref/runners/all/salt.runners.digicertapi.rst b/doc/ref/runners/all/salt.runners.digicertapi.rst index 10919c8a91..280fc059fa 100644 --- a/doc/ref/runners/all/salt.runners.digicertapi.rst +++ b/doc/ref/runners/all/salt.runners.digicertapi.rst @@ -1,5 +1,5 @@ -salt.runners.digicertapi module -=============================== +salt.runners.digicertapi +======================== .. automodule:: salt.runners.digicertapi :members: diff --git a/doc/ref/runners/all/salt.runners.mattermost.rst b/doc/ref/runners/all/salt.runners.mattermost.rst index 4a2b8e28c6..c33a9f459b 100644 --- a/doc/ref/runners/all/salt.runners.mattermost.rst +++ b/doc/ref/runners/all/salt.runners.mattermost.rst @@ -1,5 +1,11 @@ -salt.runners.mattermost module -============================== +salt.runners.mattermost +======================= + +**Note for 2017.7 releases!** + +Due to the `salt.runners.config `_ module not being available in this release series, importing the `salt.runners.config `_ module from the develop branch is required to make this module work. + +Ref: `Mattermost runner failing to retrieve config values due to unavailable config runner #43479 `_ .. automodule:: salt.runners.mattermost :members: diff --git a/doc/ref/runners/all/salt.runners.vault.rst b/doc/ref/runners/all/salt.runners.vault.rst index 7c424f24ee..434774b0dd 100644 --- a/doc/ref/runners/all/salt.runners.vault.rst +++ b/doc/ref/runners/all/salt.runners.vault.rst @@ -1,5 +1,5 @@ -salt.runners.vault module -========================= +salt.runners.vault +================== .. automodule:: salt.runners.vault :members: diff --git a/doc/ref/runners/all/salt.runners.venafiapi.rst b/doc/ref/runners/all/salt.runners.venafiapi.rst index 9fd9c41de4..d7e4d545eb 100644 --- a/doc/ref/runners/all/salt.runners.venafiapi.rst +++ b/doc/ref/runners/all/salt.runners.venafiapi.rst @@ -1,5 +1,5 @@ -salt.runners.venafiapi module -============================= +salt.runners.venafiapi +====================== .. automodule:: salt.runners.venafiapi :members: diff --git a/doc/ref/states/aggregate.rst b/doc/ref/states/aggregate.rst index e8aa61f689..ce25507a1c 100644 --- a/doc/ref/states/aggregate.rst +++ b/doc/ref/states/aggregate.rst @@ -122,7 +122,7 @@ This example, simplified from the pkg state, shows how to create mod_aggregate f for chunk in chunks: # The state runtime uses "tags" to track completed jobs, it may # look familiar with the _|- - tag = salt.utils.gen_state_tag(chunk) + tag = __utils__['state.gen_tag'](chunk) if tag in running: # Already ran the pkg state, skip aggregation continue diff --git a/doc/ref/states/all/index.rst b/doc/ref/states/all/index.rst index 4803648006..0b681ace7e 100644 --- a/doc/ref/states/all/index.rst +++ b/doc/ref/states/all/index.rst @@ -31,6 +31,7 @@ state modules boto_apigateway boto_asg boto_cfn + boto_cloudfront boto_cloudtrail boto_cloudwatch_alarm boto_cloudwatch_event @@ -179,6 +180,7 @@ state modules netusers network netyang + nfs_export nftables npm ntp diff --git a/doc/ref/states/all/salt.states.boto_cloudfront.rst b/doc/ref/states/all/salt.states.boto_cloudfront.rst new file mode 100644 index 0000000000..671965b2dc --- /dev/null +++ b/doc/ref/states/all/salt.states.boto_cloudfront.rst @@ -0,0 +1,6 @@ +=========================== +salt.states.boto_cloudfront +=========================== + +.. automodule:: salt.states.boto_cloudfront + :members: diff --git a/doc/ref/states/all/salt.states.nfs_export.rst b/doc/ref/states/all/salt.states.nfs_export.rst new file mode 100644 index 0000000000..231992626b --- /dev/null +++ b/doc/ref/states/all/salt.states.nfs_export.rst @@ -0,0 +1,6 @@ +====================== +salt.states.nfs_export +====================== + +.. automodule:: salt.states.nfs_export + :members: \ No newline at end of file diff --git a/doc/ref/states/writing.rst b/doc/ref/states/writing.rst index f278df5294..5e94c1ccc3 100644 --- a/doc/ref/states/writing.rst +++ b/doc/ref/states/writing.rst @@ -153,7 +153,12 @@ A State Module must return a dict containing the following keys/values: However, if a state is going to fail and this can be determined in test mode without applying the change, ``False`` can be returned. -- **comment:** A string containing a summary of the result. +- **comment:** A list of strings or a single string summarizing the result. + Note that support for lists of strings is available as of Salt Oxygen. + Lists of strings will be joined with newlines to form the final comment; + this is useful to allow multiple comments from subparts of a state. + Prefer to keep line lengths short (use multiple lines as needed), + and end with punctuation (e.g. a period) to delimit multiple comments. The return data can also, include the **pchanges** key, this stands for `predictive changes`. The **pchanges** key informs the State system what diff --git a/doc/topics/beacons/index.rst b/doc/topics/beacons/index.rst index e68d3d6dd5..effefc4e0f 100644 --- a/doc/topics/beacons/index.rst +++ b/doc/topics/beacons/index.rst @@ -253,9 +253,8 @@ in ``/etc/salt/master.d/reactor.conf``: .. note:: You can have only one top level ``reactor`` section, so if one already - exists, add this code to the existing section. See :ref:`Understanding the - Structure of Reactor Formulas ` to learn more about - reactor SLS syntax. + exists, add this code to the existing section. See :ref:`here + ` to learn more about reactor SLS syntax. Start the Salt Master in Debug Mode diff --git a/doc/topics/cloud/cloud.rst b/doc/topics/cloud/cloud.rst index c88a5eccb7..568866a7e4 100644 --- a/doc/topics/cloud/cloud.rst +++ b/doc/topics/cloud/cloud.rst @@ -183,7 +183,7 @@ imports should be absent from the Salt Cloud module. A good example of a non-libcloud driver is the DigitalOcean driver: -https://github.com/saltstack/salt/tree/develop/salt/cloud/clouds/digital_ocean.py +https://github.com/saltstack/salt/tree/develop/salt/cloud/clouds/digitalocean.py The ``create()`` Function ------------------------- diff --git a/doc/topics/cloud/config.rst b/doc/topics/cloud/config.rst index 8028aa414f..173ea4e692 100644 --- a/doc/topics/cloud/config.rst +++ b/doc/topics/cloud/config.rst @@ -444,7 +444,7 @@ under the API Access tab. .. code-block:: yaml my-digitalocean-config: - driver: digital_ocean + driver: digitalocean personal_access_token: xxx location: New York 1 diff --git a/doc/topics/cloud/digitalocean.rst b/doc/topics/cloud/digitalocean.rst index e89faf1a5c..dd7c76d91f 100644 --- a/doc/topics/cloud/digitalocean.rst +++ b/doc/topics/cloud/digitalocean.rst @@ -19,7 +19,7 @@ under the "SSH Keys" section. # /etc/salt/cloud.providers.d/ directory. my-digitalocean-config: - driver: digital_ocean + driver: digitalocean personal_access_token: xxx ssh_key_file: /path/to/ssh/key/file ssh_key_names: my-key-name,my-key-name-2 @@ -63,7 +63,7 @@ command: # salt-cloud --list-locations my-digitalocean-config my-digitalocean-config: ---------- - digital_ocean: + digitalocean: ---------- Amsterdam 1: ---------- @@ -87,7 +87,7 @@ command: # salt-cloud --list-sizes my-digitalocean-config my-digitalocean-config: ---------- - digital_ocean: + digitalocean: ---------- 512MB: ---------- @@ -117,7 +117,7 @@ command: # salt-cloud --list-images my-digitalocean-config my-digitalocean-config: ---------- - digital_ocean: + digitalocean: ---------- 10.1: ---------- @@ -142,7 +142,7 @@ Profile Specifics: ssh_username ------------ -If using a FreeBSD image from Digital Ocean, you'll need to set the ``ssh_username`` +If using a FreeBSD image from DigitalOcean, you'll need to set the ``ssh_username`` setting to ``freebsd`` in your profile configuration. .. code-block:: yaml diff --git a/doc/topics/development/contributing.rst b/doc/topics/development/contributing.rst index fd21d86a23..7941397682 100644 --- a/doc/topics/development/contributing.rst +++ b/doc/topics/development/contributing.rst @@ -263,9 +263,17 @@ against that branch. Release Branches ---------------- -For each release a branch will be created when we are ready to tag. The branch will be the same name as the tag minus the v. For example, the v2017.7.1 release was created from the 2017.7.1 branch. This branching strategy will allow for more stability when there is a need for a re-tag during the testing phase of our releases. +For each release, a branch will be created when the SaltStack release team is +ready to tag. The release branch is created from the parent branch and will be +the same name as the tag minus the ``v``. For example, the ``2017.7.1`` release +branch was created from the ``2017.7`` parent branch and the ``v2017.7.1`` +release was tagged at the ``HEAD`` of the ``2017.7.1`` branch. This branching +strategy will allow for more stability when there is a need for a re-tag during +the testing phase of the release process. -Once the branch is created, the fixes required for a given release, as determined by the SaltStack release team, will be added to this branch. All commits in this branch will be merged forward into the parent branch as well. +Once the release branch is created, the fixes required for a given release, as +determined by the SaltStack release team, will be added to this branch. All +commits in this branch will be merged forward into the parent branch as well. Keeping Salt Forks in Sync ========================== diff --git a/doc/topics/development/tests/index.rst b/doc/topics/development/tests/index.rst index 2679ebf0fd..f743b1da09 100644 --- a/doc/topics/development/tests/index.rst +++ b/doc/topics/development/tests/index.rst @@ -219,7 +219,7 @@ the default cloud provider configuration file for DigitalOcean looks like this: .. code-block:: yaml digitalocean-config: - driver: digital_ocean + driver: digitalocean client_key: '' api_key: '' location: New York 1 @@ -230,7 +230,7 @@ must be provided: .. code-block:: yaml digitalocean-config: - driver: digital_ocean + driver: digitalocean client_key: wFGEwgregeqw3435gDger api_key: GDE43t43REGTrkilg43934t34qT43t4dgegerGEgg location: New York 1 diff --git a/doc/topics/development/tests/integration.rst b/doc/topics/development/tests/integration.rst index c6140abda5..79c1eb3a48 100644 --- a/doc/topics/development/tests/integration.rst +++ b/doc/topics/development/tests/integration.rst @@ -541,7 +541,7 @@ provider configuration file in the integration test file directory located at ``tests/integration/files/conf/cloud.*.d/``. The following is an example of the default profile configuration file for Digital -Ocean, located at: ``tests/integration/files/conf/cloud.profiles.d/digital_ocean.conf``: +Ocean, located at: ``tests/integration/files/conf/cloud.profiles.d/digitalocean.conf``: .. code-block:: yaml @@ -557,12 +557,12 @@ be provided by the user by editing the provider configuration file before runnin tests. The following is an example of the default provider configuration file for Digital -Ocean, located at: ``tests/integration/files/conf/cloud.providers.d/digital_ocean.conf``: +Ocean, located at: ``tests/integration/files/conf/cloud.providers.d/digitalocean.conf``: .. code-block:: yaml digitalocean-config: - driver: digital_ocean + driver: digitalocean client_key: '' api_key: '' location: New York 1 diff --git a/doc/topics/engines/index.rst b/doc/topics/engines/index.rst index 5fbc99a0b9..c9a8ef5235 100644 --- a/doc/topics/engines/index.rst +++ b/doc/topics/engines/index.rst @@ -27,7 +27,12 @@ Salt engines are configured under an ``engines`` top-level section in your Salt port: 5959 proto: tcp -Salt engines must be in the Salt path, or you can add the ``engines_dirs`` option in your Salt master configuration with a list of directories under which Salt attempts to find Salt engines. +Salt engines must be in the Salt path, or you can add the ``engines_dirs`` option in your Salt master configuration with a list of directories under which Salt attempts to find Salt engines. This option should be formatted as a list of directories to search, such as: + +.. code-block:: yaml + + engines_dirs: + - /home/bob/engines Writing an Engine ================= diff --git a/doc/topics/installation/debian.rst b/doc/topics/installation/debian.rst index 36a47fa8ff..369991ebaa 100644 --- a/doc/topics/installation/debian.rst +++ b/doc/topics/installation/debian.rst @@ -18,7 +18,7 @@ Installation from official Debian and Raspbian repositories is described Installation from the Official SaltStack Repository =================================================== -Packages for Debian 8 (Jessie) and Debian 7 (Wheezy) are available in the +Packages for Debian 9 (Stretch) and Debian 8 (Jessie) are available in the Official SaltStack repository. Instructions are at https://repo.saltstack.com/#debian. diff --git a/doc/topics/reactor/index.rst b/doc/topics/reactor/index.rst index 2586245a1a..de5df946ac 100644 --- a/doc/topics/reactor/index.rst +++ b/doc/topics/reactor/index.rst @@ -27,9 +27,9 @@ event bus is an open system used for sending information notifying Salt and other systems about operations. The event system fires events with a very specific criteria. Every event has a -:strong:`tag`. Event tags allow for fast top level filtering of events. In -addition to the tag, each event has a data structure. This data structure is a -dict, which contains information about the event. +**tag**. Event tags allow for fast top-level filtering of events. In addition +to the tag, each event has a data structure. This data structure is a +dictionary, which contains information about the event. .. _reactor-mapping-events: @@ -65,15 +65,12 @@ and each event tag has a list of reactor SLS files to be run. the :ref:`querystring syntax ` (e.g. ``salt://reactor/mycustom.sls?saltenv=reactor``). -Reactor sls files are similar to state and pillar sls files. They are -by default yaml + Jinja templates and are passed familiar context variables. +Reactor SLS files are similar to State and Pillar SLS files. They are by +default YAML + Jinja templates and are passed familiar context variables. +Click :ref:`here ` for more detailed information on the +variables availble in Jinja templating. -They differ because of the addition of the ``tag`` and ``data`` variables. - -- The ``tag`` variable is just the tag in the fired event. -- The ``data`` variable is the event's data dict. - -Here is a simple reactor sls: +Here is the SLS for a simple reaction: .. code-block:: jinja @@ -90,71 +87,278 @@ data structure and compiler used for the state system is used for the reactor system. The only difference is that the data is matched up to the salt command API and the runner system. In this example, a command is published to the ``mysql1`` minion with a function of :py:func:`state.apply -`. Similarly, a runner can be called: +`, which performs a :ref:`highstate +`. Similarly, a runner can be called: .. code-block:: jinja {% if data['data']['custom_var'] == 'runit' %} call_runit_orch: runner.state.orchestrate: - - mods: _orch.runit + - args: + - mods: orchestrate.runit {% endif %} This example will execute the state.orchestrate runner and intiate an execution -of the runit orchestrator located at ``/srv/salt/_orch/runit.sls``. Using -``_orch/`` is any arbitrary path but it is recommended to avoid using "orchestrate" -as this is most likely to cause confusion. +of the ``runit`` orchestrator located at ``/srv/salt/orchestrate/runit.sls``. -Writing SLS Files ------------------ +Types of Reactions +================== -Reactor SLS files are stored in the same location as State SLS files. This means -that both ``file_roots`` and ``gitfs_remotes`` impact what SLS files are -available to the reactor and orchestrator. +============================== ================================================================================== +Name Description +============================== ================================================================================== +:ref:`local ` Runs a :ref:`remote-execution function ` on targeted minions +:ref:`runner ` Executes a :ref:`runner function ` +:ref:`wheel ` Executes a :ref:`wheel function ` on the master +:ref:`caller ` Runs a :ref:`remote-execution function ` on a masterless minion +============================== ================================================================================== -It is recommended to keep reactor and orchestrator SLS files in their own uniquely -named subdirectories such as ``_orch/``, ``orch/``, ``_orchestrate/``, ``react/``, -``_reactor/``, etc. Keeping a unique name helps prevent confusion when trying to -read through this a few years down the road. +.. note:: + The ``local`` and ``caller`` reaction types will be renamed for the Oxygen + release. These reaction types were named after Salt's internal client + interfaces, and are not intuitively named. Both ``local`` and ``caller`` + will continue to work in Reactor SLS files, but for the Oxygen release the + documentation will be updated to reflect the new preferred naming. -The Goal of Writing Reactor SLS Files -===================================== +Where to Put Reactor SLS Files +============================== -Reactor SLS files share the familiar syntax from Salt States but there are -important differences. The goal of a Reactor file is to process a Salt event as -quickly as possible and then to optionally start a **new** process in response. +Reactor SLS files can come both from files local to the master, and from any of +backends enabled via the :conf_master:`fileserver_backend` config option. Files +placed in the Salt fileserver can be referenced using a ``salt://`` URL, just +like they can in State SLS files. -1. The Salt Reactor watches Salt's event bus for new events. -2. The event tag is matched against the list of event tags under the - ``reactor`` section in the Salt Master config. -3. The SLS files for any matches are Rendered into a data structure that - represents one or more function calls. -4. That data structure is given to a pool of worker threads for execution. +It is recommended to place reactor and orchestrator SLS files in their own +uniquely-named subdirectories such as ``orch/``, ``orchestrate/``, ``react/``, +``reactor/``, etc., to keep them organized. + +.. _reactor-sls: + +Writing Reactor SLS +=================== + +The different reaction types were developed separately and have historically +had different methods for passing arguments. For the 2017.7.2 release a new, +unified configuration schema has been introduced, which applies to all reaction +types. + +The old config schema will continue to be supported, and there is no plan to +deprecate it at this time. + +.. _reactor-local: + +Local Reactions +--------------- + +A ``local`` reaction runs a :ref:`remote-execution function ` +on the targeted minions. + +The old config schema required the positional and keyword arguments to be +manually separated by the user under ``arg`` and ``kwarg`` parameters. However, +this is not very user-friendly, as it forces the user to distinguish which type +of argument is which, and make sure that positional arguments are ordered +properly. Therefore, the new config schema is recommended if the master is +running a supported release. + +The below two examples are equivalent: + ++---------------------------------+-----------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++=================================+=============================+ +| :: | :: | +| | | +| install_zsh: | install_zsh: | +| local.state.single: | local.state.single: | +| - tgt: 'kernel:Linux' | - tgt: 'kernel:Linux' | +| - tgt_type: grain | - tgt_type: grain | +| - args: | - arg: | +| - fun: pkg.installed | - pkg.installed | +| - name: zsh | - zsh | +| - fromrepo: updates | - kwarg: | +| | fromrepo: updates | ++---------------------------------+-----------------------------+ + +This reaction would be equvalent to running the following Salt command: + +.. code-block:: bash + + salt -G 'kernel:Linux' state.single pkg.installed name=zsh fromrepo=updates + +.. note:: + Any other parameters in the :py:meth:`LocalClient().cmd_async() + ` method can be passed at the same + indentation level as ``tgt``. + +.. note:: + ``tgt_type`` is only required when the target expression defined in ``tgt`` + uses a :ref:`target type ` other than a minion ID glob. + + The ``tgt_type`` argument was named ``expr_form`` in releases prior to + 2017.7.0. + +.. _reactor-runner: + +Runner Reactions +---------------- + +Runner reactions execute :ref:`runner functions ` locally on +the master. + +The old config schema called for passing arguments to the reaction directly +under the name of the runner function. However, this can cause unpredictable +interactions with the Reactor system's internal arguments. It is also possible +to pass positional and keyword arguments under ``arg`` and ``kwarg`` like above +in :ref:`local reactions `, but as noted above this is not very +user-friendly. Therefore, the new config schema is recommended if the master +is running a supported release. + +The below two examples are equivalent: + ++-------------------------------------------------+-------------------------------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++=================================================+=================================================+ +| :: | :: | +| | | +| deploy_app: | deploy_app: | +| runner.state.orchestrate: | runner.state.orchestrate: | +| - args: | - mods: orchestrate.deploy_app | +| - mods: orchestrate.deploy_app | - kwarg: | +| - pillar: | pillar: | +| event_tag: {{ tag }} | event_tag: {{ tag }} | +| event_data: {{ data['data']|json }} | event_data: {{ data['data']|json }} | ++-------------------------------------------------+-------------------------------------------------+ + +Assuming that the event tag is ``foo``, and the data passed to the event is +``{'bar': 'baz'}``, then this reaction is equvalent to running the following +Salt command: + +.. code-block:: bash + + salt-run state.orchestrate mods=orchestrate.deploy_app pillar='{"event_tag": "foo", "event_data": {"bar": "baz"}}' + +.. _reactor-wheel: + +Wheel Reactions +--------------- + +Wheel reactions run :ref:`wheel functions ` locally on the +master. + +Like :ref:`runner reactions `, the old config schema called for +wheel reactions to have arguments passed directly under the name of the +:ref:`wheel function ` (or in ``arg`` or ``kwarg`` parameters). + +The below two examples are equivalent: + ++-----------------------------------+---------------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++===================================+=================================+ +| :: | :: | +| | | +| remove_key: | remove_key: | +| wheel.key.delete: | wheel.key.delete: | +| - args: | - match: {{ data['id'] }} | +| - match: {{ data['id'] }} | | ++-----------------------------------+---------------------------------+ + +.. _reactor-caller: + +Caller Reactions +---------------- + +Caller reactions run :ref:`remote-execution functions ` on a +minion daemon's Reactor system. To run a Reactor on the minion, it is necessary +to configure the :mod:`Reactor Engine ` in the minion +config file, and then setup your watched events in a ``reactor`` section in the +minion config file as well. + +.. note:: Masterless Minions use this Reactor + + This is the only way to run the Reactor if you use masterless minions. + +Both the old and new config schemas involve passing arguments under an ``args`` +parameter. However, the old config schema only supports positional arguments. +Therefore, the new config schema is recommended if the masterless minion is +running a supported release. + +The below two examples are equivalent: + ++---------------------------------+---------------------------+ +| Supported in 2017.7.2 and later | Supported in all releases | ++=================================+===========================+ +| :: | :: | +| | | +| touch_file: | touch_file: | +| caller.file.touch: | caller.file.touch: | +| - args: | - args: | +| - name: /tmp/foo | - /tmp/foo | ++---------------------------------+---------------------------+ + +This reaction is equvalent to running the following Salt command: + +.. code-block:: bash + + salt-call file.touch name=/tmp/foo + +Best Practices for Writing Reactor SLS Files +============================================ + +The Reactor works as follows: + +1. The Salt Reactor watches Salt's event bus for new events. +2. Each event's tag is matched against the list of event tags configured under + the :conf_master:`reactor` section in the Salt Master config. +3. The SLS files for any matches are rendered into a data structure that + represents one or more function calls. +4. That data structure is given to a pool of worker threads for execution. Matching and rendering Reactor SLS files is done sequentially in a single -process. Complex Jinja that calls out to slow Execution or Runner modules slows -down the rendering and causes other reactions to pile up behind the current -one. The worker pool is designed to handle complex and long-running processes -such as Salt Orchestrate. +process. For that reason, reactor SLS files should contain few individual +reactions (one, if at all possible). Also, keep in mind that reactions are +fired asynchronously (with the exception of :ref:`caller `) and +do *not* support :ref:`requisites `. -tl;dr: Rendering Reactor SLS files MUST be simple and quick. The new process -started by the worker threads can be long-running. Using the reactor to fire -an orchestrate runner would be ideal. +Complex Jinja templating that calls out to slow :ref:`remote-execution +` or :ref:`runner ` functions slows down +the rendering and causes other reactions to pile up behind the current one. The +worker pool is designed to handle complex and long-running processes like +:ref:`orchestration ` jobs. + +Therefore, when complex tasks are in order, :ref:`orchestration +` is a natural fit. Orchestration SLS files can be more +complex, and use requisites. Performing a complex task using orchestration lets +the Reactor system fire off the orchestration job and proceed with processing +other reactions. + +.. _reactor-jinja-context: Jinja Context -------------- +============= -Reactor files only have access to a minimal Jinja context. ``grains`` and -``pillar`` are not available. The ``salt`` object is available for calling -Runner and Execution modules but it should be used sparingly and only for quick -tasks for the reasons mentioned above. +Reactor SLS files only have access to a minimal Jinja context. ``grains`` and +``pillar`` are *not* available. The ``salt`` object is available for calling +:ref:`remote-execution ` or :ref:`runner ` +functions, but it should be used sparingly and only for quick tasks for the +reasons mentioned above. + +In addition to the ``salt`` object, the following variables are available in +the Jinja context: + +- ``tag`` - the tag from the event that triggered execution of the Reactor SLS + file +- ``data`` - the event's data dictionary + +The ``data`` dict will contain an ``id`` key containing the minion ID, if the +event was fired from a minion, and a ``data`` key containing the data passed to +the event. Advanced State System Capabilities ----------------------------------- +================================== -Reactor SLS files, by design, do not support Requisites, ordering, -``onlyif``/``unless`` conditionals and most other powerful constructs from -Salt's State system. +Reactor SLS files, by design, do not support :ref:`requisites `, +ordering, ``onlyif``/``unless`` conditionals and most other powerful constructs +from Salt's State system. Complex Master-side operations are best performed by Salt's Orchestrate system so using the Reactor to kick off an Orchestrate run is a very common pairing. @@ -166,7 +370,7 @@ For example: # /etc/salt/master.d/reactor.conf # A custom event containing: {"foo": "Foo!", "bar: "bar*", "baz": "Baz!"} reactor: - - myco/custom/event: + - my/custom/event: - /srv/reactor/some_event.sls .. code-block:: jinja @@ -174,15 +378,15 @@ For example: # /srv/reactor/some_event.sls invoke_orchestrate_file: runner.state.orchestrate: - - mods: _orch.do_complex_thing # /srv/salt/_orch/do_complex_thing.sls - - kwarg: - pillar: - event_tag: {{ tag }} - event_data: {{ data|json() }} + - args: + - mods: orchestrate.do_complex_thing + - pillar: + event_tag: {{ tag }} + event_data: {{ data|json }} .. code-block:: jinja - # /srv/salt/_orch/do_complex_thing.sls + # /srv/salt/orchestrate/do_complex_thing.sls {% set tag = salt.pillar.get('event_tag') %} {% set data = salt.pillar.get('event_data') %} @@ -209,7 +413,7 @@ For example: .. _beacons-and-reactors: Beacons and Reactors --------------------- +==================== An event initiated by a beacon, when it arrives at the master will be wrapped inside a second event, such that the data object containing the beacon @@ -219,27 +423,52 @@ For example, to access the ``id`` field of the beacon event in a reactor file, you will need to reference ``{{ data['data']['id'] }}`` rather than ``{{ data['id'] }}`` as for events initiated directly on the event bus. +Similarly, the data dictionary attached to the event would be located in +``{{ data['data']['data'] }}`` instead of ``{{ data['data'] }}``. + See the :ref:`beacon documentation ` for examples. -Fire an event -============= +Manually Firing an Event +======================== -To fire an event from a minion call ``event.send`` +From the Master +--------------- + +Use the :py:func:`event.send ` runner: .. code-block:: bash - salt-call event.send 'foo' '{orchestrate: refresh}' + salt-run event.send foo '{orchestrate: refresh}' -After this is called, any reactor sls files matching event tag ``foo`` will -execute with ``{{ data['data']['orchestrate'] }}`` equal to ``'refresh'``. +From the Minion +--------------- -See :py:mod:`salt.modules.event` for more information. +To fire an event to the master from a minion, call :py:func:`event.send +`: -Knowing what event is being fired -================================= +.. code-block:: bash -The best way to see exactly what events are fired and what data is available in -each event is to use the :py:func:`state.event runner + salt-call event.send foo '{orchestrate: refresh}' + +To fire an event to the minion's local event bus, call :py:func:`event.fire +`: + +.. code-block:: bash + + salt-call event.fire '{orchestrate: refresh}' foo + +Referencing Data Passed in Events +--------------------------------- + +Assuming any of the above examples, any reactor SLS files triggered by watching +the event tag ``foo`` will execute with ``{{ data['data']['orchestrate'] }}`` +equal to ``'refresh'``. + +Getting Information About Events +================================ + +The best way to see exactly what events have been fired and what data is +available in each event is to use the :py:func:`state.event runner `. .. seealso:: :ref:`Common Salt Events ` @@ -308,156 +537,10 @@ rendered SLS file (or any errors generated while rendering the SLS file). view the result of referencing Jinja variables. If the result is empty then Jinja produced an empty result and the Reactor will ignore it. -.. _reactor-structure: +Passing Event Data to Minions or Orchestration as Pillar +-------------------------------------------------------- -Understanding the Structure of Reactor Formulas -=============================================== - -**I.e., when to use `arg` and `kwarg` and when to specify the function -arguments directly.** - -While the reactor system uses the same basic data structure as the state -system, the functions that will be called using that data structure are -different functions than are called via Salt's state system. The Reactor can -call Runner modules using the `runner` prefix, Wheel modules using the `wheel` -prefix, and can also cause minions to run Execution modules using the `local` -prefix. - -.. versionchanged:: 2014.7.0 - The ``cmd`` prefix was renamed to ``local`` for consistency with other - parts of Salt. A backward-compatible alias was added for ``cmd``. - -The Reactor runs on the master and calls functions that exist on the master. In -the case of Runner and Wheel functions the Reactor can just call those -functions directly since they exist on the master and are run on the master. - -In the case of functions that exist on minions and are run on minions, the -Reactor still needs to call a function on the master in order to send the -necessary data to the minion so the minion can execute that function. - -The Reactor calls functions exposed in :ref:`Salt's Python API documentation -`. and thus the structure of Reactor files very transparently -reflects the function signatures of those functions. - -Calling Execution modules on Minions ------------------------------------- - -The Reactor sends commands down to minions in the exact same way Salt's CLI -interface does. It calls a function locally on the master that sends the name -of the function as well as a list of any arguments and a dictionary of any -keyword arguments that the minion should use to execute that function. - -Specifically, the Reactor calls the async version of :py:meth:`this function -`. You can see that function has 'arg' and 'kwarg' -parameters which are both values that are sent down to the minion. - -Executing remote commands maps to the :strong:`LocalClient` interface which is -used by the :strong:`salt` command. This interface more specifically maps to -the :strong:`cmd_async` method inside of the :strong:`LocalClient` class. This -means that the arguments passed are being passed to the :strong:`cmd_async` -method, not the remote method. A field starts with :strong:`local` to use the -:strong:`LocalClient` subsystem. The result is, to execute a remote command, -a reactor formula would look like this: - -.. code-block:: yaml - - clean_tmp: - local.cmd.run: - - tgt: '*' - - arg: - - rm -rf /tmp/* - -The ``arg`` option takes a list of arguments as they would be presented on the -command line, so the above declaration is the same as running this salt -command: - -.. code-block:: bash - - salt '*' cmd.run 'rm -rf /tmp/*' - -Use the ``tgt_type`` argument to specify a matcher: - -.. code-block:: yaml - - clean_tmp: - local.cmd.run: - - tgt: 'os:Ubuntu' - - tgt_type: grain - - arg: - - rm -rf /tmp/* - - - clean_tmp: - local.cmd.run: - - tgt: 'G@roles:hbase_master' - - tgt_type: compound - - arg: - - rm -rf /tmp/* - -.. note:: - The ``tgt_type`` argument was named ``expr_form`` in releases prior to - 2017.7.0 (2016.11.x and earlier). - -Any other parameters in the :py:meth:`LocalClient().cmd() -` method can be specified as well. - -Executing Reactors from the Minion ----------------------------------- - -The minion can be setup to use the Reactor via a reactor engine. This just -sets up and listens to the minions event bus, instead of to the masters. - -The biggest difference is that you have to use the caller method on the -Reactor, which is the equivalent of salt-call, to run your commands. - -:mod:`Reactor Engine setup ` - -.. code-block:: yaml - - clean_tmp: - caller.cmd.run: - - arg: - - rm -rf /tmp/* - -.. note:: Masterless Minions use this Reactor - - This is the only way to run the Reactor if you use masterless minions. - -Calling Runner modules and Wheel modules ----------------------------------------- - -Calling Runner modules and Wheel modules from the Reactor uses a more direct -syntax since the function is being executed locally instead of sending a -command to a remote system to be executed there. There are no 'arg' or 'kwarg' -parameters (unless the Runner function or Wheel function accepts a parameter -with either of those names.) - -For example: - -.. code-block:: yaml - - clear_the_grains_cache_for_all_minions: - runner.cache.clear_grains - -If the :py:func:`the runner takes arguments ` then -they must be specified as keyword arguments. - -.. code-block:: yaml - - spin_up_more_web_machines: - runner.cloud.profile: - - prof: centos_6 - - instances: - - web11 # These VM names would be generated via Jinja in a - - web12 # real-world example. - -To determine the proper names for the arguments, check the documentation -or source code for the runner function you wish to call. - -Passing event data to Minions or Orchestrate as Pillar ------------------------------------------------------- - -An interesting trick to pass data from the Reactor script to +An interesting trick to pass data from the Reactor SLS file to :py:func:`state.apply ` is to pass it as inline Pillar data since both functions take a keyword argument named ``pillar``. @@ -484,10 +567,9 @@ from the event to the state file via inline Pillar. add_new_minion_to_pool: local.state.apply: - tgt: 'haproxy*' - - arg: - - haproxy.refresh_pool - - kwarg: - pillar: + - args: + - mods: haproxy.refresh_pool + - pillar: new_minion: {{ data['id'] }} {% endif %} @@ -503,17 +585,16 @@ This works with Orchestrate files as well: call_some_orchestrate_file: runner.state.orchestrate: - - mods: _orch.some_orchestrate_file - - pillar: - stuff: things + - args: + - mods: orchestrate.some_orchestrate_file + - pillar: + stuff: things Which is equivalent to the following command at the CLI: .. code-block:: bash - salt-run state.orchestrate _orch.some_orchestrate_file pillar='{stuff: things}' - -This expects to find a file at /srv/salt/_orch/some_orchestrate_file.sls. + salt-run state.orchestrate orchestrate.some_orchestrate_file pillar='{stuff: things}' Finally, that data is available in the state file using the normal Pillar lookup syntax. The following example is grabbing web server names and IP @@ -564,7 +645,7 @@ includes the minion id, which we can use for matching. - 'salt/minion/ink*/start': - /srv/reactor/auth-complete.sls -In this sls file, we say that if the key was rejected we will delete the key on +In this SLS file, we say that if the key was rejected we will delete the key on the master and then also tell the master to ssh in to the minion and tell it to restart the minion, since a minion process will die if the key is rejected. @@ -580,19 +661,21 @@ authentication every ten seconds by default. {% if not data['result'] and data['id'].startswith('ink') %} minion_remove: wheel.key.delete: - - match: {{ data['id'] }} + - args: + - match: {{ data['id'] }} minion_rejoin: local.cmd.run: - tgt: salt-master.domain.tld - - arg: - - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "{{ data['id'] }}" 'sleep 10 && /etc/init.d/salt-minion restart' + - args: + - cmd: ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "{{ data['id'] }}" 'sleep 10 && /etc/init.d/salt-minion restart' {% endif %} {# Ink server is sending new key -- accept this key #} {% if 'act' in data and data['act'] == 'pend' and data['id'].startswith('ink') %} minion_add: wheel.key.accept: - - match: {{ data['id'] }} + - args: + - match: {{ data['id'] }} {% endif %} No if statements are needed here because we already limited this action to just diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 327ebb6b81..2a6450fca9 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -51,6 +51,19 @@ New NaCl Renderer A new renderer has been added for encrypted data. +New support for Cisco UCS Chassis +--------------------------------- + +The salt proxy minion now allows for control of Cisco USC chassis. See +the `cimc` modules for details. + +New salt-ssh roster +------------------- + +A new roster has been added that allows users to pull in a list of hosts +for salt-ssh targeting from a ~/.ssh configuration. For full details, +please see the `sshconfig` roster. + New GitFS Features ------------------ @@ -154,6 +167,14 @@ Newer PyWinRM Versions Versions of ``pywinrm>=0.2.1`` are finally able to disable validation of self signed certificates. :ref:`Here` for more information. +DigitalOcean +------------ + +The DigitalOcean driver has been renamed to conform to the companies name. The +new driver name is ``digitalocean``. The old name ``digital_ocean`` and a +short one ``do`` will still be supported through virtual aliases, this is mostly +cosmetic. + Solaris Logical Domains In Virtual Grain ---------------------------------------- @@ -161,9 +182,15 @@ Support has been added to the ``virtual`` grain for detecting Solaris LDOMs running on T-Series SPARC hardware. The ``virtual_subtype`` grain is populated as a list of domain roles. +Lists of comments in state returns +---------------------------------- + +State functions can now return a list of strings for the ``comment`` field, +as opposed to only a single string. +This is meant to ease writing states with multiple or multi-part comments. Beacon configuration changes ----------------------------------------- +---------------------------- In order to remain consistent and to align with other Salt components such as states, support for configuring beacons using dictionary based configuration has been deprecated @@ -780,3 +807,7 @@ Other Miscellaneous Deprecations The ``version.py`` file had the following changes: - The ``rc_info`` function was removed. Please use ``pre_info`` instead. + +Warnings for moving away from the ``env`` option were removed. ``saltenv`` should be +used instead. The removal of these warnings does not have a behavior change. Only +the warning text was removed. diff --git a/doc/topics/tutorials/libcloud.rst b/doc/topics/tutorials/libcloud.rst index 793c37aaa8..a66c2e4e76 100644 --- a/doc/topics/tutorials/libcloud.rst +++ b/doc/topics/tutorials/libcloud.rst @@ -13,7 +13,7 @@ Using Apache Libcloud for declarative and procedural multi-cloud orchestration Apache Libcloud is a Python library which hides differences between different cloud provider APIs and allows you to manage different cloud resources through a unified and easy to use API. Apache Libcloud supports over -60 cloud platforms, including Amazon, Microsoft Azure, Digital Ocean, Google Cloud Platform and OpenStack. +60 cloud platforms, including Amazon, Microsoft Azure, DigitalOcean, Google Cloud Platform and OpenStack. Execution and state modules are available for Compute, DNS, Storage and Load Balancer drivers from Apache Libcloud in SaltStack. diff --git a/doc/topics/windows/windows-package-manager.rst b/doc/topics/windows/windows-package-manager.rst index 9d1838c807..20ed60baf6 100644 --- a/doc/topics/windows/windows-package-manager.rst +++ b/doc/topics/windows/windows-package-manager.rst @@ -481,11 +481,17 @@ Alternatively the ``uninstaller`` can also simply repeat the URL of the msi file :param bool allusers: This parameter is specific to `.msi` installations. It tells `msiexec` to install the software for all users. The default is True. -:param bool cache_dir: If true, the entire directory where the installer resides - will be recursively cached. This is useful for installers that depend on - other files in the same directory for installation. +:param bool cache_dir: If true when installer URL begins with salt://, the + entire directory where the installer resides will be recursively cached. + This is useful for installers that depend on other files in the same + directory for installation. -.. note:: Only applies to salt: installer URLs. +:param str cache_file: + When installer URL begins with salt://, this indicates single file to copy + down for use with the installer. Copied to the same location as the + installer. Use this over ``cache_dir`` if there are many files in the + directory and you only need a specific file and don't want to cache + additional files that may reside in the installer directory. Here's an example for a software package that has dependent files: diff --git a/pkg/osx/pkg-scripts/preinstall b/pkg/osx/pkg-scripts/preinstall index 3eb4235107..8c671c6df9 100755 --- a/pkg/osx/pkg-scripts/preinstall +++ b/pkg/osx/pkg-scripts/preinstall @@ -132,7 +132,7 @@ fi ############################################################################### # Remove the salt from the paths.d ############################################################################### -if [ ! -f "/etc/paths.d/salt" ]; then +if [ -f "/etc/paths.d/salt" ]; then echo "Path: Removing salt from the path..." >> "$TEMP_DIR/preinstall.txt" rm "/etc/paths.d/salt" echo "Path: Removed Successfully" >> "$TEMP_DIR/preinstall.txt" diff --git a/pkg/salt.bash b/pkg/salt.bash index 40928f33a5..7b9f166cdd 100644 --- a/pkg/salt.bash +++ b/pkg/salt.bash @@ -35,8 +35,9 @@ _salt_get_keys(){ } _salt(){ - local _salt_cache_functions=${SALT_COMP_CACHE_FUNCTIONS:-"$HOME/.cache/salt-comp-cache_functions"} - local _salt_cache_timeout=${SALT_COMP_CACHE_TIMEOUT:-"last hour"} + CACHE_DIR="$HOME/.cache/salt-comp-cache_functions" + local _salt_cache_functions=${SALT_COMP_CACHE_FUNCTIONS:=$CACHE_DIR} + local _salt_cache_timeout=${SALT_COMP_CACHE_TIMEOUT:='last hour'} if [ ! -d "$(dirname ${_salt_cache_functions})" ]; then mkdir -p "$(dirname ${_salt_cache_functions})" diff --git a/pkg/windows/build.bat b/pkg/windows/build.bat index 0117718539..59fafde137 100644 --- a/pkg/windows/build.bat +++ b/pkg/windows/build.bat @@ -89,7 +89,7 @@ if Defined x ( if %Python%==2 ( Set "PyDir=C:\Python27" ) else ( - Set "PyDir=C:\Program Files\Python35" + Set "PyDir=C:\Python35" ) Set "PATH=%PATH%;%PyDir%;%PyDir%\Scripts" diff --git a/pkg/windows/build_env_2.ps1 b/pkg/windows/build_env_2.ps1 index 98a922ca3d..b186517812 100644 --- a/pkg/windows/build_env_2.ps1 +++ b/pkg/windows/build_env_2.ps1 @@ -175,7 +175,7 @@ If (Test-Path "$($ini['Settings']['Python2Dir'])\python.exe") { DownloadFileWithProgress $url $file Write-Output " - $script_name :: Installing $($ini[$bitPrograms]['Python2']) . . ." - $p = Start-Process msiexec -ArgumentList "/i $file /qb ADDLOCAL=DefaultFeature,SharedCRT,Extensions,pip_feature,PrependPath TARGETDIR=$($ini['Settings']['Python2Dir'])" -Wait -NoNewWindow -PassThru + $p = Start-Process msiexec -ArgumentList "/i $file /qb ADDLOCAL=DefaultFeature,SharedCRT,Extensions,pip_feature,PrependPath TARGETDIR=`"$($ini['Settings']['Python2Dir'])`"" -Wait -NoNewWindow -PassThru } #------------------------------------------------------------------------------ @@ -191,7 +191,7 @@ If (!($Path.ToLower().Contains("$($ini['Settings']['Scripts2Dir'])".ToLower()))) #============================================================================== # Update PIP and SetupTools -# caching depends on environmant variable SALT_PIP_LOCAL_CACHE +# caching depends on environment variable SALT_PIP_LOCAL_CACHE #============================================================================== Write-Output " ----------------------------------------------------------------" Write-Output " - $script_name :: Updating PIP and SetupTools . . ." @@ -212,7 +212,7 @@ if ( ! [bool]$Env:SALT_PIP_LOCAL_CACHE) { #============================================================================== # Install pypi resources using pip -# caching depends on environmant variable SALT_REQ_LOCAL_CACHE +# caching depends on environment variable SALT_REQ_LOCAL_CACHE #============================================================================== Write-Output " ----------------------------------------------------------------" Write-Output " - $script_name :: Installing pypi resources using pip . . ." @@ -230,6 +230,24 @@ if ( ! [bool]$Env:SALT_REQ_LOCAL_CACHE) { Start_Process_and_test_exitcode "$($ini['Settings']['Python2Dir'])\python.exe" "-m pip install --no-index --find-links=$Env:SALT_REQ_LOCAL_CACHE -r $($script_path)\req_2.txt" "pip install" } +#============================================================================== +# Move PyWin32 DLL's to site-packages\win32 +#============================================================================== +Write-Output " - $script_name :: Moving PyWin32 DLLs . . ." +Move-Item "$($ini['Settings']['SitePkgs2Dir'])\pywin32_system32\*.dll" "$($ini['Settings']['SitePkgs2Dir'])\win32" -Force + +# Remove pywin32_system32 directory +Write-Output " - $script_name :: Removing pywin32_system32 Directory . . ." +Remove-Item "$($ini['Settings']['SitePkgs2Dir'])\pywin32_system32" + +# Remove pythonwin directory +Write-Output " - $script_name :: Removing pythonwin Directory . . ." +Remove-Item "$($ini['Settings']['SitePkgs2Dir'])\pythonwin" -Force -Recurse + +# Remove PyWin32 PostInstall and testall Scripts +Write-Output " - $script_name :: Removing PyWin32 scripts . . ." +Remove-Item "$($ini['Settings']['Scripts2Dir'])\pywin32_*" -Force -Recurse + #============================================================================== # Install PyYAML with CLoader # This has to be a compiled binary to get the CLoader diff --git a/pkg/windows/build_env_3.ps1 b/pkg/windows/build_env_3.ps1 index 33f95871ae..0dcbafd996 100644 --- a/pkg/windows/build_env_3.ps1 +++ b/pkg/windows/build_env_3.ps1 @@ -175,7 +175,7 @@ If (Test-Path "$($ini['Settings']['Python3Dir'])\python.exe") { DownloadFileWithProgress $url $file Write-Output " - $script_name :: Installing $($ini[$bitPrograms]['Python3']) . . ." - $p = Start-Process $file -ArgumentList '/passive InstallAllUsers=1 TargetDir="C:\Program Files\Python35" Include_doc=0 Include_tcltk=0 Include_test=0 Include_launcher=0 PrependPath=1 Shortcuts=0' -Wait -NoNewWindow -PassThru + $p = Start-Process $file -ArgumentList "/passive InstallAllUsers=1 TargetDir=`"$($ini['Settings']['Python3Dir'])`" Include_doc=0 Include_tcltk=0 Include_test=0 Include_launcher=0 PrependPath=1 Shortcuts=0" -Wait -NoNewWindow -PassThru } #------------------------------------------------------------------------------ @@ -247,7 +247,7 @@ Start_Process_and_test_exitcode "$($ini['Settings']['Scripts3Dir'])\pip.exe" "i # Move DLL's to Python Root Write-Output " - $script_name :: Moving PyWin32 DLLs . . ." -Move-Item "$($ini['Settings']['SitePkgs3Dir'])\pywin32_system32\*.dll" "$($ini['Settings']['Python3Dir'])" -Force +Move-Item "$($ini['Settings']['SitePkgs3Dir'])\pywin32_system32\*.dll" "$($ini['Settings']['SitePkgs3Dir'])\win32" -Force # Remove pywin32_system32 directory Write-Output " - $script_name :: Removing pywin32_system32 Directory . . ." @@ -257,6 +257,10 @@ Remove-Item "$($ini['Settings']['SitePkgs3Dir'])\pywin32_system32" Write-Output " - $script_name :: Removing pythonwin Directory . . ." Remove-Item "$($ini['Settings']['SitePkgs3Dir'])\pythonwin" -Force -Recurse +# Remove PyWin32 PostInstall and testall Scripts +Write-Output " - $script_name :: Removing PyWin32 scripts . . ." +Remove-Item "$($ini['Settings']['Scripts3Dir'])\pywin32_*" -Force -Recurse + #============================================================================== # Fix PyCrypto #============================================================================== diff --git a/pkg/windows/build_pkg.bat b/pkg/windows/build_pkg.bat index 0d30f047ac..95b185bfa7 100644 --- a/pkg/windows/build_pkg.bat +++ b/pkg/windows/build_pkg.bat @@ -56,7 +56,7 @@ if %Python%==2 ( Set "PyVerMajor=2" Set "PyVerMinor=7" ) else ( - Set "PyDir=C:\Program Files\Python35" + Set "PyDir=C:\Python35" Set "PyVerMajor=3" Set "PyVerMinor=5" ) diff --git a/pkg/windows/clean_env.bat b/pkg/windows/clean_env.bat index fb6e63a661..7c5e497802 100644 --- a/pkg/windows/clean_env.bat +++ b/pkg/windows/clean_env.bat @@ -16,9 +16,10 @@ if %errorLevel%==0 ( ) echo. +:CheckPython2 if exist "\Python27" goto RemovePython2 -if exist "\Program Files\Python35" goto RemovePython3 -goto eof + +goto CheckPython3 :RemovePython2 rem Uninstall Python 2.7 @@ -47,25 +48,30 @@ goto eof goto eof +:CheckPython3 +if exist "\Python35" goto RemovePython3 + +goto eof + :RemovePython3 echo %0 :: Uninstalling Python 3 ... echo --------------------------------------------------------------------- :: 64 bit if exist "%LOCALAPPDATA%\Package Cache\{b94f45d6-8461-440c-aa4d-bf197b2c2499}" ( echo %0 :: - 3.5.3 64bit - "%LOCALAPPDATA%\Package Cache\{b94f45d6-8461-440c-aa4d-bf197b2c2499}\python-3.5.3-amd64.exe" /uninstall + "%LOCALAPPDATA%\Package Cache\{b94f45d6-8461-440c-aa4d-bf197b2c2499}\python-3.5.3-amd64.exe" /uninstall /passive ) :: 32 bit if exist "%LOCALAPPDATA%\Package Cache\{a10037e1-4247-47c9-935b-c5ca049d0299}" ( echo %0 :: - 3.5.3 32bit - "%LOCALAPPDATA%\Package Cache\{a10037e1-4247-47c9-935b-c5ca049d0299}\python-3.5.3" /uninstall + "%LOCALAPPDATA%\Package Cache\{a10037e1-4247-47c9-935b-c5ca049d0299}\python-3.5.3" /uninstall /passive ) rem wipe the Python directory - echo %0 :: Removing the C:\Program Files\Python35 Directory ... + echo %0 :: Removing the C:\Python35 Directory ... echo --------------------------------------------------------------------- - rd /s /q "C:\Program Files\Python35" + rd /s /q "C:\Python35" if %errorLevel%==0 ( echo Successful ) else ( diff --git a/pkg/windows/installer/Salt-Minion-Setup.nsi b/pkg/windows/installer/Salt-Minion-Setup.nsi index 46fb821fb8..a8efca2101 100644 --- a/pkg/windows/installer/Salt-Minion-Setup.nsi +++ b/pkg/windows/installer/Salt-Minion-Setup.nsi @@ -44,7 +44,7 @@ ${StrStrAdv} !define CPUARCH "x86" !endif -; Part of the Trim function for Strings +# Part of the Trim function for Strings !define Trim "!insertmacro Trim" !macro Trim ResultVar String Push "${String}" @@ -61,27 +61,27 @@ ${StrStrAdv} !define MUI_UNICON "salt.ico" !define MUI_WELCOMEFINISHPAGE_BITMAP "panel.bmp" -; Welcome page +# Welcome page !insertmacro MUI_PAGE_WELCOME -; License page +# License page !insertmacro MUI_PAGE_LICENSE "LICENSE.txt" -; Configure Minion page +# Configure Minion page Page custom pageMinionConfig pageMinionConfig_Leave -; Instfiles page +# Instfiles page !insertmacro MUI_PAGE_INSTFILES -; Finish page (Customized) +# Finish page (Customized) !define MUI_PAGE_CUSTOMFUNCTION_SHOW pageFinish_Show !define MUI_PAGE_CUSTOMFUNCTION_LEAVE pageFinish_Leave !insertmacro MUI_PAGE_FINISH -; Uninstaller pages +# Uninstaller pages !insertmacro MUI_UNPAGE_INSTFILES -; Language files +# Language files !insertmacro MUI_LANGUAGE "English" @@ -201,8 +201,8 @@ ShowInstDetails show ShowUnInstDetails show -; Check and install Visual C++ redist packages -; See http://blogs.msdn.com/b/astebner/archive/2009/01/29/9384143.aspx for more info +# Check and install Visual C++ redist packages +# See http://blogs.msdn.com/b/astebner/archive/2009/01/29/9384143.aspx for more info Section -Prerequisites Var /GLOBAL VcRedistName @@ -211,12 +211,12 @@ Section -Prerequisites Var /Global CheckVcRedist StrCpy $CheckVcRedist "False" - ; Visual C++ 2015 redist packages + # Visual C++ 2015 redist packages !define PY3_VC_REDIST_NAME "VC_Redist_2015" !define PY3_VC_REDIST_X64_GUID "{50A2BC33-C9CD-3BF1-A8FF-53C10A0B183C}" !define PY3_VC_REDIST_X86_GUID "{BBF2AC74-720C-3CB3-8291-5E34039232FA}" - ; Visual C++ 2008 SP1 MFC Security Update redist packages + # Visual C++ 2008 SP1 MFC Security Update redist packages !define PY2_VC_REDIST_NAME "VC_Redist_2008_SP1_MFC" !define PY2_VC_REDIST_X64_GUID "{5FCE6D76-F5DC-37AB-B2B8-22AB8CEDB1D4}" !define PY2_VC_REDIST_X86_GUID "{9BE518E6-ECC6-35A9-88E4-87755C07200F}" @@ -239,7 +239,7 @@ Section -Prerequisites StrCpy $VcRedistGuid ${PY2_VC_REDIST_X86_GUID} ${EndIf} - ; VCRedist 2008 only needed on Windows Server 2008R2/Windows 7 and below + # VCRedist 2008 only needed on Windows Server 2008R2/Windows 7 and below ${If} ${AtMostWin2008R2} StrCpy $CheckVcRedist "True" ${EndIf} @@ -255,20 +255,41 @@ Section -Prerequisites "$VcRedistName is currently not installed. Would you like to install?" \ /SD IDYES IDNO endVcRedist - ClearErrors - ; The Correct version of VCRedist is copied over by "build_pkg.bat" + # The Correct version of VCRedist is copied over by "build_pkg.bat" SetOutPath "$INSTDIR\" File "..\prereqs\vcredist.exe" - ; /passive used by 2015 installer - ; /qb! used by 2008 installer - ; It just ignores the unrecognized switches... - ExecWait "$INSTDIR\vcredist.exe /qb! /passive" - IfErrors 0 endVcRedist + # If an output variable is specified ($0 in the case below), + # ExecWait sets the variable with the exit code (and only sets the + # error flag if an error occurs; if an error occurs, the contents + # of the user variable are undefined). + # http://nsis.sourceforge.net/Reference/ExecWait + # /passive used by 2015 installer + # /qb! used by 2008 installer + # It just ignores the unrecognized switches... + ClearErrors + ExecWait '"$INSTDIR\vcredist.exe" /qb! /passive /norestart' $0 + IfErrors 0 CheckVcRedistErrorCode MessageBox MB_OK \ "$VcRedistName failed to install. Try installing the package manually." \ /SD IDOK + Goto endVcRedist + + CheckVcRedistErrorCode: + # Check for Reboot Error Code (3010) + ${If} $0 == 3010 + MessageBox MB_OK \ + "$VcRedistName installed but requires a restart to complete." \ + /SD IDOK + + # Check for any other errors + ${ElseIfNot} $0 == 0 + MessageBox MB_OK \ + "$VcRedistName failed with ErrorCode: $0. Try installing the package manually." \ + /SD IDOK + ${EndIf} endVcRedist: + ${EndIf} ${EndIf} @@ -294,12 +315,12 @@ Function .onInit Call parseCommandLineSwitches - ; Check for existing installation + # Check for existing installation ReadRegStr $R0 HKLM \ "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ "UninstallString" StrCmp $R0 "" checkOther - ; Found existing installation, prompt to uninstall + # Found existing installation, prompt to uninstall MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \ "${PRODUCT_NAME} is already installed.$\n$\n\ Click `OK` to remove the existing installation." \ @@ -307,12 +328,12 @@ Function .onInit Abort checkOther: - ; Check for existing installation of full salt + # Check for existing installation of full salt ReadRegStr $R0 HKLM \ "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME_OTHER}" \ "UninstallString" StrCmp $R0 "" skipUninstall - ; Found existing installation, prompt to uninstall + # Found existing installation, prompt to uninstall MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \ "${PRODUCT_NAME_OTHER} is already installed.$\n$\n\ Click `OK` to remove the existing installation." \ @@ -321,22 +342,22 @@ Function .onInit uninst: - ; Get current Silent status + # Get current Silent status StrCpy $R0 0 ${If} ${Silent} StrCpy $R0 1 ${EndIf} - ; Turn on Silent mode + # Turn on Silent mode SetSilent silent - ; Don't remove all directories + # Don't remove all directories StrCpy $DeleteInstallDir 0 - ; Uninstall silently + # Uninstall silently Call uninstallSalt - ; Set it back to Normal mode, if that's what it was before + # Set it back to Normal mode, if that's what it was before ${If} $R0 == 0 SetSilent normal ${EndIf} @@ -350,7 +371,7 @@ Section -Post WriteUninstaller "$INSTDIR\uninst.exe" - ; Uninstall Registry Entries + # Uninstall Registry Entries WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" \ "DisplayName" "$(^Name)" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" \ @@ -366,19 +387,19 @@ Section -Post WriteRegStr HKLM "SYSTEM\CurrentControlSet\services\salt-minion" \ "DependOnService" "nsi" - ; Set the estimated size + # Set the estimated size ${GetSize} "$INSTDIR\bin" "/S=OK" $0 $1 $2 IntFmt $0 "0x%08X" $0 WriteRegDWORD ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" \ "EstimatedSize" "$0" - ; Commandline Registry Entries + # Commandline Registry Entries WriteRegStr HKLM "${PRODUCT_CALL_REGKEY}" "" "$INSTDIR\salt-call.bat" WriteRegStr HKLM "${PRODUCT_CALL_REGKEY}" "Path" "$INSTDIR\bin\" WriteRegStr HKLM "${PRODUCT_MINION_REGKEY}" "" "$INSTDIR\salt-minion.bat" WriteRegStr HKLM "${PRODUCT_MINION_REGKEY}" "Path" "$INSTDIR\bin\" - ; Register the Salt-Minion Service + # Register the Salt-Minion Service nsExec::Exec "nssm.exe install salt-minion $INSTDIR\bin\python.exe -E -s $INSTDIR\bin\Scripts\salt-minion -c $INSTDIR\conf -l quiet" nsExec::Exec "nssm.exe set salt-minion Description Salt Minion from saltstack.com" nsExec::Exec "nssm.exe set salt-minion Start SERVICE_AUTO_START" @@ -398,12 +419,12 @@ SectionEnd Function .onInstSuccess - ; If StartMinionDelayed is 1, then set the service to start delayed + # If StartMinionDelayed is 1, then set the service to start delayed ${If} $StartMinionDelayed == 1 nsExec::Exec "nssm.exe set salt-minion Start SERVICE_DELAYED_AUTO_START" ${EndIf} - ; If start-minion is 1, then start the service + # If start-minion is 1, then start the service ${If} $StartMinion == 1 nsExec::Exec 'net start salt-minion' ${EndIf} @@ -413,10 +434,11 @@ FunctionEnd Function un.onInit - ; Load the parameters + # Load the parameters ${GetParameters} $R0 # Uninstaller: Remove Installation Directory + ClearErrors ${GetOptions} $R0 "/delete-install-dir" $R1 IfErrors delete_install_dir_not_found StrCpy $DeleteInstallDir 1 @@ -434,7 +456,7 @@ Section Uninstall Call un.uninstallSalt - ; Remove C:\salt from the Path + # Remove C:\salt from the Path Push "C:\salt" Call un.RemoveFromPath @@ -444,27 +466,27 @@ SectionEnd !macro uninstallSalt un Function ${un}uninstallSalt - ; Make sure we're in the right directory + # Make sure we're in the right directory ${If} $INSTDIR == "c:\salt\bin\Scripts" StrCpy $INSTDIR "C:\salt" ${EndIf} - ; Stop and Remove salt-minion service + # Stop and Remove salt-minion service nsExec::Exec 'net stop salt-minion' nsExec::Exec 'sc delete salt-minion' - ; Stop and remove the salt-master service + # Stop and remove the salt-master service nsExec::Exec 'net stop salt-master' nsExec::Exec 'sc delete salt-master' - ; Remove files + # Remove files Delete "$INSTDIR\uninst.exe" Delete "$INSTDIR\nssm.exe" Delete "$INSTDIR\salt*" Delete "$INSTDIR\vcredist.exe" RMDir /r "$INSTDIR\bin" - ; Remove Registry entries + # Remove Registry entries DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY_OTHER}" DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_CALL_REGKEY}" @@ -474,17 +496,17 @@ Function ${un}uninstallSalt DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_MINION_REGKEY}" DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_RUN_REGKEY}" - ; Automatically close when finished + # Automatically close when finished SetAutoClose true - ; Prompt to remove the Installation directory + # Prompt to remove the Installation directory ${IfNot} $DeleteInstallDir == 1 MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 \ "Would you like to completely remove $INSTDIR and all of its contents?" \ /SD IDNO IDNO finished ${EndIf} - ; Make sure you're not removing Program Files + # Make sure you're not removing Program Files ${If} $INSTDIR != 'Program Files' ${AndIf} $INSTDIR != 'Program Files (x86)' RMDir /r "$INSTDIR" @@ -526,7 +548,7 @@ FunctionEnd Function Trim - Exch $R1 ; Original string + Exch $R1 # Original string Push $R2 Loop: @@ -558,36 +580,36 @@ Function Trim FunctionEnd -;------------------------------------------------------------------------------ -; StrStr Function -; - find substring in a string -; -; Usage: -; Push "this is some string" -; Push "some" -; Call StrStr -; Pop $0 ; "some string" -;------------------------------------------------------------------------------ +#------------------------------------------------------------------------------ +# StrStr Function +# - find substring in a string +# +# Usage: +# Push "this is some string" +# Push "some" +# Call StrStr +# Pop $0 ; "some string" +#------------------------------------------------------------------------------ !macro StrStr un Function ${un}StrStr - Exch $R1 ; $R1=substring, stack=[old$R1,string,...] - Exch ; stack=[string,old$R1,...] - Exch $R2 ; $R2=string, stack=[old$R2,old$R1,...] - Push $R3 ; $R3=strlen(substring) - Push $R4 ; $R4=count - Push $R5 ; $R5=tmp - StrLen $R3 $R1 ; Get the length of the Search String - StrCpy $R4 0 ; Set the counter to 0 + Exch $R1 # $R1=substring, stack=[old$R1,string,...] + Exch # stack=[string,old$R1,...] + Exch $R2 # $R2=string, stack=[old$R2,old$R1,...] + Push $R3 # $R3=strlen(substring) + Push $R4 # $R4=count + Push $R5 # $R5=tmp + StrLen $R3 $R1 # Get the length of the Search String + StrCpy $R4 0 # Set the counter to 0 loop: - StrCpy $R5 $R2 $R3 $R4 ; Create a moving window of the string that is - ; the size of the length of the search string - StrCmp $R5 $R1 done ; Is the contents of the window the same as - ; search string, then done - StrCmp $R5 "" done ; Is the window empty, then done - IntOp $R4 $R4 + 1 ; Shift the windows one character - Goto loop ; Repeat + StrCpy $R5 $R2 $R3 $R4 # Create a moving window of the string that is + # the size of the length of the search string + StrCmp $R5 $R1 done # Is the contents of the window the same as + # search string, then done + StrCmp $R5 "" done # Is the window empty, then done + IntOp $R4 $R4 + 1 # Shift the windows one character + Goto loop # Repeat done: StrCpy $R1 $R2 "" $R4 @@ -595,7 +617,7 @@ Function ${un}StrStr Pop $R4 Pop $R3 Pop $R2 - Exch $R1 ; $R1=old$R1, stack=[result,...] + Exch $R1 # $R1=old$R1, stack=[result,...] FunctionEnd !macroend @@ -603,74 +625,74 @@ FunctionEnd !insertmacro StrStr "un." -;------------------------------------------------------------------------------ -; AddToPath Function -; - Adds item to Path for All Users -; - Overcomes NSIS ReadRegStr limitation of 1024 characters by using Native -; Windows Commands -; -; Usage: -; Push "C:\path\to\add" -; Call AddToPath -;------------------------------------------------------------------------------ +#------------------------------------------------------------------------------ +# AddToPath Function +# - Adds item to Path for All Users +# - Overcomes NSIS ReadRegStr limitation of 1024 characters by using Native +# Windows Commands +# +# Usage: +# Push "C:\path\to\add" +# Call AddToPath +#------------------------------------------------------------------------------ !define Environ 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' Function AddToPath - Exch $0 ; Path to add - Push $1 ; Current Path - Push $2 ; Results of StrStr / Length of Path + Path to Add - Push $3 ; Handle to Reg / Length of Path - Push $4 ; Result of Registry Call + Exch $0 # Path to add + Push $1 # Current Path + Push $2 # Results of StrStr / Length of Path + Path to Add + Push $3 # Handle to Reg / Length of Path + Push $4 # Result of Registry Call - ; Open a handle to the key in the registry, handle in $3, Error in $4 + # Open a handle to the key in the registry, handle in $3, Error in $4 System::Call "advapi32::RegOpenKey(i 0x80000002, t'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', *i.r3) i.r4" - ; Make sure registry handle opened successfully (returned 0) + # Make sure registry handle opened successfully (returned 0) IntCmp $4 0 0 done done - ; Load the contents of path into $1, Error Code into $4, Path length into $2 + # Load the contents of path into $1, Error Code into $4, Path length into $2 System::Call "advapi32::RegQueryValueEx(i $3, t'PATH', i 0, i 0, t.r1, *i ${NSIS_MAX_STRLEN} r2) i.r4" - ; Close the handle to the registry ($3) + # Close the handle to the registry ($3) System::Call "advapi32::RegCloseKey(i $3)" - ; Check for Error Code 234, Path too long for the variable - IntCmp $4 234 0 +4 +4 ; $4 == ERROR_MORE_DATA + # Check for Error Code 234, Path too long for the variable + IntCmp $4 234 0 +4 +4 # $4 == ERROR_MORE_DATA DetailPrint "AddToPath Failed: original length $2 > ${NSIS_MAX_STRLEN}" MessageBox MB_OK \ "You may add C:\salt to the %PATH% for convenience when issuing local salt commands from the command line." \ /SD IDOK Goto done - ; If no error, continue - IntCmp $4 0 +5 ; $4 != NO_ERROR - ; Error 2 means the Key was not found - IntCmp $4 2 +3 ; $4 != ERROR_FILE_NOT_FOUND + # If no error, continue + IntCmp $4 0 +5 # $4 != NO_ERROR + # Error 2 means the Key was not found + IntCmp $4 2 +3 # $4 != ERROR_FILE_NOT_FOUND DetailPrint "AddToPath: unexpected error code $4" Goto done StrCpy $1 "" - ; Check if already in PATH - Push "$1;" ; The string to search - Push "$0;" ; The string to find + # Check if already in PATH + Push "$1;" # The string to search + Push "$0;" # The string to find Call StrStr - Pop $2 ; The result of the search - StrCmp $2 "" 0 done ; String not found, try again with ';' at the end - ; Otherwise, it's already in the path - Push "$1;" ; The string to search - Push "$0\;" ; The string to find + Pop $2 # The result of the search + StrCmp $2 "" 0 done # String not found, try again with ';' at the end + # Otherwise, it's already in the path + Push "$1;" # The string to search + Push "$0\;" # The string to find Call StrStr - Pop $2 ; The result - StrCmp $2 "" 0 done ; String not found, continue (add) - ; Otherwise, it's already in the path + Pop $2 # The result + StrCmp $2 "" 0 done # String not found, continue (add) + # Otherwise, it's already in the path - ; Prevent NSIS string overflow - StrLen $2 $0 ; Length of path to add ($2) - StrLen $3 $1 ; Length of current path ($3) - IntOp $2 $2 + $3 ; Length of current path + path to add ($2) - IntOp $2 $2 + 2 ; Account for the additional ';' - ; $2 = strlen(dir) + strlen(PATH) + sizeof(";") + # Prevent NSIS string overflow + StrLen $2 $0 # Length of path to add ($2) + StrLen $3 $1 # Length of current path ($3) + IntOp $2 $2 + $3 # Length of current path + path to add ($2) + IntOp $2 $2 + 2 # Account for the additional ';' + # $2 = strlen(dir) + strlen(PATH) + sizeof(";") - ; Make sure the new length isn't over the NSIS_MAX_STRLEN + # Make sure the new length isn't over the NSIS_MAX_STRLEN IntCmp $2 ${NSIS_MAX_STRLEN} +4 +4 0 DetailPrint "AddToPath: new length $2 > ${NSIS_MAX_STRLEN}" MessageBox MB_OK \ @@ -678,18 +700,18 @@ Function AddToPath /SD IDOK Goto done - ; Append dir to PATH + # Append dir to PATH DetailPrint "Add to PATH: $0" - StrCpy $2 $1 1 -1 ; Copy the last character of the existing path - StrCmp $2 ";" 0 +2 ; Check for trailing ';' - StrCpy $1 $1 -1 ; remove trailing ';' - StrCmp $1 "" +2 ; Make sure Path is not empty - StrCpy $0 "$1;$0" ; Append new path at the end ($0) + StrCpy $2 $1 1 -1 # Copy the last character of the existing path + StrCmp $2 ";" 0 +2 # Check for trailing ';' + StrCpy $1 $1 -1 # remove trailing ';' + StrCmp $1 "" +2 # Make sure Path is not empty + StrCpy $0 "$1;$0" # Append new path at the end ($0) - ; We can use the NSIS command here. Only 'ReadRegStr' is affected + # We can use the NSIS command here. Only 'ReadRegStr' is affected WriteRegExpandStr ${Environ} "PATH" $0 - ; Broadcast registry change to open programs + # Broadcast registry change to open programs SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 done: @@ -702,16 +724,16 @@ Function AddToPath FunctionEnd -;------------------------------------------------------------------------------ -; RemoveFromPath Function -; - Removes item from Path for All Users -; - Overcomes NSIS ReadRegStr limitation of 1024 characters by using Native -; Windows Commands -; -; Usage: -; Push "C:\path\to\add" -; Call un.RemoveFromPath -;------------------------------------------------------------------------------ +#------------------------------------------------------------------------------ +# RemoveFromPath Function +# - Removes item from Path for All Users +# - Overcomes NSIS ReadRegStr limitation of 1024 characters by using Native +# Windows Commands +# +# Usage: +# Push "C:\path\to\add" +# Call un.RemoveFromPath +#------------------------------------------------------------------------------ Function un.RemoveFromPath Exch $0 @@ -722,59 +744,59 @@ Function un.RemoveFromPath Push $5 Push $6 - ; Open a handle to the key in the registry, handle in $3, Error in $4 + # Open a handle to the key in the registry, handle in $3, Error in $4 System::Call "advapi32::RegOpenKey(i 0x80000002, t'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', *i.r3) i.r4" - ; Make sure registry handle opened successfully (returned 0) + # Make sure registry handle opened successfully (returned 0) IntCmp $4 0 0 done done - ; Load the contents of path into $1, Error Code into $4, Path length into $2 + # Load the contents of path into $1, Error Code into $4, Path length into $2 System::Call "advapi32::RegQueryValueEx(i $3, t'PATH', i 0, i 0, t.r1, *i ${NSIS_MAX_STRLEN} r2) i.r4" - ; Close the handle to the registry ($3) + # Close the handle to the registry ($3) System::Call "advapi32::RegCloseKey(i $3)" - ; Check for Error Code 234, Path too long for the variable - IntCmp $4 234 0 +4 +4 ; $4 == ERROR_MORE_DATA + # Check for Error Code 234, Path too long for the variable + IntCmp $4 234 0 +4 +4 # $4 == ERROR_MORE_DATA DetailPrint "AddToPath: original length $2 > ${NSIS_MAX_STRLEN}" Goto done - ; If no error, continue - IntCmp $4 0 +5 ; $4 != NO_ERROR - ; Error 2 means the Key was not found - IntCmp $4 2 +3 ; $4 != ERROR_FILE_NOT_FOUND + # If no error, continue + IntCmp $4 0 +5 # $4 != NO_ERROR + # Error 2 means the Key was not found + IntCmp $4 2 +3 # $4 != ERROR_FILE_NOT_FOUND DetailPrint "AddToPath: unexpected error code $4" Goto done StrCpy $1 "" - ; Ensure there's a trailing ';' - StrCpy $5 $1 1 -1 ; Copy the last character of the path - StrCmp $5 ";" +2 ; Check for trailing ';', if found continue - StrCpy $1 "$1;" ; ensure trailing ';' + # Ensure there's a trailing ';' + StrCpy $5 $1 1 -1 # Copy the last character of the path + StrCmp $5 ";" +2 # Check for trailing ';', if found continue + StrCpy $1 "$1;" # ensure trailing ';' - ; Check for our directory inside the path - Push $1 ; String to Search - Push "$0;" ; Dir to Find + # Check for our directory inside the path + Push $1 # String to Search + Push "$0;" # Dir to Find Call un.StrStr - Pop $2 ; The results of the search - StrCmp $2 "" done ; If results are empty, we're done, otherwise continue + Pop $2 # The results of the search + StrCmp $2 "" done # If results are empty, we're done, otherwise continue - ; Remove our Directory from the Path + # Remove our Directory from the Path DetailPrint "Remove from PATH: $0" - StrLen $3 "$0;" ; Get the length of our dir ($3) - StrLen $4 $2 ; Get the length of the return from StrStr ($4) - StrCpy $5 $1 -$4 ; $5 is now the part before the path to remove - StrCpy $6 $2 "" $3 ; $6 is now the part after the path to remove - StrCpy $3 "$5$6" ; Combine $5 and $6 + StrLen $3 "$0;" # Get the length of our dir ($3) + StrLen $4 $2 # Get the length of the return from StrStr ($4) + StrCpy $5 $1 -$4 # $5 is now the part before the path to remove + StrCpy $6 $2 "" $3 # $6 is now the part after the path to remove + StrCpy $3 "$5$6" # Combine $5 and $6 - ; Check for Trailing ';' - StrCpy $5 $3 1 -1 ; Load the last character of the string - StrCmp $5 ";" 0 +2 ; Check for ';' - StrCpy $3 $3 -1 ; remove trailing ';' + # Check for Trailing ';' + StrCpy $5 $3 1 -1 # Load the last character of the string + StrCmp $5 ";" 0 +2 # Check for ';' + StrCpy $3 $3 -1 # remove trailing ';' - ; Write the new path to the registry + # Write the new path to the registry WriteRegExpandStr ${Environ} "PATH" $3 - ; Broadcast the change to all open applications + # Broadcast the change to all open applications SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 done: @@ -808,6 +830,7 @@ Function getMinionConfig confFound: FileOpen $0 "$INSTDIR\conf\minion" r + ClearErrors confLoop: FileRead $0 $1 IfErrors EndOfFile @@ -838,68 +861,69 @@ FunctionEnd Function updateMinionConfig ClearErrors - FileOpen $0 "$INSTDIR\conf\minion" "r" ; open target file for reading - GetTempFileName $R0 ; get new temp file name - FileOpen $1 $R0 "w" ; open temp file for writing + FileOpen $0 "$INSTDIR\conf\minion" "r" # open target file for reading + GetTempFileName $R0 # get new temp file name + FileOpen $1 $R0 "w" # open temp file for writing - loop: ; loop through each line - FileRead $0 $2 ; read line from target file - IfErrors done ; end if errors are encountered (end of line) + loop: # loop through each line + FileRead $0 $2 # read line from target file + IfErrors done # end if errors are encountered (end of line) - ${If} $MasterHost_State != "" ; if master is empty - ${AndIf} $MasterHost_State != "salt" ; and if master is not 'salt' - ${StrLoc} $3 $2 "master:" ">" ; where is 'master:' in this line - ${If} $3 == 0 ; is it in the first... - ${OrIf} $3 == 1 ; or second position (account for comments) - StrCpy $2 "master: $MasterHost_State$\r$\n" ; write the master - ${EndIf} ; close if statement - ${EndIf} ; close if statement + ${If} $MasterHost_State != "" # if master is empty + ${AndIf} $MasterHost_State != "salt" # and if master is not 'salt' + ${StrLoc} $3 $2 "master:" ">" # where is 'master:' in this line + ${If} $3 == 0 # is it in the first... + ${OrIf} $3 == 1 # or second position (account for comments) + StrCpy $2 "master: $MasterHost_State$\r$\n" # write the master + ${EndIf} # close if statement + ${EndIf} # close if statement - ${If} $MinionName_State != "" ; if minion is empty - ${AndIf} $MinionName_State != "hostname" ; and if minion is not 'hostname' - ${StrLoc} $3 $2 "id:" ">" ; where is 'id:' in this line - ${If} $3 == 0 ; is it in the first... - ${OrIf} $3 == 1 ; or the second position (account for comments) - StrCpy $2 "id: $MinionName_State$\r$\n" ; change line - ${EndIf} ; close if statement - ${EndIf} ; close if statement + ${If} $MinionName_State != "" # if minion is empty + ${AndIf} $MinionName_State != "hostname" # and if minion is not 'hostname' + ${StrLoc} $3 $2 "id:" ">" # where is 'id:' in this line + ${If} $3 == 0 # is it in the first... + ${OrIf} $3 == 1 # or the second position (account for comments) + StrCpy $2 "id: $MinionName_State$\r$\n" # change line + ${EndIf} # close if statement + ${EndIf} # close if statement - FileWrite $1 $2 ; write changed or unchanged line to temp file + FileWrite $1 $2 # write changed or unchanged line to temp file Goto loop done: - FileClose $0 ; close target file - FileClose $1 ; close temp file - Delete "$INSTDIR\conf\minion" ; delete target file - CopyFiles /SILENT $R0 "$INSTDIR\conf\minion" ; copy temp file to target file - Delete $R0 ; delete temp file + FileClose $0 # close target file + FileClose $1 # close temp file + Delete "$INSTDIR\conf\minion" # delete target file + CopyFiles /SILENT $R0 "$INSTDIR\conf\minion" # copy temp file to target file + Delete $R0 # delete temp file FunctionEnd Function parseCommandLineSwitches - ; Load the parameters + # Load the parameters ${GetParameters} $R0 - ; Check for start-minion switches - ; /start-service is to be deprecated, so we must check for both + # Check for start-minion switches + # /start-service is to be deprecated, so we must check for both ${GetOptions} $R0 "/start-service=" $R1 ${GetOptions} $R0 "/start-minion=" $R2 # Service: Start Salt Minion ${IfNot} $R2 == "" - ; If start-minion was passed something, then set it + # If start-minion was passed something, then set it StrCpy $StartMinion $R2 ${ElseIfNot} $R1 == "" - ; If start-service was passed something, then set StartMinion to that + # If start-service was passed something, then set StartMinion to that StrCpy $StartMinion $R1 ${Else} - ; Otherwise default to 1 + # Otherwise default to 1 StrCpy $StartMinion 1 ${EndIf} # Service: Minion Startup Type Delayed + ClearErrors ${GetOptions} $R0 "/start-minion-delayed" $R1 IfErrors start_minion_delayed_not_found StrCpy $StartMinionDelayed 1 diff --git a/pkg/windows/modules/get-settings.psm1 b/pkg/windows/modules/get-settings.psm1 index 292732cb83..5c57738fd3 100644 --- a/pkg/windows/modules/get-settings.psm1 +++ b/pkg/windows/modules/get-settings.psm1 @@ -19,9 +19,9 @@ Function Get-Settings { "Python2Dir" = "C:\Python27" "Scripts2Dir" = "C:\Python27\Scripts" "SitePkgs2Dir" = "C:\Python27\Lib\site-packages" - "Python3Dir" = "C:\Program Files\Python35" - "Scripts3Dir" = "C:\Program Files\Python35\Scripts" - "SitePkgs3Dir" = "C:\Program Files\Python35\Lib\site-packages" + "Python3Dir" = "C:\Python35" + "Scripts3Dir" = "C:\Python35\Scripts" + "SitePkgs3Dir" = "C:\Python35\Lib\site-packages" "DownloadDir" = "$env:Temp\DevSalt" } # The script deletes the DownLoadDir (above) for each install. diff --git a/salt/auth/__init__.py b/salt/auth/__init__.py index 96a8786daa..38e3c3362e 100644 --- a/salt/auth/__init__.py +++ b/salt/auth/__init__.py @@ -29,6 +29,7 @@ import salt.config import salt.loader import salt.transport.client import salt.utils +import salt.utils.args import salt.utils.files import salt.utils.minions import salt.utils.versions @@ -69,7 +70,7 @@ class LoadAuth(object): if fstr not in self.auth: return '' try: - pname_arg = salt.utils.arg_lookup(self.auth[fstr])['args'][0] + pname_arg = salt.utils.args.arg_lookup(self.auth[fstr])['args'][0] return load[pname_arg] except IndexError: return '' @@ -216,8 +217,9 @@ class LoadAuth(object): acl_ret = self.__get_acl(load) tdata['auth_list'] = acl_ret - if 'groups' in load: - tdata['groups'] = load['groups'] + groups = self.get_groups(load) + if groups: + tdata['groups'] = groups return self.tokens["{0}.mk_token".format(self.opts['eauth_tokens'])](self.opts, tdata) @@ -292,29 +294,31 @@ class LoadAuth(object): def authenticate_key(self, load, key): ''' Authenticate a user by the key passed in load. - Return the effective user id (name) if it's differ from the specified one (for sudo). - If the effective user id is the same as passed one return True on success or False on + Return the effective user id (name) if it's different from the specified one (for sudo). + If the effective user id is the same as the passed one, return True on success or False on failure. ''' - auth_key = load.pop('key') - if not auth_key: - log.warning('Authentication failure of type "user" occurred.') + error_msg = 'Authentication failure of type "user" occurred.' + auth_key = load.pop('key', None) + if auth_key is None: + log.warning(error_msg) return False + if 'user' in load: auth_user = AuthUser(load['user']) if auth_user.is_sudo(): # If someone sudos check to make sure there is no ACL's around their username if auth_key != key[self.opts.get('user', 'root')]: - log.warning('Authentication failure of type "user" occurred.') + log.warning(error_msg) return False return auth_user.sudo_name() elif load['user'] == self.opts.get('user', 'root') or load['user'] == 'root': if auth_key != key[self.opts.get('user', 'root')]: - log.warning('Authentication failure of type "user" occurred.') + log.warning(error_msg) return False elif auth_user.is_running_user(): if auth_key != key.get(load['user']): - log.warning('Authentication failure of type "user" occurred.') + log.warning(error_msg) return False elif auth_key == key.get('root'): pass @@ -322,19 +326,19 @@ class LoadAuth(object): if load['user'] in key: # User is authorised, check key and check perms if auth_key != key[load['user']]: - log.warning('Authentication failure of type "user" occurred.') + log.warning(error_msg) return False return load['user'] else: - log.warning('Authentication failure of type "user" occurred.') + log.warning(error_msg) return False else: if auth_key != key[salt.utils.get_user()]: - log.warning('Authentication failure of type "other" occurred.') + log.warning(error_msg) return False return True - def get_auth_list(self, load): + def get_auth_list(self, load, token=None): ''' Retrieve access list for the user specified in load. The list is built by eauth module or from master eauth configuration. @@ -342,30 +346,37 @@ class LoadAuth(object): list if the user has no rights to execute anything on this master and returns non-empty list if user is allowed to execute particular functions. ''' + # Get auth list from token + if token and self.opts['keep_acl_in_token'] and 'auth_list' in token: + return token['auth_list'] # Get acl from eauth module. auth_list = self.__get_acl(load) if auth_list is not None: return auth_list - if load['eauth'] not in self.opts['external_auth']: + eauth = token['eauth'] if token else load['eauth'] + if eauth not in self.opts['external_auth']: # No matching module is allowed in config log.warning('Authorization failure occurred.') return None - name = self.load_name(load) # The username we are attempting to auth with - groups = self.get_groups(load) # The groups this user belongs to - eauth_config = self.opts['external_auth'][load['eauth']] - if groups is None or groups is False: + if token: + name = token['name'] + groups = token.get('groups') + else: + name = self.load_name(load) # The username we are attempting to auth with + groups = self.get_groups(load) # The groups this user belongs to + eauth_config = self.opts['external_auth'][eauth] + if not groups: groups = [] group_perm_keys = [item for item in eauth_config if item.endswith('%')] # The configured auth groups # First we need to know if the user is allowed to proceed via any of their group memberships. group_auth_match = False for group_config in group_perm_keys: - group_config = group_config.rstrip('%') - for group in groups: - if group == group_config: - group_auth_match = True + if group_config.rstrip('%') in groups: + group_auth_match = True + break # If a group_auth_match is set it means only that we have a # user which matches at least one or more of the groups defined # in the configuration file. @@ -405,6 +416,64 @@ class LoadAuth(object): return auth_list + def check_authentication(self, load, auth_type, key=None, show_username=False): + ''' + .. versionadded:: Oxygen + + Go through various checks to see if the token/eauth/user can be authenticated. + + Returns a dictionary containing the following keys: + + - auth_list + - username + - error + + If an error is encountered, return immediately with the relevant error dictionary + as authentication has failed. Otherwise, return the username and valid auth_list. + ''' + auth_list = [] + username = load.get('username', 'UNKNOWN') + ret = {'auth_list': auth_list, + 'username': username, + 'error': {}} + + # Authenticate + if auth_type == 'token': + token = self.authenticate_token(load) + if not token: + ret['error'] = {'name': 'TokenAuthenticationError', + 'message': 'Authentication failure of type "token" occurred.'} + return ret + + # Update username for token + username = token['name'] + ret['username'] = username + auth_list = self.get_auth_list(load, token=token) + elif auth_type == 'eauth': + if not self.authenticate_eauth(load): + ret['error'] = {'name': 'EauthAuthenticationError', + 'message': 'Authentication failure of type "eauth" occurred for ' + 'user {0}.'.format(username)} + return ret + + auth_list = self.get_auth_list(load) + elif auth_type == 'user': + if not self.authenticate_key(load, key): + if show_username: + msg = 'Authentication failure of type "user" occurred for user {0}.'.format(username) + else: + msg = 'Authentication failure of type "user" occurred' + ret['error'] = {'name': 'UserAuthenticationError', 'message': msg} + return ret + else: + ret['error'] = {'name': 'SaltInvocationError', + 'message': 'Authentication type not supported.'} + return ret + + # Authentication checks passed + ret['auth_list'] = auth_list + return ret + class Authorize(object): ''' @@ -550,6 +619,15 @@ class Authorize(object): load.get('arg', None), load.get('tgt', None), load.get('tgt_type', 'glob')) + + # Handle possible return of dict data structure from any_auth call to + # avoid a stacktrace. As mentioned in PR #43181, this entire class is + # dead code and is marked for removal in Salt Neon. But until then, we + # should handle the dict return, which is an error and should return + # False until this class is removed. + if isinstance(good, dict): + return False + if not good: # Accept find_job so the CLI will function cleanly if load.get('fun', '') != 'saltutil.find_job': @@ -562,7 +640,7 @@ class Authorize(object): authorization Note: this will check that the user has at least one right that will let - him execute "load", this does not deal with conflicting rules + the user execute "load", this does not deal with conflicting rules ''' adata = self.auth_data @@ -634,7 +712,7 @@ class Resolver(object): 'not available').format(eauth)) return ret - args = salt.utils.arg_lookup(self.auth[fstr]) + args = salt.utils.args.arg_lookup(self.auth[fstr]) for arg in args['args']: if arg in self.opts: ret[arg] = self.opts[arg] diff --git a/salt/auth/ldap.py b/salt/auth/ldap.py index f5a44cba04..6d82a74013 100644 --- a/salt/auth/ldap.py +++ b/salt/auth/ldap.py @@ -378,7 +378,7 @@ def groups(username, **kwargs): search_results = bind.search_s(search_base, ldap.SCOPE_SUBTREE, search_string, - [_config('accountattributename'), 'cn']) + [_config('accountattributename'), 'cn', _config('groupattribute')]) for _, entry in search_results: if username in entry[_config('accountattributename')]: group_list.append(entry['cn'][0]) @@ -390,7 +390,7 @@ def groups(username, **kwargs): # Only test user auth on first call for job. # 'show_jid' only exists on first payload so we can use that for the conditional. - if 'show_jid' in kwargs and not _bind(username, kwargs['password'], + if 'show_jid' in kwargs and not _bind(username, kwargs.get('password'), anonymous=_config('auth_by_group_membership_only', mandatory=False) and _config('anonymous', mandatory=False)): log.error('LDAP username and password do not match') diff --git a/salt/beacons/__init__.py b/salt/beacons/__init__.py index 0b27bfbe67..54bea7aa96 100644 --- a/salt/beacons/__init__.py +++ b/salt/beacons/__init__.py @@ -59,7 +59,7 @@ class Beacon(object): if 'enabled' in current_beacon_config: if not current_beacon_config['enabled']: - log.trace('Beacon {0} disabled'.format(mod)) + log.trace('Beacon %s disabled', mod) continue else: # remove 'enabled' item before processing the beacon @@ -68,7 +68,7 @@ class Beacon(object): else: self._remove_list_item(config[mod], 'enabled') - log.trace('Beacon processing: {0}'.format(mod)) + log.trace('Beacon processing: %s', mod) fun_str = '{0}.beacon'.format(mod) validate_str = '{0}.validate'.format(mod) if fun_str in self.beacons: @@ -77,10 +77,10 @@ class Beacon(object): if interval: b_config = self._trim_config(b_config, mod, 'interval') if not self._process_interval(mod, interval): - log.trace('Skipping beacon {0}. Interval not reached.'.format(mod)) + log.trace('Skipping beacon %s. Interval not reached.', mod) continue if self._determine_beacon_config(current_beacon_config, 'disable_during_state_run'): - log.trace('Evaluting if beacon {0} should be skipped due to a state run.'.format(mod)) + log.trace('Evaluting if beacon %s should be skipped due to a state run.', mod) b_config = self._trim_config(b_config, mod, 'disable_during_state_run') is_running = False running_jobs = salt.utils.minion.running(self.opts) @@ -90,10 +90,10 @@ class Beacon(object): if is_running: close_str = '{0}.close'.format(mod) if close_str in self.beacons: - log.info('Closing beacon {0}. State run in progress.'.format(mod)) + log.info('Closing beacon %s. State run in progress.', mod) self.beacons[close_str](b_config[mod]) else: - log.info('Skipping beacon {0}. State run in progress.'.format(mod)) + log.info('Skipping beacon %s. State run in progress.', mod) continue # Update __grains__ on the beacon self.beacons[fun_str].__globals__['__grains__'] = grains @@ -120,7 +120,7 @@ class Beacon(object): if runonce: self.disable_beacon(mod) else: - log.warning('Unable to process beacon {0}'.format(mod)) + log.warning('Unable to process beacon %s', mod) return ret def _trim_config(self, b_config, mod, key): @@ -149,19 +149,19 @@ class Beacon(object): Process beacons with intervals Return True if a beacon should be run on this loop ''' - log.trace('Processing interval {0} for beacon mod {1}'.format(interval, mod)) + log.trace('Processing interval %s for beacon mod %s', interval, mod) loop_interval = self.opts['loop_interval'] if mod in self.interval_map: log.trace('Processing interval in map') counter = self.interval_map[mod] - log.trace('Interval counter: {0}'.format(counter)) + log.trace('Interval counter: %s', counter) if counter * loop_interval >= interval: self.interval_map[mod] = 1 return True else: self.interval_map[mod] += 1 else: - log.trace('Interval process inserting mod: {0}'.format(mod)) + log.trace('Interval process inserting mod: %s', mod) self.interval_map[mod] = 1 return False @@ -205,15 +205,50 @@ class Beacon(object): ''' # Fire the complete event back along with the list of beacons evt = salt.utils.event.get_event('minion', opts=self.opts) - b_conf = self.functions['config.merge']('beacons') - if not isinstance(self.opts['beacons'], dict): - self.opts['beacons'] = {} - self.opts['beacons'].update(b_conf) evt.fire_event({'complete': True, 'beacons': self.opts['beacons']}, tag='/salt/minion/minion_beacons_list_complete') return True + def list_available_beacons(self): + ''' + List the available beacons + ''' + _beacons = ['{0}'.format(_beacon.replace('.beacon', '')) + for _beacon in self.beacons if '.beacon' in _beacon] + + # Fire the complete event back along with the list of beacons + evt = salt.utils.event.get_event('minion', opts=self.opts) + evt.fire_event({'complete': True, 'beacons': _beacons}, + tag='/salt/minion/minion_beacons_list_available_complete') + + return True + + def validate_beacon(self, name, beacon_data): + ''' + Return available beacon functions + ''' + validate_str = '{}.validate'.format(name) + # Run the validate function if it's available, + # otherwise there is a warning about it being missing + if validate_str in self.beacons: + if 'enabled' in beacon_data: + del beacon_data['enabled'] + valid, vcomment = self.beacons[validate_str](beacon_data) + else: + log.info('Beacon %s does not have a validate' + ' function, skipping validation.', name) + valid = True + + # Fire the complete event back along with the list of beacons + evt = salt.utils.event.get_event('minion', opts=self.opts) + evt.fire_event({'complete': True, + 'vcomment': vcomment, + 'valid': valid}, + tag='/salt/minion/minion_beacon_validation_complete') + + return True + def add_beacon(self, name, beacon_data): ''' Add a beacon item @@ -224,9 +259,9 @@ class Beacon(object): if name in self.opts['beacons']: log.info('Updating settings for beacon ' - 'item: {0}'.format(name)) + 'item: %s', name) else: - log.info('Added new beacon item {0}'.format(name)) + log.info('Added new beacon item %s', name) self.opts['beacons'].update(data) # Fire the complete event back along with updated list of beacons @@ -245,7 +280,7 @@ class Beacon(object): data[name] = beacon_data log.info('Updating settings for beacon ' - 'item: {0}'.format(name)) + 'item: %s', name) self.opts['beacons'].update(data) # Fire the complete event back along with updated list of beacons @@ -261,7 +296,7 @@ class Beacon(object): ''' if name in self.opts['beacons']: - log.info('Deleting beacon item {0}'.format(name)) + log.info('Deleting beacon item %s', name) del self.opts['beacons'][name] # Fire the complete event back along with updated list of beacons diff --git a/salt/beacons/btmp.py b/salt/beacons/btmp.py index 40b50470d8..fe7f329025 100644 --- a/salt/beacons/btmp.py +++ b/salt/beacons/btmp.py @@ -10,14 +10,19 @@ Beacon to fire events at failed login of users # Import python libs from __future__ import absolute_import +import logging import os import struct +import time # Import Salt Libs import salt.utils.files # Import 3rd-party libs -from salt.ext import six +import salt.ext.six +# pylint: disable=import-error +from salt.ext.six.moves import map +# pylint: enable=import-error __virtualname__ = 'btmp' BTMP = '/var/log/btmp' @@ -37,6 +42,15 @@ FIELDS = [ SIZE = struct.calcsize(FMT) LOC_KEY = 'btmp.loc' +log = logging.getLogger(__name__) + +# pylint: disable=import-error +try: + import dateutil.parser as dateutil_parser + _TIME_SUPPORTED = True +except ImportError: + _TIME_SUPPORTED = False + def __virtual__(): if os.path.isfile(BTMP): @@ -44,6 +58,20 @@ def __virtual__(): return False +def _check_time_range(time_range, now): + ''' + Check time range + ''' + if _TIME_SUPPORTED: + _start = int(time.mktime(dateutil_parser.parse(time_range['start']).timetuple())) + _end = int(time.mktime(dateutil_parser.parse(time_range['end']).timetuple())) + + return bool(_start <= now <= _end) + else: + log.error('Dateutil is required.') + return False + + def _get_loc(): ''' return the active file location @@ -60,6 +88,45 @@ def validate(config): if not isinstance(config, list): return False, ('Configuration for btmp beacon must ' 'be a list.') + else: + _config = {} + list(map(_config.update, config)) + + if 'users' in _config: + if not isinstance(_config['users'], dict): + return False, ('User configuration for btmp beacon must ' + 'be a dictionary.') + else: + for user in _config['users']: + if _config['users'][user] and \ + 'time_range' in _config['users'][user]: + _time_range = _config['users'][user]['time_range'] + if not isinstance(_time_range, dict): + return False, ('The time_range parameter for ' + 'btmp beacon must ' + 'be a dictionary.') + else: + if not all(k in _time_range for k in ('start', 'end')): + return False, ('The time_range parameter for ' + 'btmp beacon must contain ' + 'start & end options.') + if 'defaults' in _config: + if not isinstance(_config['defaults'], dict): + return False, ('Defaults configuration for btmp beacon must ' + 'be a dictionary.') + else: + if 'time_range' in _config['defaults']: + _time_range = _config['defaults']['time_range'] + if not isinstance(_time_range, dict): + return False, ('The time_range parameter for ' + 'btmp beacon must ' + 'be a dictionary.') + else: + if not all(k in _time_range for k in ('start', 'end')): + return False, ('The time_range parameter for ' + 'btmp beacon must contain ' + 'start & end options.') + return True, 'Valid beacon configuration' @@ -72,8 +139,40 @@ def beacon(config): beacons: btmp: [] + + beacons: + btmp: + - users: + gareth: + - defaults: + time_range: + start: '8am' + end: '4pm' + + beacons: + btmp: + - users: + gareth: + time_range: + start: '8am' + end: '4pm' + - defaults: + time_range: + start: '8am' + end: '4pm' ''' ret = [] + + users = None + defaults = None + + for config_item in config: + if 'users' in config_item: + users = config_item['users'] + + if 'defaults' in config_item: + defaults = config_item['defaults'] + with salt.utils.files.fopen(BTMP, 'rb') as fp_: loc = __context__.get(LOC_KEY, 0) if loc == 0: @@ -83,6 +182,7 @@ def beacon(config): else: fp_.seek(loc) while True: + now = int(time.time()) raw = fp_.read(SIZE) if len(raw) != SIZE: return ret @@ -91,7 +191,30 @@ def beacon(config): event = {} for ind, field in enumerate(FIELDS): event[field] = pack[ind] - if isinstance(event[field], six.string_types): - event[field] = event[field].strip('\x00') - ret.append(event) + if isinstance(event[field], salt.ext.six.string_types): + if isinstance(event[field], bytes): + event[field] = event[field].decode() + event[field] = event[field].strip('b\x00') + else: + event[field] = event[field].strip('\x00') + + if users: + if event['user'] in users: + _user = users[event['user']] + if isinstance(_user, dict) and 'time_range' in _user: + if _check_time_range(_user['time_range'], now): + ret.append(event) + else: + if defaults and 'time_range' in defaults: + if _check_time_range(defaults['time_range'], + now): + ret.append(event) + else: + ret.append(event) + else: + if defaults and 'time_range' in defaults: + if _check_time_range(defaults['time_range'], now): + ret.append(event) + else: + ret.append(event) return ret diff --git a/salt/beacons/inotify.py b/salt/beacons/inotify.py index ee1dfa4f78..7e5f4df863 100644 --- a/salt/beacons/inotify.py +++ b/salt/beacons/inotify.py @@ -23,7 +23,9 @@ import re # Import salt libs import salt.ext.six +# pylint: disable=import-error from salt.ext.six.moves import map +# pylint: enable=import-error # Import third party libs try: diff --git a/salt/beacons/napalm_beacon.py b/salt/beacons/napalm_beacon.py new file mode 100644 index 0000000000..6650215471 --- /dev/null +++ b/salt/beacons/napalm_beacon.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +''' +NAPALM functions +================ + +.. versionadded:: Oxygen + +Watch NAPALM functions and fire events on specific triggers. + +.. note:: + + The ``NAPALM`` beacon only works only when running under + a regular Minion or a Proxy Minion, managed via NAPALM_. + Check the documentation for the + :mod:`NAPALM proxy module `. + + _NAPALM: http://napalm.readthedocs.io/en/latest/index.html + +The configuration accepts a list of Salt functions to be +invoked, and the corresponding output hierarchy that should +be matched against. To invoke a function with certain +arguments, they can be specified using the ``_args`` key, or +``_kwargs`` for more specific key-value arguments. + +The match structure follows the output hierarchy of the NAPALM +functions, under the ``out`` key. + +For example, the following is normal structure returned by the +:mod:`ntp.stats ` execution function: + +.. code-block:: json + + { + "comment": "", + "result": true, + "out": [ + { + "referenceid": ".GPSs.", + "remote": "172.17.17.1", + "synchronized": true, + "reachability": 377, + "offset": 0.461, + "when": "860", + "delay": 143.606, + "hostpoll": 1024, + "stratum": 1, + "jitter": 0.027, + "type": "-" + }, + { + "referenceid": ".INIT.", + "remote": "172.17.17.2", + "synchronized": false, + "reachability": 0, + "offset": 0.0, + "when": "-", + "delay": 0.0, + "hostpoll": 1024, + "stratum": 16, + "jitter": 4000.0, + "type": "-" + } + ] + } + +In order to fire events when the synchronization is lost with +one of the NTP peers, e.g., ``172.17.17.2``, we can match it explicitly as: + +.. code-block:: yaml + + ntp.stats: + remote: 172.17.17.2 + synchronized: false + +There is one single nesting level, as the output of ``ntp.stats`` is +just a list of dictionaries, and this beacon will compare each dictionary +from the list with the structure examplified above. + +.. note:: + + When we want to match on any element at a certain level, we can + configure ``*`` to match anything. + +Considering a more complex structure consisting on multiple nested levels, +e.g., the output of the :mod:`bgp.neighbors ` +execution function, to check when any neighbor from the ``global`` +routing table is down, the match structure would have the format: + +.. code-block:: yaml + + bgp.neighbors: + global: + '*': + up: false + +The match structure above will match any BGP neighbor, with +any network (``*`` matches any AS number), under the ``global`` VRF. +In other words, this beacon will push an event on the Salt bus +when there's a BGP neighbor down. + +The right operand can also accept mathematical operations +(i.e., ``<``, ``<=``, ``!=``, ``>``, ``>=`` etc.) when comparing +numerical values. + +Configuration Example: + +.. code-block:: yaml + + beacons: + napalm: + - net.interfaces: + # fire events when any interfaces is down + '*': + is_up: false + - net.interfaces: + # fire events only when the xe-0/0/0 interface is down + 'xe-0/0/0': + is_up: false + - ntp.stats: + # fire when there's any NTP peer unsynchornized + synchronized: false + - ntp.stats: + # fire only when the synchronization + # with with the 172.17.17.2 NTP server is lost + _args: + - 172.17.17.2 + synchronized: false + - ntp.stats: + # fire only when there's a NTP peer with + # synchronization stratum > 5 + stratum: '> 5' + +Event structure example: + +.. code-block:: json + + salt/beacon/edge01.bjm01/napalm/junos/ntp.stats { + "_stamp": "2017-09-05T09:51:09.377202", + "args": [], + "data": { + "comment": "", + "out": [ + { + "delay": 0.0, + "hostpoll": 1024, + "jitter": 4000.0, + "offset": 0.0, + "reachability": 0, + "referenceid": ".INIT.", + "remote": "172.17.17.1", + "stratum": 16, + "synchronized": false, + "type": "-", + "when": "-" + } + ], + "result": true + }, + "fun": "ntp.stats", + "id": "edge01.bjm01", + "kwargs": {}, + "match": { + "stratum": "> 5" + } + } + +The event examplified above has been fired when the device +identified by the Minion id ``edge01.bjm01`` has been synchronized +with a NTP server at a stratum level greater than 5. +''' +from __future__ import absolute_import + +# Import Python std lib +import re +import logging + +# Import Salt modules +from salt.ext import six +import salt.utils.napalm + +log = logging.getLogger(__name__) +_numeric_regex = re.compile(r'^(<|>|<=|>=|==|!=)\s*(\d+(\.\d+){0,1})$') +# the numeric regex will match the right operand, e.g '>= 20', '< 100', '!= 20', '< 1000.12' etc. +_numeric_operand = { + '<': '__lt__', + '>': '__gt__', + '>=': '__ge__', + '<=': '__le__', + '==': '__eq__', + '!=': '__ne__', +} # mathematical operand - private method map + + +__virtualname__ = 'napalm' + + +def __virtual__(): + ''' + This beacon can only work when running under a regular or a proxy minion, managed through napalm. + ''' + return salt.utils.napalm.virtual(__opts__, __virtualname__, __file__) + + +def _compare(cur_cmp, cur_struct): + ''' + Compares two objects and return a boolean value + when there's a match. + ''' + if isinstance(cur_cmp, dict) and isinstance(cur_struct, dict): + log.debug('Comparing dict to dict') + for cmp_key, cmp_value in six.iteritems(cur_cmp): + if cmp_key == '*': + # matches any key from the source dictionary + if isinstance(cmp_value, dict): + found = False + for _, cur_struct_val in six.iteritems(cur_struct): + found |= _compare(cmp_value, cur_struct_val) + return found + else: + found = False + if isinstance(cur_struct, (list, tuple)): + for cur_ele in cur_struct: + found |= _compare(cmp_value, cur_ele) + elif isinstance(cur_struct, dict): + for _, cur_ele in six.iteritems(cur_struct): + found |= _compare(cmp_value, cur_ele) + return found + else: + if isinstance(cmp_value, dict): + if cmp_key not in cur_struct: + return False + return _compare(cmp_value, cur_struct[cmp_key]) + if isinstance(cmp_value, list): + found = False + for _, cur_struct_val in six.iteritems(cur_struct): + found |= _compare(cmp_value, cur_struct_val) + return found + else: + return _compare(cmp_value, cur_struct[cmp_key]) + elif isinstance(cur_cmp, (list, tuple)) and isinstance(cur_struct, (list, tuple)): + log.debug('Comparing list to list') + found = False + for cur_cmp_ele in cur_cmp: + for cur_struct_ele in cur_struct: + found |= _compare(cur_cmp_ele, cur_struct_ele) + return found + elif isinstance(cur_cmp, dict) and isinstance(cur_struct, (list, tuple)): + log.debug('Comparing dict to list (of dicts?)') + found = False + for cur_struct_ele in cur_struct: + found |= _compare(cur_cmp, cur_struct_ele) + return found + elif isinstance(cur_cmp, bool) and isinstance(cur_struct, bool): + log.debug('Comparing booleans: %s ? %s', cur_cmp, cur_struct) + return cur_cmp == cur_struct + elif isinstance(cur_cmp, (six.string_types, six.text_type)) and \ + isinstance(cur_struct, (six.string_types, six.text_type)): + log.debug('Comparing strings (and regex?): %s ? %s', cur_cmp, cur_struct) + # Trying literal match + matched = re.match(cur_cmp, cur_struct, re.I) + if matched: + return True + return False + elif isinstance(cur_cmp, (six.integer_types, float)) and \ + isinstance(cur_struct, (six.integer_types, float)): + log.debug('Comparing numeric values: %d ? %d', cur_cmp, cur_struct) + # numeric compare + return cur_cmp == cur_struct + elif isinstance(cur_struct, (six.integer_types, float)) and \ + isinstance(cur_cmp, (six.string_types, six.text_type)): + # Comapring the numerical value agains a presumably mathematical value + log.debug('Comparing a numeric value (%d) with a string (%s)', cur_struct, cur_cmp) + numeric_compare = _numeric_regex.match(cur_cmp) + # determine if the value to compare agains is a mathematical operand + if numeric_compare: + compare_value = numeric_compare.group(2) + return getattr(float(cur_struct), _numeric_operand[numeric_compare.group(1)])(float(compare_value)) + return False + return False + + +def validate(config): + ''' + Validate the beacon configuration. + ''' + # Must be a list of dicts. + if not isinstance(config, list): + return False, 'Configuration for napalm beacon must be a list.' + for mod in config: + fun = mod.keys()[0] + fun_cfg = mod.values()[0] + if not isinstance(fun_cfg, dict): + return False, 'The match structure for the {} execution function output must be a dictionary'.format(fun) + if fun not in __salt__: + return False, 'Execution function {} is not availabe!'.format(fun) + return True, 'Valid configuration for the napal beacon!' + + +def beacon(config): + ''' + Watch napalm function and fire events. + ''' + log.debug('Executing napalm beacon with config:') + log.debug(config) + ret = [] + for mod in config: + if not mod: + continue + event = {} + fun = mod.keys()[0] + fun_cfg = mod.values()[0] + args = fun_cfg.pop('_args', []) + kwargs = fun_cfg.pop('_kwargs', {}) + log.debug('Executing {fun} with {args} and {kwargs}'.format( + fun=fun, + args=args, + kwargs=kwargs + )) + fun_ret = __salt__[fun](*args, **kwargs) + log.debug('Got the reply from the minion:') + log.debug(fun_ret) + if not fun_ret.get('result', False): + log.error('Error whilst executing {}'.format(fun)) + log.error(fun_ret) + continue + fun_ret_out = fun_ret['out'] + log.debug('Comparing to:') + log.debug(fun_cfg) + try: + fun_cmp_result = _compare(fun_cfg, fun_ret_out) + except Exception as err: + log.error(err, exc_info=True) + # catch any exception and continue + # to not jeopardise the execution of the next function in the list + continue + log.debug('Result of comparison: {res}'.format(res=fun_cmp_result)) + if fun_cmp_result: + log.info('Matched {fun} with {cfg}'.format( + fun=fun, + cfg=fun_cfg + )) + event['tag'] = '{os}/{fun}'.format(os=__grains__['os'], fun=fun) + event['fun'] = fun + event['args'] = args + event['kwargs'] = kwargs + event['data'] = fun_ret + event['match'] = fun_cfg + log.debug('Queueing event:') + log.debug(event) + ret.append(event) + log.debug('NAPALM beacon generated the events:') + log.debug(ret) + return ret diff --git a/salt/beacons/wtmp.py b/salt/beacons/wtmp.py index c10a335e0c..4cb3a0f4fc 100644 --- a/salt/beacons/wtmp.py +++ b/salt/beacons/wtmp.py @@ -10,14 +10,19 @@ Beacon to fire events at login of users as registered in the wtmp file # Import Python libs from __future__ import absolute_import +import logging import os import struct +import time # Import salt libs import salt.utils.files # Import 3rd-party libs -from salt.ext import six +import salt.ext.six +# pylint: disable=import-error +from salt.ext.six.moves import map +# pylint: enable=import-error __virtualname__ = 'wtmp' WTMP = '/var/log/wtmp' @@ -37,9 +42,15 @@ FIELDS = [ SIZE = struct.calcsize(FMT) LOC_KEY = 'wtmp.loc' -import logging log = logging.getLogger(__name__) +# pylint: disable=import-error +try: + import dateutil.parser as dateutil_parser + _TIME_SUPPORTED = True +except ImportError: + _TIME_SUPPORTED = False + def __virtual__(): if os.path.isfile(WTMP): @@ -47,6 +58,20 @@ def __virtual__(): return False +def _check_time_range(time_range, now): + ''' + Check time range + ''' + if _TIME_SUPPORTED: + _start = int(time.mktime(dateutil_parser.parse(time_range['start']).timetuple())) + _end = int(time.mktime(dateutil_parser.parse(time_range['end']).timetuple())) + + return bool(_start <= now <= _end) + else: + log.error('Dateutil is required.') + return False + + def _get_loc(): ''' return the active file location @@ -62,6 +87,44 @@ def validate(config): # Configuration for wtmp beacon should be a list of dicts if not isinstance(config, list): return False, ('Configuration for wtmp beacon must be a list.') + else: + _config = {} + list(map(_config.update, config)) + + if 'users' in _config: + if not isinstance(_config['users'], dict): + return False, ('User configuration for btmp beacon must ' + 'be a dictionary.') + else: + for user in _config['users']: + if _config['users'][user] and \ + 'time_range' in _config['users'][user]: + _time_range = _config['users'][user]['time_range'] + if not isinstance(_time_range, dict): + return False, ('The time_range parameter for ' + 'btmp beacon must ' + 'be a dictionary.') + else: + if not all(k in _time_range for k in ('start', 'end')): + return False, ('The time_range parameter for ' + 'btmp beacon must contain ' + 'start & end options.') + if 'defaults' in _config: + if not isinstance(_config['defaults'], dict): + return False, ('Defaults configuration for btmp beacon must ' + 'be a dictionary.') + else: + if 'time_range' in _config['defaults']: + _time_range = _config['defaults']['time_range'] + if not isinstance(_time_range, dict): + return False, ('The time_range parameter for ' + 'btmp beacon must ' + 'be a dictionary.') + else: + if not all(k in _time_range for k in ('start', 'end')): + return False, ('The time_range parameter for ' + 'btmp beacon must contain ' + 'start & end options.') return True, 'Valid beacon configuration' @@ -74,8 +137,40 @@ def beacon(config): beacons: wtmp: [] - ''' + + beacons: + wtmp: + - users: + gareth: + - defaults: + time_range: + start: '8am' + end: '4pm' + + beacons: + wtmp: + - users: + gareth: + time_range: + start: '8am' + end: '4pm' + - defaults: + time_range: + start: '8am' + end: '4pm' +''' ret = [] + + users = None + defaults = None + + for config_item in config: + if 'users' in config_item: + users = config_item['users'] + + if 'defaults' in config_item: + defaults = config_item['defaults'] + with salt.utils.files.fopen(WTMP, 'rb') as fp_: loc = __context__.get(LOC_KEY, 0) if loc == 0: @@ -85,6 +180,7 @@ def beacon(config): else: fp_.seek(loc) while True: + now = int(time.time()) raw = fp_.read(SIZE) if len(raw) != SIZE: return ret @@ -93,7 +189,30 @@ def beacon(config): event = {} for ind, field in enumerate(FIELDS): event[field] = pack[ind] - if isinstance(event[field], six.string_types): - event[field] = event[field].strip('\x00') - ret.append(event) + if isinstance(event[field], salt.ext.six.string_types): + if isinstance(event[field], bytes): + event[field] = event[field].decode() + event[field] = event[field].strip('b\x00') + else: + event[field] = event[field].strip('\x00') + + if users: + if event['user'] in users: + _user = users[event['user']] + if isinstance(_user, dict) and 'time_range' in _user: + if _check_time_range(_user['time_range'], now): + ret.append(event) + else: + if defaults and 'time_range' in defaults: + if _check_time_range(defaults['time_range'], + now): + ret.append(event) + else: + ret.append(event) + else: + if defaults and 'time_range' in defaults: + if _check_time_range(defaults['time_range'], now): + ret.append(event) + else: + ret.append(event) return ret diff --git a/salt/cache/__init__.py b/salt/cache/__init__.py index 94d7a36f1e..fc5e5f0972 100644 --- a/salt/cache/__init__.py +++ b/salt/cache/__init__.py @@ -73,7 +73,7 @@ class Cache(object): self.cachedir = opts.get('cachedir', salt.syspaths.CACHE_DIR) else: self.cachedir = cachedir - self.driver = opts.get('cache', salt.config.DEFAULT_MASTER_OPTS) + self.driver = opts.get('cache', salt.config.DEFAULT_MASTER_OPTS['cache']) self.serial = Serial(opts) self._modules = None self._kwargs = kwargs diff --git a/salt/cache/consul.py b/salt/cache/consul.py index d8f1b32a74..d226cad64c 100644 --- a/salt/cache/consul.py +++ b/salt/cache/consul.py @@ -4,6 +4,8 @@ Minion data cache plugin for Consul key/value data store. .. versionadded:: 2016.11.2 +:depends: python-consul >= 0.2.0 + It is up to the system administrator to set up and configure the Consul infrastructure. All is needed for this plugin is a working Consul agent with a read-write access to the key-value store. @@ -81,8 +83,11 @@ def __virtual__(): 'verify': __opts__.get('consul.verify', True), } - global api - api = consul.Consul(**consul_kwargs) + try: + global api + api = consul.Consul(**consul_kwargs) + except AttributeError: + return (False, "Failed to invoke consul.Consul, please make sure you have python-consul >= 0.2.0 installed") return __virtualname__ diff --git a/salt/cli/caller.py b/salt/cli/caller.py index 75025ea473..5c04bab11c 100644 --- a/salt/cli/caller.py +++ b/salt/cli/caller.py @@ -157,7 +157,7 @@ class BaseCaller(object): ''' ret = {} fun = self.opts['fun'] - ret['jid'] = salt.utils.jid.gen_jid() + ret['jid'] = salt.utils.jid.gen_jid(self.opts) proc_fn = os.path.join( salt.minion.get_proc_dir(self.opts['cachedir']), ret['jid'] diff --git a/salt/cli/cp.py b/salt/cli/cp.py index 19efc47ee3..b45edc0c4d 100644 --- a/salt/cli/cp.py +++ b/salt/cli/cp.py @@ -184,9 +184,10 @@ class SaltCP(object): if gzip \ else salt.utils.itertools.read_file - minions = salt.utils.minions.CkMinions(self.opts).check_minions( + _res = salt.utils.minions.CkMinions(self.opts).check_minions( tgt, tgt_type=selected_target_option or 'glob') + minions = _res['minions'] local = salt.client.get_local_client(self.opts['conf_file']) diff --git a/salt/cli/spm.py b/salt/cli/spm.py index 3d347c80a8..303e5ce65f 100644 --- a/salt/cli/spm.py +++ b/salt/cli/spm.py @@ -14,7 +14,7 @@ from __future__ import absolute_import # Import Salt libs import salt.spm import salt.utils.parsers as parsers -from salt.utils.verify import verify_log +from salt.utils.verify import verify_log, verify_env class SPM(parsers.SPMParser): @@ -29,6 +29,10 @@ class SPM(parsers.SPMParser): ui = salt.spm.SPMCmdlineInterface() self.parse_args() self.setup_logfile_logger() + v_dirs = [ + self.config['cachedir'], + ] + verify_env(v_dirs, self.config['user'],) verify_log(self.config) client = salt.spm.SPMClient(ui, self.config) client.run(self.args) diff --git a/salt/client/__init__.py b/salt/client/__init__.py index 61a6a12db8..b047e59936 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -347,7 +347,8 @@ class LocalClient(object): return self._check_pub_data(pub_data) def gather_minions(self, tgt, expr_form): - return salt.utils.minions.CkMinions(self.opts).check_minions(tgt, tgt_type=expr_form) + _res = salt.utils.minions.CkMinions(self.opts).check_minions(tgt, tgt_type=expr_form) + return _res['minions'] @tornado.gen.coroutine def run_job_async( @@ -1141,6 +1142,7 @@ class LocalClient(object): minion_timeouts = {} found = set() + missing = [] # Check to see if the jid is real, if not return the empty dict try: if self.returners[u'{0}.get_load'.format(self.opts[u'master_job_cache'])](jid) == {}: @@ -1179,6 +1181,8 @@ class LocalClient(object): break if u'minions' in raw.get(u'data', {}): minions.update(raw[u'data'][u'minions']) + if u'missing' in raw.get(u'data', {}): + missing.extend(raw[u'data'][u'missing']) continue if u'return' not in raw[u'data']: continue @@ -1320,6 +1324,10 @@ class LocalClient(object): for minion in list((minions - found)): yield {minion: {u'failed': True}} + if missing: + for minion in missing: + yield {minion: {'failed': True}} + def get_returns( self, jid, diff --git a/salt/client/api.py b/salt/client/api.py index 55a6e32728..27ad6a46f3 100644 --- a/salt/client/api.py +++ b/salt/client/api.py @@ -14,8 +14,9 @@ client applications. http://docs.saltstack.com/ref/clients/index.html ''' -from __future__ import absolute_import + # Import Python libs +from __future__ import absolute_import import os # Import Salt libs @@ -24,9 +25,9 @@ import salt.auth import salt.client import salt.runner import salt.wheel -import salt.utils +import salt.utils.args +import salt.utils.event import salt.syspaths as syspaths -from salt.utils.event import tagify from salt.exceptions import EauthAuthenticationError @@ -229,7 +230,7 @@ class APIClient(object): functions = self.wheelClient.functions elif client == u'runner': functions = self.runnerClient.functions - result = {u'master': salt.utils.argspec_report(functions, module)} + result = {u'master': salt.utils.args.argspec_report(functions, module)} return result def create_token(self, creds): @@ -322,4 +323,4 @@ class APIClient(object): Need to convert this to a master call with appropriate authentication ''' - return self.event.fire_event(data, tagify(tag, u'wui')) + return self.event.fire_event(data, salt.utils.event.tagify(tag, u'wui')) diff --git a/salt/client/mixins.py b/salt/client/mixins.py index 2e4214e52b..f5fc1d22f7 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -16,7 +16,8 @@ import copy as pycopy # Import Salt libs import salt.exceptions import salt.minion -import salt.utils +import salt.utils # Can be removed once daemonize, get_specific_user, format_call are moved +import salt.utils.args import salt.utils.doc import salt.utils.error import salt.utils.event @@ -25,6 +26,7 @@ import salt.utils.job import salt.utils.lazy import salt.utils.platform import salt.utils.process +import salt.utils.state import salt.utils.versions import salt.transport import salt.log.setup @@ -297,7 +299,7 @@ class SyncClientMixin(object): # this is not to clutter the output with the module loading # if we have a high debug level. self.mminion # pylint: disable=W0104 - jid = low.get(u'__jid__', salt.utils.jid.gen_jid()) + jid = low.get(u'__jid__', salt.utils.jid.gen_jid(self.opts)) tag = low.get(u'__tag__', salt.utils.event.tagify(jid, prefix=self.tag_prefix)) data = {u'fun': u'{0}.{1}'.format(self.client, fun), @@ -362,29 +364,19 @@ class SyncClientMixin(object): # packed into the top level object. The plan is to move away from # that since the caller knows what is an arg vs a kwarg, but while # we make the transition we will load "kwargs" using format_call if - # there are no kwargs in the low object passed in - f_call = None - if u'arg' not in low: + # there are no kwargs in the low object passed in. + + if u'arg' in low and u'kwarg' in low: + args = low[u'arg'] + kwargs = low[u'kwarg'] + else: f_call = salt.utils.format_call( self.functions[fun], low, expected_extra_kws=CLIENT_INTERNAL_KEYWORDS ) args = f_call.get(u'args', ()) - else: - args = low[u'arg'] - - if u'kwarg' not in low: - log.critical( - u'kwargs must be passed inside the low data within the ' - u'\'kwarg\' key. See usage of ' - u'salt.utils.args.parse_input() and ' - u'salt.minion.load_args_and_kwargs() elsewhere in the ' - u'codebase.' - ) - kwargs = {} - else: - kwargs = low[u'kwarg'] + kwargs = f_call.get(u'kwargs', {}) # Update the event data with loaded args and kwargs data[u'fun_args'] = list(args) + ([kwargs] if kwargs else []) @@ -396,7 +388,7 @@ class SyncClientMixin(object): data[u'success'] = True if isinstance(data[u'return'], dict) and u'data' in data[u'return']: # some functions can return boolean values - data[u'success'] = salt.utils.check_state_result(data[u'return'][u'data']) + data[u'success'] = salt.utils.state.check_result(data[u'return'][u'data']) except (Exception, SystemExit) as ex: if isinstance(ex, salt.exceptions.NotImplemented): data[u'return'] = str(ex) @@ -510,7 +502,7 @@ class AsyncClientMixin(object): def _gen_async_pub(self, jid=None): if jid is None: - jid = salt.utils.jid.gen_jid() + jid = salt.utils.jid.gen_jid(self.opts) tag = salt.utils.event.tagify(jid, prefix=self.tag_prefix) return {u'tag': tag, u'jid': jid} diff --git a/salt/cloud/clouds/digital_ocean.py b/salt/cloud/clouds/digitalocean.py similarity index 98% rename from salt/cloud/clouds/digital_ocean.py rename to salt/cloud/clouds/digitalocean.py index daf5b8f75a..d5bcb4fb6f 100644 --- a/salt/cloud/clouds/digital_ocean.py +++ b/salt/cloud/clouds/digitalocean.py @@ -20,7 +20,7 @@ under the "SSH Keys" section. personal_access_token: xxx ssh_key_file: /path/to/ssh/key/file ssh_key_names: my-key-name,my-key-name-2 - driver: digital_ocean + driver: digitalocean :depends: requests ''' @@ -59,10 +59,11 @@ except ImportError: # Get logging started log = logging.getLogger(__name__) -__virtualname__ = 'digital_ocean' +__virtualname__ = 'digitalocean' +__virtual_aliases__ = ('digital_ocean', 'do') -# Only load in this module if the DIGITAL_OCEAN configurations are in place +# Only load in this module if the DIGITALOCEAN configurations are in place def __virtual__(): ''' Check for DigitalOcean configurations @@ -274,7 +275,7 @@ def create(vm_): try: # Check for required profile parameters before sending any API calls. if vm_['profile'] and config.is_profile_configured(__opts__, - __active_provider_name__ or 'digital_ocean', + __active_provider_name__ or 'digitalocean', vm_['profile'], vm_=vm_) is False: return False @@ -441,7 +442,7 @@ def create(vm_): ret = create_node(kwargs) except Exception as exc: log.error( - 'Error creating {0} on DIGITAL_OCEAN\n\n' + 'Error creating {0} on DIGITALOCEAN\n\n' 'The following exception was thrown when trying to ' 'run the initial deployment: {1}'.format( vm_['name'], @@ -716,12 +717,12 @@ def import_keypair(kwargs=None, call=None): with salt.utils.files.fopen(kwargs['file'], 'r') as public_key_filename: public_key_content = public_key_filename.read() - digital_ocean_kwargs = { + digitalocean_kwargs = { 'name': kwargs['keyname'], 'public_key': public_key_content } - created_result = create_key(digital_ocean_kwargs, call=call) + created_result = create_key(digitalocean_kwargs, call=call) return created_result @@ -938,11 +939,11 @@ def show_pricing(kwargs=None, call=None): if not profile: return {'Error': 'The requested profile was not found'} - # Make sure the profile belongs to Digital Ocean + # Make sure the profile belongs to DigitalOcean provider = profile.get('provider', '0:0') comps = provider.split(':') - if len(comps) < 2 or comps[1] != 'digital_ocean': - return {'Error': 'The requested profile does not belong to Digital Ocean'} + if len(comps) < 2 or comps[1] != 'digitalocean': + return {'Error': 'The requested profile does not belong to DigitalOcean'} raw = {} ret = {} @@ -968,7 +969,7 @@ def list_floating_ips(call=None): CLI Examples: - ... code-block:: bash + .. code-block:: bash salt-cloud -f list_floating_ips my-digitalocean-config ''' @@ -1008,7 +1009,7 @@ def show_floating_ip(kwargs=None, call=None): CLI Examples: - ... code-block:: bash + .. code-block:: bash salt-cloud -f show_floating_ip my-digitalocean-config floating_ip='45.55.96.47' ''' @@ -1041,7 +1042,7 @@ def create_floating_ip(kwargs=None, call=None): CLI Examples: - ... code-block:: bash + .. code-block:: bash salt-cloud -f create_floating_ip my-digitalocean-config region='NYC2' @@ -1083,7 +1084,7 @@ def delete_floating_ip(kwargs=None, call=None): CLI Examples: - ... code-block:: bash + .. code-block:: bash salt-cloud -f delete_floating_ip my-digitalocean-config floating_ip='45.55.96.47' ''' @@ -1118,7 +1119,7 @@ def assign_floating_ip(kwargs=None, call=None): CLI Examples: - ... code-block:: bash + .. code-block:: bash salt-cloud -f assign_floating_ip my-digitalocean-config droplet_id=1234567 floating_ip='45.55.96.47' ''' @@ -1151,7 +1152,7 @@ def unassign_floating_ip(kwargs=None, call=None): CLI Examples: - ... code-block:: bash + .. code-block:: bash salt-cloud -f unassign_floating_ip my-digitalocean-config floating_ip='45.55.96.47' ''' diff --git a/salt/cloud/clouds/gce.py b/salt/cloud/clouds/gce.py index 8d2f891c12..6e26cf8b95 100644 --- a/salt/cloud/clouds/gce.py +++ b/salt/cloud/clouds/gce.py @@ -2643,7 +2643,7 @@ def show_pricing(kwargs=None, call=None): if not profile: return {'Error': 'The requested profile was not found'} - # Make sure the profile belongs to Digital Ocean + # Make sure the profile belongs to DigitalOcean provider = profile.get('provider', '0:0') comps = provider.split(':') if len(comps) < 2 or comps[1] != 'gce': diff --git a/salt/cloud/clouds/libvirt.py b/salt/cloud/clouds/libvirt.py index 53a8c4b659..c3a1b56c0e 100644 --- a/salt/cloud/clouds/libvirt.py +++ b/salt/cloud/clouds/libvirt.py @@ -41,6 +41,7 @@ Example profile: master_port: 5506 Tested on: +- Fedora 26 (libvirt 3.2.1, qemu 2.9.1) - Fedora 25 (libvirt 1.3.3.2, qemu 2.6.1) - Fedora 23 (libvirt 1.2.18, qemu 2.4.1) - Centos 7 (libvirt 1.2.17, qemu 1.5.3) @@ -82,9 +83,6 @@ from salt.exceptions import ( SaltCloudSystemExit ) -# Get logging started -log = logging.getLogger(__name__) - VIRT_STATE_NAME_MAP = {0: 'running', 1: 'running', 2: 'running', @@ -99,6 +97,20 @@ IP_LEARNING_XML = """ __virtualname__ = 'libvirt' +# Set up logging +log = logging.getLogger(__name__) + + +def libvirt_error_handler(ctx, error): + ''' + Redirect stderr prints from libvirt to salt logging. + ''' + log.debug("libvirt error {0}".format(error)) + + +if HAS_LIBVIRT: + libvirt.registerErrorHandler(f=libvirt_error_handler, ctx=None) + def __virtual__(): ''' @@ -280,7 +292,7 @@ def create(vm_): validate_xml = vm_.get('validate_xml') if vm_.get('validate_xml') is not None else True - log.info("Cloning machine '{0}' with strategy '{1}' validate_xml='{2}'".format(vm_['name'], clone_strategy, validate_xml)) + log.info("Cloning '{0}' with strategy '{1}' validate_xml='{2}'".format(vm_['name'], clone_strategy, validate_xml)) try: # Check for required profile parameters before sending any API calls. @@ -516,7 +528,7 @@ def destroy(name, call=None): 'event', 'destroying instance', 'salt/cloud/{0}/destroying'.format(name), - {'name': name}, + args={'name': name}, sock_dir=__opts__['sock_dir'], transport=__opts__['transport'] ) @@ -527,7 +539,7 @@ def destroy(name, call=None): 'event', 'destroyed instance', 'salt/cloud/{0}/destroyed'.format(name), - {'name': name}, + args={'name': name}, sock_dir=__opts__['sock_dir'], transport=__opts__['transport'] ) diff --git a/salt/cloud/clouds/linode.py b/salt/cloud/clouds/linode.py index 6b761cb985..b64b69685c 100644 --- a/salt/cloud/clouds/linode.py +++ b/salt/cloud/clouds/linode.py @@ -44,9 +44,6 @@ from salt.exceptions import ( SaltCloudSystemExit ) -# Import Salt-Cloud Libs -import salt.utils.cloud - # Get logging started log = logging.getLogger(__name__) @@ -1193,7 +1190,7 @@ def list_nodes_select(call=None): ''' Return a list of the VMs that are on the provider, with select fields. ''' - return salt.utils.cloud.list_nodes_select( + return __utils__['cloud.list_nodes_select']( list_nodes_full(), __opts__['query.selection'], call, ) @@ -1503,7 +1500,7 @@ def _query(action=None, if LASTCALL >= now: time.sleep(ratelimit_sleep) - result = salt.utils.http.query( + result = __utils__['http.query']( url, method, params=args, diff --git a/salt/cloud/clouds/virtualbox.py b/salt/cloud/clouds/virtualbox.py index 903722fd39..4266ce5f0e 100644 --- a/salt/cloud/clouds/virtualbox.py +++ b/salt/cloud/clouds/virtualbox.py @@ -24,7 +24,6 @@ import logging # Import salt libs from salt.exceptions import SaltCloudSystemExit import salt.config as config -import salt.utils.cloud as cloud # Import Third Party Libs try: @@ -136,7 +135,7 @@ def create(vm_info): ) log.debug("Going to fire event: starting create") - cloud.fire_event( + __utils__['cloud.fire_event']( 'event', 'starting create', 'salt/cloud/{0}/creating'.format(vm_info['name']), @@ -151,7 +150,7 @@ def create(vm_info): 'clone_from': vm_info['clonefrom'] } - cloud.fire_event( + __utils__['cloud.fire_event']( 'event', 'requesting instance', 'salt/cloud/{0}/requesting'.format(vm_info['name']), @@ -174,10 +173,10 @@ def create(vm_info): vm_info['key_filename'] = key_filename vm_info['ssh_host'] = ip - res = cloud.bootstrap(vm_info, __opts__) + res = __utils__['cloud.bootstrap'](vm_info) vm_result.update(res) - cloud.fire_event( + __utils__['cloud.fire_event']( 'event', 'created machine', 'salt/cloud/{0}/created'.format(vm_info['name']), @@ -269,7 +268,7 @@ def list_nodes(kwargs=None, call=None): "private_ips", "public_ips", ] - return cloud.list_nodes_select( + return __utils__['cloud.list_nodes_select']( list_nodes_full('function'), attributes, call, ) @@ -278,7 +277,7 @@ def list_nodes_select(call=None): """ Return a list of the VMs that are on the provider, with select fields """ - return cloud.list_nodes_select( + return __utils__['cloud.list_nodes_select']( list_nodes_full('function'), __opts__['query.selection'], call, ) @@ -306,7 +305,7 @@ def destroy(name, call=None): if not vb_machine_exists(name): return "{0} doesn't exist and can't be deleted".format(name) - cloud.fire_event( + __utils__['cloud.fire_event']( 'event', 'destroying instance', 'salt/cloud/{0}/destroying'.format(name), @@ -317,7 +316,7 @@ def destroy(name, call=None): vb_destroy_machine(name) - cloud.fire_event( + __utils__['cloud.fire_event']( 'event', 'destroyed instance', 'salt/cloud/{0}/destroyed'.format(name), diff --git a/salt/config/__init__.py b/salt/config/__init__.py index 593828af58..6a89e1f485 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -53,7 +53,7 @@ _DFLT_LOG_DATEFMT = '%H:%M:%S' _DFLT_LOG_DATEFMT_LOGFILE = '%Y-%m-%d %H:%M:%S' _DFLT_LOG_FMT_CONSOLE = '[%(levelname)-8s] %(message)s' _DFLT_LOG_FMT_LOGFILE = ( - '%(asctime)s,%(msecs)03d [%(name)-17s][%(levelname)-8s][%(process)d] %(message)s' + '%(asctime)s,%(msecs)03d [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(process)d] %(message)s' ) _DFLT_REFSPECS = ['+refs/heads/*:refs/remotes/origin/*', '+refs/tags/*:refs/tags/*'] @@ -111,9 +111,10 @@ VALID_OPTS = { 'master_port': (six.string_types, int), # The behaviour of the minion when connecting to a master. Can specify 'failover', - # 'disable' or 'func'. If 'func' is specified, the 'master' option should be set to an - # exec module function to run to determine the master hostname. If 'disable' is specified - # the minion will run, but will not try to connect to a master. + # 'disable', 'distributed', or 'func'. If 'func' is specified, the 'master' option should be + # set to an exec module function to run to determine the master hostname. If 'disable' is + # specified the minion will run, but will not try to connect to a master. If 'distributed' + # is specified the minion will try to deterministically pick a master based on its' id. 'master_type': str, # Specify the format in which the master address will be specified. Can @@ -186,6 +187,16 @@ VALID_OPTS = { # A unique identifier for this daemon 'id': str, + # Use a module function to determine the unique identifier. If this is + # set and 'id' is not set, it will allow invocation of a module function + # to determine the value of 'id'. For simple invocations without function + # arguments, this may be a string that is the function name. For + # invocations with function arguments, this may be a dictionary with the + # key being the function name, and the value being an embedded dictionary + # where each key is a function argument name and each value is the + # corresponding argument value. + 'id_function': (dict, str), + # The directory to store all cache files. 'cachedir': str, @@ -332,7 +343,7 @@ VALID_OPTS = { # Whether or not scheduled mine updates should be accompanied by a job return for the job cache 'mine_return_job': bool, - # Schedule a mine update every n number of seconds + # The number of minutes between mine updates. 'mine_interval': int, # The ipc strategy. (i.e., sockets versus tcp, etc) @@ -417,6 +428,12 @@ VALID_OPTS = { # Tell the client to display the jid when a job is published 'show_jid': bool, + # Ensure that a generated jid is always unique. If this is set, the jid + # format is different due to an underscore and process id being appended + # to the jid. WARNING: A change to the jid format may break external + # applications that depend on the original format. + 'unique_jid': bool, + # Tells the highstate outputter to show successful states. False will omit successes. 'state_verbose': bool, @@ -573,6 +590,23 @@ VALID_OPTS = { # False in 2016.3.0 'add_proxymodule_to_opts': bool, + # Merge pillar data into configuration opts. + # As multiple proxies can run on the same server, we may need different + # configuration options for each, while there's one single configuration file. + # The solution is merging the pillar data of each proxy minion into the opts. + 'proxy_merge_pillar_in_opts': bool, + + # Deep merge of pillar data into configuration opts. + # Evaluated only when `proxy_merge_pillar_in_opts` is True. + 'proxy_deep_merge_pillar_in_opts': bool, + + # The strategy used when merging pillar into opts. + # Considered only when `proxy_merge_pillar_in_opts` is True. + 'proxy_merge_pillar_in_opts_strategy': str, + + # Allow enabling mine details using pillar data. + 'proxy_mines_pillar': bool, + # In some particular cases, always alive proxies are not beneficial. # This option can be used in those less dynamic environments: # the user can request the connection @@ -908,6 +942,7 @@ VALID_OPTS = { 'ssh_scan_timeout': float, 'ssh_identities_only': bool, 'ssh_log_file': str, + 'ssh_config_file': str, # Enable ioflo verbose logging. Warning! Very verbose! 'ioflo_verbose': int, @@ -1079,6 +1114,11 @@ VALID_OPTS = { # (in other words, require that minions have 'minion_sign_messages' # turned on) 'require_minion_sign_messages': bool, + + # The list of config entries to be passed to external pillar function as + # part of the extra_minion_data param + # Subconfig entries can be specified by using the ':' notation (e.g. key:subkey) + 'pass_to_ext_pillars': (six.string_types, list), } # default configurations @@ -1102,6 +1142,7 @@ DEFAULT_MINION_OPTS = { 'root_dir': salt.syspaths.ROOT_DIR, 'pki_dir': os.path.join(salt.syspaths.CONFIG_DIR, 'pki', 'minion'), 'id': '', + 'id_function': {}, 'cachedir': os.path.join(salt.syspaths.CACHE_DIR, 'minion'), 'append_minionid_config_dirs': [], 'cache_jobs': False, @@ -1197,6 +1238,7 @@ DEFAULT_MINION_OPTS = { 'gitfs_ref_types': ['branch', 'tag', 'sha'], 'gitfs_refspecs': _DFLT_REFSPECS, 'gitfs_disable_saltenv_mapping': False, + 'unique_jid': False, 'hash_type': 'sha256', 'disable_modules': [], 'disable_returners': [], @@ -1441,6 +1483,7 @@ DEFAULT_MASTER_OPTS = { 'hgfs_saltenv_blacklist': [], 'show_timeout': True, 'show_jid': False, + 'unique_jid': False, 'svnfs_remotes': [], 'svnfs_mountpoint': '', 'svnfs_root': '', @@ -1607,6 +1650,7 @@ DEFAULT_MASTER_OPTS = { 'ssh_scan_timeout': 0.01, 'ssh_identities_only': False, 'ssh_log_file': os.path.join(salt.syspaths.LOGS_DIR, 'ssh'), + 'ssh_config_file': os.path.join(salt.syspaths.HOME_DIR, '.ssh', 'config'), 'master_floscript': os.path.join(FLO_DIR, 'master.flo'), 'worker_floscript': os.path.join(FLO_DIR, 'worker.flo'), 'maintenance_floscript': os.path.join(FLO_DIR, 'maint.flo'), @@ -1673,6 +1717,12 @@ DEFAULT_PROXY_MINION_OPTS = { 'append_minionid_config_dirs': ['cachedir', 'pidfile', 'default_include', 'extension_modules'], 'default_include': 'proxy.d/*.conf', + 'proxy_merge_pillar_in_opts': False, + 'proxy_deep_merge_pillar_in_opts': False, + 'proxy_merge_pillar_in_opts_strategy': 'smart', + + 'proxy_mines_pillar': True, + # By default, proxies will preserve the connection. # If this option is set to False, # the connection with the remote dumb device @@ -2671,7 +2721,7 @@ def old_to_new(opts): providers = ( 'AWS', 'CLOUDSTACK', - 'DIGITAL_OCEAN', + 'DIGITALOCEAN', 'EC2', 'GOGRID', 'IBMSCE', @@ -3335,6 +3385,57 @@ def _cache_id(minion_id, cache_file): log.error('Could not cache minion ID: {0}'.format(exc)) +def call_id_function(opts): + ''' + Evaluate the function that determines the ID if the 'id_function' + option is set and return the result + ''' + if opts.get('id'): + return opts['id'] + + # Import 'salt.loader' here to avoid a circular dependency + import salt.loader as loader + + if isinstance(opts['id_function'], str): + mod_fun = opts['id_function'] + fun_kwargs = {} + elif isinstance(opts['id_function'], dict): + mod_fun, fun_kwargs = six.next(six.iteritems(opts['id_function'])) + if fun_kwargs is None: + fun_kwargs = {} + else: + log.error('\'id_function\' option is neither a string nor a dictionary') + sys.exit(salt.defaults.exitcodes.EX_GENERIC) + + # split module and function and try loading the module + mod, fun = mod_fun.split('.') + if not opts.get('grains'): + # Get grains for use by the module + opts['grains'] = loader.grains(opts) + + try: + id_mod = loader.raw_mod(opts, mod, fun) + if not id_mod: + raise KeyError + # we take whatever the module returns as the minion ID + newid = id_mod[mod_fun](**fun_kwargs) + if not isinstance(newid, str) or not newid: + log.error('Function {0} returned value "{1}" of type {2} instead of string'.format( + mod_fun, newid, type(newid)) + ) + sys.exit(salt.defaults.exitcodes.EX_GENERIC) + log.info('Evaluated minion ID from module: {0}'.format(mod_fun)) + return newid + except TypeError: + log.error('Function arguments {0} are incorrect for function {1}'.format( + fun_kwargs, mod_fun) + ) + sys.exit(salt.defaults.exitcodes.EX_GENERIC) + except KeyError: + log.error('Failed to load module {0}'.format(mod_fun)) + sys.exit(salt.defaults.exitcodes.EX_GENERIC) + + def get_id(opts, cache_minion_id=False): ''' Guess the id of the minion. @@ -3376,13 +3477,21 @@ def get_id(opts, cache_minion_id=False): log.debug('Guessing ID. The id can be explicitly set in {0}' .format(os.path.join(salt.syspaths.CONFIG_DIR, 'minion'))) - newid = salt.utils.network.generate_minion_id() + if opts.get('id_function'): + newid = call_id_function(opts) + else: + newid = salt.utils.network.generate_minion_id() if opts.get('minion_id_lowercase'): newid = newid.lower() log.debug('Changed minion id {0} to lowercase.'.format(newid)) if '__role' in opts and opts.get('__role') == 'minion': - log.debug('Found minion id from generate_minion_id(): {0}'.format(newid)) + if opts.get('id_function'): + log.debug('Found minion id from external function {0}: {1}'.format( + opts['id_function'], newid)) + else: + log.debug('Found minion id from generate_minion_id(): {0}'.format( + newid)) if cache_minion_id and opts.get('minion_id_caching', True): _cache_id(newid, id_cache) is_ipv4 = salt.utils.network.is_ipv4(newid) diff --git a/salt/config/schemas/esxcluster.py b/salt/config/schemas/esxcluster.py new file mode 100644 index 0000000000..ba88357cf7 --- /dev/null +++ b/salt/config/schemas/esxcluster.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu (alexandru.bleotu@morganstanley.com)` + + + salt.config.schemas.esxcluster + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ESX Cluster configuration schemas +''' + +# Import Python libs +from __future__ import absolute_import + +# Import Salt libs +from salt.utils.schema import (Schema, + DefinitionsSchema, + ComplexSchemaItem, + DictItem, + ArrayItem, + IntegerItem, + BooleanItem, + StringItem, + AnyOfItem) + + +class OptionValueItem(ComplexSchemaItem): + '''Sechma item of the OptionValue''' + + title = 'OptionValue' + key = StringItem(title='Key', required=True) + value = AnyOfItem(items=[StringItem(), BooleanItem(), IntegerItem()]) + + +class AdmissionControlPolicyItem(ComplexSchemaItem): + ''' + Schema item of the HA admission control policy + ''' + + title = 'Admission Control Policy' + + cpu_failover_percent = IntegerItem( + title='CPU Failover Percent', + minimum=0, maximum=100) + memory_failover_percent = IntegerItem( + title='Memory Failover Percent', + minimum=0, maximum=100) + + +class DefaultVmSettingsItem(ComplexSchemaItem): + ''' + Schema item of the HA default vm settings + ''' + + title = 'Default VM Settings' + + isolation_response = StringItem( + title='Isolation Response', + enum=['clusterIsolationResponse', 'none', 'powerOff', 'shutdown']) + restart_priority = StringItem( + title='Restart Priority', + enum=['clusterRestartPriority', 'disabled', 'high', 'low', 'medium']) + + +class HAConfigItem(ComplexSchemaItem): + ''' + Schema item of ESX cluster high availability + ''' + + title = 'HA Configuration' + description = 'ESX cluster HA configuration json schema item' + + enabled = BooleanItem( + title='Enabled', + description='Specifies if HA should be enabled') + admission_control_enabled = BooleanItem( + title='Admission Control Enabled') + admission_control_policy = AdmissionControlPolicyItem() + default_vm_settings = DefaultVmSettingsItem() + hb_ds_candidate_policy = StringItem( + title='Heartbeat Datastore Candidate Policy', + enum=['allFeasibleDs', 'allFeasibleDsWithUserPreference', + 'userSelectedDs']) + host_monitoring = StringItem(title='Host Monitoring', + choices=['enabled', 'disabled']) + options = ArrayItem(min_items=1, items=OptionValueItem()) + vm_monitoring = StringItem( + title='Vm Monitoring', + choices=['vmMonitoringDisabled', 'vmAndAppMonitoring', + 'vmMonitoringOnly']) + + +class vSANClusterConfigItem(ComplexSchemaItem): + ''' + Schema item of the ESX cluster vSAN configuration + ''' + + title = 'vSAN Configuration' + description = 'ESX cluster vSAN configurationi item' + + enabled = BooleanItem( + title='Enabled', + description='Specifies if vSAN should be enabled') + auto_claim_storage = BooleanItem( + title='Auto Claim Storage', + description='Specifies whether the storage of member ESXi hosts should ' + 'be automatically claimed for vSAN') + dedup_enabled = BooleanItem( + title='Enabled', + description='Specifies dedup should be enabled') + compression_enabled = BooleanItem( + title='Enabled', + description='Specifies if compression should be enabled') + + +class DRSConfigItem(ComplexSchemaItem): + ''' + Schema item of the ESX cluster DRS configuration + ''' + + title = 'DRS Configuration' + description = 'ESX cluster DRS configuration item' + + enabled = BooleanItem( + title='Enabled', + description='Specifies if DRS should be enabled') + vmotion_rate = IntegerItem( + title='vMotion rate', + description='Aggressiveness to do automatic vMotions: ' + '1 (least aggressive) - 5 (most aggressive)', + minimum=1, + maximum=5) + default_vm_behavior = StringItem( + title='Default VM DRS Behavior', + description='Specifies the default VM DRS behavior', + enum=['fullyAutomated', 'partiallyAutomated', 'manual']) + + +class ESXClusterConfigSchema(DefinitionsSchema): + ''' + Schema of the ESX cluster config + ''' + + title = 'ESX Cluster Configuration Schema' + description = 'ESX cluster configuration schema' + + ha = HAConfigItem() + vsan = vSANClusterConfigItem() + drs = DRSConfigItem() + vm_swap_placement = StringItem(title='VM Swap Placement') + + +class ESXClusterEntitySchema(Schema): + '''Schema of the ESX cluster entity''' + + title = 'ESX Cluster Entity Schema' + description = 'ESX cluster entity schema' + + type = StringItem(title='Type', + description='Specifies the entity type', + required=True, + enum=['cluster']) + + datacenter = StringItem(title='Datacenter', + description='Specifies the cluster datacenter', + required=True, + pattern=r'\w+') + + cluster = StringItem(title='Cluster', + description='Specifies the cluster name', + required=True, + pattern=r'\w+') + + +class LicenseSchema(Schema): + ''' + Schema item of the ESX cluster vSAN configuration + ''' + + title = 'Licenses schema' + description = 'License configuration schema' + + licenses = DictItem( + title='Licenses', + description='Dictionary containing the license name to key mapping', + required=True, + additional_properties=StringItem( + title='License Key', + description='Specifies the license key', + pattern=r'^(\w{5}-\w{5}-\w{5}-\w{5}-\w{5})$')) + + +class EsxclusterProxySchema(Schema): + ''' + Schema of the esxcluster proxy input + ''' + + title = 'Esxcluster Proxy Schema' + description = 'Esxcluster proxy schema' + additional_properties = False + proxytype = StringItem(required=True, + enum=['esxcluster']) + vcenter = StringItem(required=True, pattern=r'[^\s]+') + datacenter = StringItem(required=True) + cluster = StringItem(required=True) + mechanism = StringItem(required=True, enum=['userpass', 'sspi']) + username = StringItem() + passwords = ArrayItem(min_items=1, + items=StringItem(), + unique_items=True) + # TODO Should be changed when anyOf is supported for schemas + domain = StringItem() + principal = StringItem() + protocol = StringItem() + port = IntegerItem(minimum=1) diff --git a/salt/config/schemas/vcenter.py b/salt/config/schemas/vcenter.py new file mode 100644 index 0000000000..4867923f27 --- /dev/null +++ b/salt/config/schemas/vcenter.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Rod McKenzie (roderick.mckenzie@morganstanley.com)` + :codeauthor: :email:`Alexandru Bleotu (alexandru.bleotu@morganstanley.com)` + + salt.config.schemas.vcenter + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + VCenter configuration schemas +''' + +# Import Python libs +from __future__ import absolute_import + +# Import Salt libs +from salt.utils.schema import (Schema, + StringItem) + + +class VCenterEntitySchema(Schema): + ''' + Entity Schema for a VCenter. + ''' + title = 'VCenter Entity Schema' + description = 'VCenter entity schema' + type = StringItem(title='Type', + description='Specifies the entity type', + required=True, + enum=['vcenter']) + + vcenter = StringItem(title='vCenter', + description='Specifies the vcenter hostname', + required=True) diff --git a/salt/daemons/flo/core.py b/salt/daemons/flo/core.py index 91f1e1b6b4..1a11e08aed 100644 --- a/salt/daemons/flo/core.py +++ b/salt/daemons/flo/core.py @@ -400,7 +400,7 @@ class SaltRaetRoadStackJoiner(ioflo.base.deeding.Deed): kind=kinds.applKinds.master)) except gaierror as ex: log.warning("Unable to connect to master {0}: {1}".format(mha, ex)) - if self.opts.value.get('master_type') != 'failover': + if self.opts.value.get(u'master_type') not in (u'failover', u'distributed'): raise ex if not stack.remotes: raise ex diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index 3ee965daf3..9c59194a05 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -550,11 +550,12 @@ class RemoteFuncs(object): if match_type.lower() == 'compound': match_type = 'compound_pillar_exact' checker = salt.utils.minions.CkMinions(self.opts) - minions = checker.check_minions( + _res = checker.check_minions( load['tgt'], match_type, greedy=False ) + minions = _res['minions'] for minion in minions: fdata = self.cache.fetch('minions/{0}'.format(minion), 'mine') if isinstance(fdata, dict): @@ -718,7 +719,7 @@ class RemoteFuncs(object): Handle the return data sent from the minions ''' # Generate EndTime - endtime = salt.utils.jid.jid_to_time(salt.utils.jid.gen_jid()) + endtime = salt.utils.jid.jid_to_time(salt.utils.jid.gen_jid(self.opts)) # If the return data is invalid, just ignore it if any(key not in load for key in ('return', 'jid', 'id')): return False @@ -872,9 +873,10 @@ class RemoteFuncs(object): pub_load['tgt_type'] = load['tgt_type'] ret = {} ret['jid'] = self.local.cmd_async(**pub_load) - ret['minions'] = self.ckminions.check_minions( + _res = self.ckminions.check_minions( load['tgt'], pub_load['tgt_type']) + ret['minions'] = _res['minions'] auth_cache = os.path.join( self.opts['cachedir'], 'publish_auth') @@ -1011,35 +1013,33 @@ class LocalFuncs(object): ''' Send a master control function back to the runner system ''' - if 'token' in load: - auth_type = 'token' - err_name = 'TokenAuthenticationError' - token = self.loadauth.authenticate_token(load) - if not token: - return dict(error=dict(name=err_name, - message='Authentication failure of type "token" occurred.')) - username = token['name'] - if self.opts['keep_acl_in_token'] and 'auth_list' in token: - auth_list = token['auth_list'] - else: - load['eauth'] = token['eauth'] - load['username'] = username - auth_list = self.loadauth.get_auth_list(load) - else: - auth_type = 'eauth' - err_name = 'EauthAuthenticationError' - username = load.get('username', 'UNKNOWN') - if not self.loadauth.authenticate_eauth(load): - return dict(error=dict(name=err_name, - message=('Authentication failure of type "eauth" occurred ' - 'for user {0}.').format(username))) - auth_list = self.loadauth.get_auth_list(load) + # All runner opts pass through eauth + auth_type, err_name, key = self._prep_auth_info(load) - if not self.ckminions.runner_check(auth_list, load['fun'], load['kwarg']): - return dict(error=dict(name=err_name, - message=('Authentication failure of type "{0}" occurred ' - 'for user {1}.').format(auth_type, username))) + # Authenticate + auth_check = self.loadauth.check_authentication(load, auth_type) + error = auth_check.get('error') + if error: + # Authentication error occurred: do not continue. + return {'error': error} + + # Authorize + runner_check = self.ckminions.runner_check( + auth_check.get('auth_list', []), + load['fun'], + load['kwarg'] + ) + username = auth_check.get('username') + if not runner_check: + return {'error': {'name': err_name, + 'message': 'Authentication failure of type "{0}" occurred ' + 'for user {1}.'.format(auth_type, username)}} + elif isinstance(runner_check, dict) and 'error' in runner_check: + # A dictionary with an error name/message was handled by ckminions.runner_check + return runner_check + + # Authorized. Do the job! try: fun = load.pop('fun') runner_client = salt.runner.RunnerClient(self.opts) @@ -1048,56 +1048,49 @@ class LocalFuncs(object): username) except Exception as exc: log.error('Exception occurred while ' - 'introspecting {0}: {1}'.format(fun, exc)) - return dict(error=dict(name=exc.__class__.__name__, - args=exc.args, - message=str(exc))) + 'introspecting {0}: {1}'.format(fun, exc)) + return {'error': {'name': exc.__class__.__name__, + 'args': exc.args, + 'message': str(exc)}} def wheel(self, load): ''' Send a master control function back to the wheel system ''' # All wheel ops pass through eauth - if 'token' in load: - auth_type = 'token' - err_name = 'TokenAuthenticationError' - token = self.loadauth.authenticate_token(load) - if not token: - return dict(error=dict(name=err_name, - message='Authentication failure of type "token" occurred.')) - username = token['name'] - if self.opts['keep_acl_in_token'] and 'auth_list' in token: - auth_list = token['auth_list'] - else: - load['eauth'] = token['eauth'] - load['username'] = username - auth_list = self.loadauth.get_auth_list(load) - elif 'eauth' in load: - auth_type = 'eauth' - err_name = 'EauthAuthenticationError' - username = load.get('username', 'UNKNOWN') - if not self.loadauth.authenticate_eauth(load): - return dict(error=dict(name=err_name, - message=('Authentication failure of type "eauth" occurred for ' - 'user {0}.').format(username))) - auth_list = self.loadauth.get_auth_list(load) - else: - auth_type = 'user' - err_name = 'UserAuthenticationError' - username = load.get('username', 'UNKNOWN') - if not self.loadauth.authenticate_key(load, self.key): - return dict(error=dict(name=err_name, - message=('Authentication failure of type "user" occurred for ' - 'user {0}.').format(username))) + auth_type, err_name, key = self._prep_auth_info(load) + # Authenticate + auth_check = self.loadauth.check_authentication( + load, + auth_type, + key=key, + show_username=True + ) + error = auth_check.get('error') + + if error: + # Authentication error occurred: do not continue. + return {'error': error} + + # Authorize + username = auth_check.get('username') if auth_type != 'user': - if not self.ckminions.wheel_check(auth_list, load['fun'], load['kwarg']): - return dict(error=dict(name=err_name, - message=('Authentication failure of type "{0}" occurred for ' - 'user {1}.').format(auth_type, username))) + wheel_check = self.ckminions.wheel_check( + auth_check.get('auth_list', []), + load['fun'], + load['kwarg'] + ) + if not wheel_check: + return {'error': {'name': err_name, + 'message': 'Authentication failure of type "{0}" occurred for ' + 'user {1}.'.format(auth_type, username)}} + elif isinstance(wheel_check, dict) and 'error' in wheel_check: + # A dictionary with an error name/message was handled by ckminions.wheel_check + return wheel_check # Authenticated. Do the job. - jid = salt.utils.jid.gen_jid() + jid = salt.utils.jid.gen_jid(self.opts) fun = load.pop('fun') tag = salt.utils.event.tagify(jid, prefix='wheel') data = {'fun': "wheel.{0}".format(fun), @@ -1114,7 +1107,7 @@ class LocalFuncs(object): 'data': data} except Exception as exc: log.error('Exception occurred while ' - 'introspecting {0}: {1}'.format(fun, exc)) + 'introspecting {0}: {1}'.format(fun, exc)) data['return'] = 'Exception occurred in wheel {0}: {1}: {2}'.format( fun, exc.__class__.__name__, @@ -1167,11 +1160,12 @@ class LocalFuncs(object): # Retrieve the minions list delimiter = load.get('kwargs', {}).get('delimiter', DEFAULT_TARGET_DELIM) - minions = self.ckminions.check_minions( + _res = self.ckminions.check_minions( load['tgt'], load.get('tgt_type', 'glob'), delimiter ) + minions = _res['minions'] # Check for external auth calls if extra.get('token', False): @@ -1181,12 +1175,7 @@ class LocalFuncs(object): return '' # Get acl from eauth module. - if self.opts['keep_acl_in_token'] and 'auth_list' in token: - auth_list = token['auth_list'] - else: - extra['eauth'] = token['eauth'] - extra['username'] = token['name'] - auth_list = self.loadauth.get_auth_list(extra) + auth_list = self.loadauth.get_auth_list(extra, token) # Authorize the request if not self.ckminions.auth_check( @@ -1383,3 +1372,18 @@ class LocalFuncs(object): }, 'pub': pub_load } + + def _prep_auth_info(self, load): + key = None + if 'token' in load: + auth_type = 'token' + err_name = 'TokenAuthenticationError' + elif 'eauth' in load: + auth_type = 'eauth' + err_name = 'EauthAuthenticationError' + else: + auth_type = 'user' + err_name = 'UserAuthenticationError' + key = self.key + + return auth_type, err_name, key diff --git a/salt/engines/slack.py b/salt/engines/slack.py index c3da20f683..9f0e1638bc 100644 --- a/salt/engines/slack.py +++ b/salt/engines/slack.py @@ -18,7 +18,7 @@ the saltmaster's minion pillar. .. versionadded: 2016.3.0 -:configuration: Example configuration using only a "default" group. The default group is not special. +:configuration: Example configuration using only a 'default' group. The default group is not special. In addition, other groups are being loaded from pillars. .. code-block:: yaml @@ -28,7 +28,7 @@ In addition, other groups are being loaded from pillars. token: 'xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx' control: True fire_all: False - groups_pillar_name: "slack_engine:groups_pillar" + groups_pillar_name: 'slack_engine:groups_pillar' groups: default: users: @@ -54,7 +54,7 @@ In addition, other groups are being loaded from pillars. target: saltmaster tgt_type: list -:configuration: Example configuration using the "default" group and a non-default group and a pillar that will be merged in +:configuration: Example configuration using the 'default' group and a non-default group and a pillar that will be merged in If the user is '*' (without the quotes) then the group's users or commands will match all users as appropriate .. versionadded: 2017.7.0 @@ -68,7 +68,7 @@ In addition, other groups are being loaded from pillars. control: True fire_all: True tag: salt/engines/slack - groups_pillar_name: "slack_engine:groups_pillar" + groups_pillar_name: 'slack_engine:groups_pillar' groups: default: valid_users: @@ -92,6 +92,8 @@ In addition, other groups are being loaded from pillars. # Import python libraries from __future__ import absolute_import +import ast +import datetime import json import itertools import logging @@ -129,605 +131,628 @@ def __virtual__(): return __virtualname__ -def get_slack_users(token): - ''' - Get all users from Slack - ''' +class SlackClient(object): + def __init__(self, token): + self.master_minion = salt.minion.MasterMinion(__opts__) - ret = salt.utils.slack.query(function='users', - api_key=token, - opts=__opts__) - users = {} - if 'message' in ret: - for item in ret['message']: - if 'is_bot' in item: - if not item['is_bot']: - users[item['name']] = item['id'] - users[item['id']] = item['name'] - return users + self.sc = slackclient.SlackClient(token) + self.slack_connect = self.sc.rtm_connect() + def get_slack_users(self, token): + ''' + Get all users from Slack + ''' -def get_slack_channels(token): - ''' - Get all channel names from Slack - ''' + ret = salt.utils.slack.query(function='users', + api_key=token, + opts=__opts__) + users = {} + if 'message' in ret: + for item in ret['message']: + if 'is_bot' in item: + if not item['is_bot']: + users[item['name']] = item['id'] + users[item['id']] = item['name'] + return users - ret = salt.utils.slack.query( - function='rooms', - api_key=token, - # These won't be honored until https://github.com/saltstack/salt/pull/41187/files is merged - opts={ - 'exclude_archived': True, - 'exclude_members': True - }) - channels = {} - if 'message' in ret: - for item in ret['message']: - channels[item["id"]] = item["name"] - return channels + def get_slack_channels(self, token): + ''' + Get all channel names from Slack + ''' + ret = salt.utils.slack.query( + function='rooms', + api_key=token, + # These won't be honored until https://github.com/saltstack/salt/pull/41187/files is merged + opts={ + 'exclude_archived': True, + 'exclude_members': True + }) + channels = {} + if 'message' in ret: + for item in ret['message']: + channels[item['id']] = item['name'] + return channels -def get_config_groups(groups_conf, groups_pillar_name): - """ - get info from groups in config, and from the named pillar + def get_config_groups(self, groups_conf, groups_pillar_name): + ''' + get info from groups in config, and from the named pillar - todo: add specification for the minion to use to recover pillar - """ - # Get groups - # Default to returning something that'll never match - ret_groups = { - "default": { - "users": set(), - "commands": set(), - "aliases": dict(), - "default_target": dict(), - "targets": dict() + todo: add specification for the minion to use to recover pillar + ''' + # Get groups + # Default to returning something that'll never match + ret_groups = { + 'default': { + 'users': set(), + 'commands': set(), + 'aliases': dict(), + 'default_target': dict(), + 'targets': dict() + } } - } - # allow for empty groups in the config file, and instead let some/all of this come - # from pillar data. - if not groups_conf: - use_groups = {} - else: - use_groups = groups_conf - # First obtain group lists from pillars, then in case there is any overlap, iterate over the groups - # that come from pillars. The configuration in files on disk/from startup - # will override any configs from pillars. They are meant to be complementary not to provide overrides. - try: - groups_gen = itertools.chain(_groups_from_pillar(groups_pillar_name).items(), use_groups.items()) - except AttributeError: - log.warn("Failed to get groups from {}: {}".format(groups_pillar_name, _groups_from_pillar(groups_pillar_name))) - log.warn("or from config: {}".format(use_groups)) - groups_gen = [] - for name, config in groups_gen: - log.info("Trying to get {} and {} to be useful".format(name, config)) - ret_groups.setdefault(name, { - "users": set(), "commands": set(), "aliases": dict(), "default_target": dict(), "targets": dict() - }) - try: - ret_groups[name]['users'].update(set(config.get('users', []))) - ret_groups[name]['commands'].update(set(config.get('commands', []))) - ret_groups[name]['aliases'].update(config.get('aliases', {})) - ret_groups[name]['default_target'].update(config.get('default_target', {})) - ret_groups[name]['targets'].update(config.get('targets', {})) - except IndexError: - log.warn("Couldn't use group {}. Check that targets is a dict and not a list".format(name)) - - log.debug("Got the groups: {}".format(ret_groups)) - return ret_groups - - -def _groups_from_pillar(pillar_name): - """pillar_prefix is the pillar.get syntax for the pillar to be queried. - Group name is gotten via the equivalent of using - ``salt['pillar.get']('{}:{}'.format(pillar_prefix, group_name))`` - in a jinja template. - - returns a dictionary (unless the pillar is mis-formatted) - XXX: instead of using Caller, make the minion to use configurable so there could be some - restrictions placed on what pillars can be used. - """ - caller = salt.client.Caller() - pillar_groups = caller.cmd('pillar.get', pillar_name) - # pillar_groups = __salt__['pillar.get'](pillar_name, {}) - log.info("Got pillar groups {} from pillar {}".format(pillar_groups, pillar_name)) - log.info("pillar groups type is {}".format(type(pillar_groups))) - return pillar_groups - - -def fire(tag, msg): - """ - This replaces a function in main called "fire" - - It fires an event into the salt bus. - """ - if __opts__.get('__role') == 'master': - fire_master = salt.utils.event.get_master_event( - __opts__, - __opts__['sock_dir']).fire_event - else: - fire_master = None - - if fire_master: - fire_master(msg, tag) - else: - __salt__['event.send'](tag, msg) - - -def can_user_run(user, command, groups): - """ - Break out the permissions into the folowing: - - Check whether a user is in any group, including whether a group has the '*' membership - - :type user: str - :param user: The username being checked against - - :type command: str - :param command: The command that is being invoked (e.g. test.ping) - - :type groups: dict - :param groups: the dictionary with groups permissions structure. - - :rtype: tuple - :returns: On a successful permitting match, returns 2-element tuple that contains - the name of the group that successfuly matched, and a dictionary containing - the configuration of the group so it can be referenced. - - On failure it returns an empty tuple - - """ - log.info("{} wants to run {} with groups {}".format(user, command, groups)) - for key, val in groups.items(): - if user not in val['users']: - if '*' not in val['users']: - continue # this doesn't grant permissions, pass - if (command not in val['commands']) and (command not in val.get('aliases', {}).keys()): - if '*' not in val['commands']: - continue # again, pass - log.info("Slack user {} permitted to run {}".format(user, command)) - return (key, val,) # matched this group, return the group - log.info("Slack user {} denied trying to run {}".format(user, command)) - return () - - -def commandline_to_list(cmdline_str, trigger_string): - """ - cmdline_str is the string of the command line - trigger_string is the trigger string, to be removed - """ - cmdline = salt.utils.args.shlex_split(cmdline_str[len(trigger_string):]) - # Remove slack url parsing - # Translate target= - # to target=host.domain.net - cmdlist = [] - for cmditem in cmdline: - pattern = r'(?P.*)(<.*\|)(?P.*)(>)(?P.*)' - mtch = re.match(pattern, cmditem) - if mtch: - origtext = mtch.group('begin') + mtch.group('url') + mtch.group('remainder') - cmdlist.append(origtext) + # allow for empty groups in the config file, and instead let some/all of this come + # from pillar data. + if not groups_conf: + use_groups = {} else: - cmdlist.append(cmditem) - return cmdlist + use_groups = groups_conf + # First obtain group lists from pillars, then in case there is any overlap, iterate over the groups + # that come from pillars. The configuration in files on disk/from startup + # will override any configs from pillars. They are meant to be complementary not to provide overrides. + log.debug('use_groups {}'.format(use_groups)) + try: + groups_gen = itertools.chain(self._groups_from_pillar(groups_pillar_name).items(), use_groups.items()) + except AttributeError: + log.warn('Failed to get groups from {}: {}'.format(groups_pillar_name, self._groups_from_pillar(groups_pillar_name))) + log.warn('or from config: {}'.format(use_groups)) + groups_gen = [] + for name, config in groups_gen: + log.info('Trying to get {} and {} to be useful'.format(name, config)) + ret_groups.setdefault(name, { + 'users': set(), 'commands': set(), 'aliases': dict(), 'default_target': dict(), 'targets': dict() + }) + try: + ret_groups[name]['users'].update(set(config.get('users', []))) + ret_groups[name]['commands'].update(set(config.get('commands', []))) + ret_groups[name]['aliases'].update(config.get('aliases', {})) + ret_groups[name]['default_target'].update(config.get('default_target', {})) + ret_groups[name]['targets'].update(config.get('targets', {})) + except IndexError: + log.warn("Couldn't use group {}. Check that targets is a dict and not a list".format(name)) + log.debug('Got the groups: {}'.format(ret_groups)) + return ret_groups + + def _groups_from_pillar(self, pillar_name): + ''' + pillar_prefix is the pillar.get syntax for the pillar to be queried. + Group name is gotten via the equivalent of using + ``salt['pillar.get']('{}:{}'.format(pillar_prefix, group_name))`` + in a jinja template. + + returns a dictionary (unless the pillar is mis-formatted) + XXX: instead of using Caller, make the minion to use configurable so there could be some + restrictions placed on what pillars can be used. + ''' + caller = salt.client.Caller() + pillar_groups = caller.cmd('pillar.get', pillar_name) + # pillar_groups = __salt__['pillar.get'](pillar_name, {}) + log.debug('Got pillar groups %s from pillar %s', pillar_groups, pillar_name) + log.debug('pillar groups is %s', pillar_groups) + log.debug('pillar groups type is %s', type(pillar_groups)) + if pillar_groups: + return pillar_groups + else: + return {} + + def fire(self, tag, msg): + ''' + This replaces a function in main called 'fire' + + It fires an event into the salt bus. + ''' + if __opts__.get('__role') == 'master': + fire_master = salt.utils.event.get_master_event( + __opts__, + __opts__['sock_dir']).fire_master + else: + fire_master = None + + if fire_master: + fire_master(msg, tag) + else: + __salt__['event.send'](tag, msg) + + def can_user_run(self, user, command, groups): + ''' + Break out the permissions into the folowing: + + Check whether a user is in any group, including whether a group has the '*' membership + + :type user: str + :param user: The username being checked against + + :type command: str + :param command: The command that is being invoked (e.g. test.ping) + + :type groups: dict + :param groups: the dictionary with groups permissions structure. + + :rtype: tuple + :returns: On a successful permitting match, returns 2-element tuple that contains + the name of the group that successfuly matched, and a dictionary containing + the configuration of the group so it can be referenced. + + On failure it returns an empty tuple + + ''' + log.info('{} wants to run {} with groups {}'.format(user, command, groups)) + for key, val in groups.items(): + if user not in val['users']: + if '*' not in val['users']: + continue # this doesn't grant permissions, pass + if (command not in val['commands']) and (command not in val.get('aliases', {}).keys()): + if '*' not in val['commands']: + continue # again, pass + log.info('Slack user {} permitted to run {}'.format(user, command)) + return (key, val,) # matched this group, return the group + log.info('Slack user {} denied trying to run {}'.format(user, command)) + return () + + def commandline_to_list(self, cmdline_str, trigger_string): + ''' + cmdline_str is the string of the command line + trigger_string is the trigger string, to be removed + ''' + cmdline = salt.utils.args.shlex_split(cmdline_str[len(trigger_string):]) + # Remove slack url parsing + # Translate target= + # to target=host.domain.net + cmdlist = [] + for cmditem in cmdline: + pattern = r'(?P.*)(<.*\|)(?P.*)(>)(?P.*)' + mtch = re.match(pattern, cmditem) + if mtch: + origtext = mtch.group('begin') + mtch.group('url') + mtch.group('remainder') + cmdlist.append(origtext) + else: + cmdlist.append(cmditem) + return cmdlist # m_data -> m_data, _text -> test, all_slack_users -> all_slack_users, -def control_message_target(slack_user_name, text, loaded_groups, trigger_string): - """Returns a tuple of (target, cmdline,) for the response + def control_message_target(self, slack_user_name, text, loaded_groups, trigger_string): + '''Returns a tuple of (target, cmdline,) for the response - Raises IndexError if a user can't be looked up from all_slack_users + Raises IndexError if a user can't be looked up from all_slack_users - Returns (False, False) if the user doesn't have permission + Returns (False, False) if the user doesn't have permission - These are returned together because the commandline and the targeting - interact with the group config (specifically aliases and targeting configuration) - so taking care of them together works out. + These are returned together because the commandline and the targeting + interact with the group config (specifically aliases and targeting configuration) + so taking care of them together works out. - The cmdline that is returned is the actual list that should be - processed by salt, and not the alias. + The cmdline that is returned is the actual list that should be + processed by salt, and not the alias. - """ + ''' - # Trim the trigger string from the front - # cmdline = _text[1:].split(' ', 1) - cmdline = commandline_to_list(text, trigger_string) - permitted_group = can_user_run(slack_user_name, cmdline[0], loaded_groups) - log.debug("slack_user_name is {} and the permitted group is {}".format(slack_user_name, permitted_group)) - if not permitted_group: - return (False, False) - if not slack_user_name: - return (False, False) + # Trim the trigger string from the front + # cmdline = _text[1:].split(' ', 1) + cmdline = self.commandline_to_list(text, trigger_string) + permitted_group = self.can_user_run(slack_user_name, cmdline[0], loaded_groups) + log.debug('slack_user_name is {} and the permitted group is {}'.format(slack_user_name, permitted_group)) - # maybe there are aliases, so check on that - if cmdline[0] in permitted_group[1].get('aliases', {}).keys(): - use_cmdline = commandline_to_list(permitted_group[1]['aliases'][cmdline[0]], "") - else: - use_cmdline = cmdline - target = get_target(permitted_group, cmdline, use_cmdline) - return (target, use_cmdline,) + if not permitted_group: + return (False, None, cmdline[0]) + if not slack_user_name: + return (False, None, cmdline[0]) - -def message_text(m_data): - """ - Raises ValueError if a value doesn't work out, and TypeError if - this isn't a message type - """ - if m_data.get('type') != 'message': - raise TypeError("This isn't a message") - # Edited messages have text in message - _text = m_data.get('text', None) or m_data.get('message', {}).get('text', None) - try: - log.info("Message is {}".format(_text)) # this can violate the ascii codec - except UnicodeEncodeError as uee: - log.warn("Got a message that I couldn't log. The reason is: {}".format(uee)) - - # Convert UTF to string - _text = json.dumps(_text) - _text = yaml.safe_load(_text) - - if not _text: - raise ValueError("_text has no value") - return _text - - -def generate_triggered_messages(token, trigger_string, groups, groups_pillar_name): - """slack_token = string - trigger_string = string - input_valid_users = set - input_valid_commands = set - - When the trigger_string prefixes the message text, yields a dictionary of { - "message_data": m_data, - "cmdline": cmdline_list, # this is a list - "channel": channel, - "user": m_data['user'], - "slack_client": sc - } - - else yields {"message_data": m_data} and the caller can handle that - - When encountering an error (e.g. invalid message), yields {}, the caller can proceed to the next message - - When the websocket being read from has given up all its messages, yields {"done": True} to - indicate that the caller has read all of the relevent data for now, and should continue - its own processing and check back for more data later. - - This relies on the caller sleeping between checks, otherwise this could flood - """ - sc = slackclient.SlackClient(token) - slack_connect = sc.rtm_connect() - all_slack_users = get_slack_users(token) # re-checks this if we have an negative lookup result - all_slack_channels = get_slack_channels(token) # re-checks this if we have an negative lookup result - - def just_data(m_data): - """Always try to return the user and channel anyway""" - user_id = m_data.get('user') - channel_id = m_data.get('channel') - if channel_id.startswith('D'): # private chate with bot user - channel_name = "private chat" + # maybe there are aliases, so check on that + if cmdline[0] in permitted_group[1].get('aliases', {}).keys(): + use_cmdline = self.commandline_to_list(permitted_group[1]['aliases'][cmdline[0]], '') else: - channel_name = all_slack_channels.get(channel_id) - data = { - "message_data": m_data, - "user_name": all_slack_users.get(user_id), - "channel_name": channel_name + use_cmdline = cmdline + target = self.get_target(permitted_group, cmdline, use_cmdline) + + return (True, target, use_cmdline) + + def message_text(self, m_data): + ''' + Raises ValueError if a value doesn't work out, and TypeError if + this isn't a message type + ''' + if m_data.get('type') != 'message': + raise TypeError('This is not a message') + # Edited messages have text in message + _text = m_data.get('text', None) or m_data.get('message', {}).get('text', None) + try: + log.info('Message is {}'.format(_text)) # this can violate the ascii codec + except UnicodeEncodeError as uee: + log.warn('Got a message that I could not log. The reason is: {}'.format(uee)) + + # Convert UTF to string + _text = json.dumps(_text) + _text = yaml.safe_load(_text) + + if not _text: + raise ValueError('_text has no value') + return _text + + def generate_triggered_messages(self, token, trigger_string, groups, groups_pillar_name): + ''' + slack_token = string + trigger_string = string + input_valid_users = set + input_valid_commands = set + + When the trigger_string prefixes the message text, yields a dictionary of { + 'message_data': m_data, + 'cmdline': cmdline_list, # this is a list + 'channel': channel, + 'user': m_data['user'], + 'slack_client': sc } - if not data["user_name"]: - all_slack_users.clear() - all_slack_users.update(get_slack_users(token)) - data["user_name"] = all_slack_users.get(user_id) - if not data["channel_name"]: - all_slack_channels.clear() - all_slack_channels.update(get_slack_channels(token)) - data["channel_name"] = all_slack_channels.get(channel_id) - return data - for sleeps in (5, 10, 30, 60): - if slack_connect: - break + else yields {'message_data': m_data} and the caller can handle that + + When encountering an error (e.g. invalid message), yields {}, the caller can proceed to the next message + + When the websocket being read from has given up all its messages, yields {'done': True} to + indicate that the caller has read all of the relevent data for now, and should continue + its own processing and check back for more data later. + + This relies on the caller sleeping between checks, otherwise this could flood + ''' + all_slack_users = self.get_slack_users(token) # re-checks this if we have an negative lookup result + all_slack_channels = self.get_slack_channels(token) # re-checks this if we have an negative lookup result + + def just_data(m_data): + '''Always try to return the user and channel anyway''' + if 'user' not in m_data: + if 'message' in m_data and 'user' in m_data['message']: + log.debug('Message was edited, ' + 'so we look for user in ' + 'the original message.') + user_id = m_data['message']['user'] + else: + user_id = m_data.get('user') + channel_id = m_data.get('channel') + if channel_id.startswith('D'): # private chate with bot user + channel_name = 'private chat' + else: + channel_name = all_slack_channels.get(channel_id) + data = { + 'message_data': m_data, + 'user_id': user_id, + 'user_name': all_slack_users.get(user_id), + 'channel_name': channel_name + } + if not data['user_name']: + all_slack_users.clear() + all_slack_users.update(self.get_slack_users(token)) + data['user_name'] = all_slack_users.get(user_id) + if not data['channel_name']: + all_slack_channels.clear() + all_slack_channels.update(self.get_slack_channels(token)) + data['channel_name'] = all_slack_channels.get(channel_id) + return data + + for sleeps in (5, 10, 30, 60): + if self.slack_connect: + break + else: + # see https://api.slack.com/docs/rate-limits + log.warning('Slack connection is invalid. Server: {}, sleeping {}'.format(self.sc.server, sleeps)) + time.sleep(sleeps) # respawning too fast makes the slack API unhappy about the next reconnection else: - # see https://api.slack.com/docs/rate-limits - log.warning("Slack connection is invalid. Server: {}, sleeping {}".format(sc.server, sleeps)) - time.sleep(sleeps) # respawning too fast makes the slack API unhappy about the next reconnection - else: - raise UserWarning("Connection to slack is still invalid, giving up: {}".format(slack_connect)) # Boom! - while True: - msg = sc.rtm_read() - for m_data in msg: - try: - msg_text = message_text(m_data) - except (ValueError, TypeError) as msg_err: - log.debug("Got an error from trying to get the message text {}".format(msg_err)) - yield {"message_data": m_data} # Not a message type from the API? - continue + raise UserWarning('Connection to slack is still invalid, giving up: {}'.format(self.slack_connect)) # Boom! + while True: + msg = self.sc.rtm_read() + for m_data in msg: + try: + msg_text = self.message_text(m_data) + except (ValueError, TypeError) as msg_err: + log.debug('Got an error from trying to get the message text {}'.format(msg_err)) + yield {'message_data': m_data} # Not a message type from the API? + continue - # Find the channel object from the channel name - channel = sc.server.channels.find(m_data['channel']) - data = just_data(m_data) - if msg_text.startswith(trigger_string): - loaded_groups = get_config_groups(groups, groups_pillar_name) - user_id = m_data.get('user') # slack user ID, e.g. 'U11011' - if not data.get('user_name'): - log.error("The user {} can't be looked up via slack. What has happened here?".format( - m_data.get('user'))) - channel.send_message("The user {} can't be looked up via slack. Not running {}".format( - user_id, msg_text)) - yield {"message_data": m_data} - continue - (target, cmdline) = control_message_target( - data['user_name'], msg_text, loaded_groups, trigger_string) - log.debug("Got target: {}, cmdline: {}".format(target, cmdline)) - if target and cmdline: - yield { - "message_data": m_data, - "slack_client": sc, - "channel": channel, - "user": user_id, - "user_name": all_slack_users[user_id], - "cmdline": cmdline, - "target": target - } - continue + # Find the channel object from the channel name + channel = self.sc.server.channels.find(m_data['channel']) + data = just_data(m_data) + if msg_text.startswith(trigger_string): + loaded_groups = self.get_config_groups(groups, groups_pillar_name) + if not data.get('user_name'): + log.error('The user {} can not be looked up via slack. What has happened here?'.format( + m_data.get('user'))) + channel.send_message('The user {} can not be looked up via slack. Not running {}'.format( + data['user_id'], msg_text)) + yield {'message_data': m_data} + continue + (allowed, target, cmdline) = self.control_message_target( + data['user_name'], msg_text, loaded_groups, trigger_string) + log.debug('Got target: {}, cmdline: {}'.format(target, cmdline)) + if allowed: + yield { + 'message_data': m_data, + 'channel': m_data['channel'], + 'user': data['user_id'], + 'user_name': data['user_name'], + 'cmdline': cmdline, + 'target': target + } + continue + else: + channel.send_message('{0} is not allowed to use command {1}.'.format( + data['user_name'], cmdline)) + yield data + continue else: - channel.send_message('{}, {} is not allowed to use command {}.'.format( - user_id, all_slack_users[user_id], cmdline)) yield data continue - else: - yield data - continue - yield {"done": True} + yield {'done': True} + def get_target(self, permitted_group, cmdline, alias_cmdline): + ''' + When we are permitted to run a command on a target, look to see + what the default targeting is for that group, and for that specific + command (if provided). -def get_target(permitted_group, cmdline, alias_cmdline): - """When we are permitted to run a command on a target, look to see - what the default targeting is for that group, and for that specific - command (if provided). + It's possible for None or False to be the result of either, which means + that it's expected that the caller provide a specific target. - It's possible for None or False to be the result of either, which means - that it's expected that the caller provide a specific target. + If no configured target is provided, the command line will be parsed + for target=foo and tgt_type=bar - If no configured target is provided, the command line will be parsed - for target=foo and tgt_type=bar + Test for this: + h = {'aliases': {}, 'commands': {'cmd.run', 'pillar.get'}, + 'default_target': {'target': '*', 'tgt_type': 'glob'}, + 'targets': {'pillar.get': {'target': 'you_momma', 'tgt_type': 'list'}}, + 'users': {'dmangot', 'jmickle', 'pcn'}} + f = {'aliases': {}, 'commands': {'cmd.run', 'pillar.get'}, + 'default_target': {}, 'targets': {},'users': {'dmangot', 'jmickle', 'pcn'}} - Test for this: - h = {'aliases': {}, 'commands': {'cmd.run', 'pillar.get'}, - 'default_target': {'target': '*', 'tgt_type': 'glob'}, - 'targets': {'pillar.get': {'target': 'you_momma', 'tgt_type': 'list'}}, - 'users': {'dmangot', 'jmickle', 'pcn'}} - f = {'aliases': {}, 'commands': {'cmd.run', 'pillar.get'}, - 'default_target': {}, 'targets': {},'users': {'dmangot', 'jmickle', 'pcn'}} + g = {'aliases': {}, 'commands': {'cmd.run', 'pillar.get'}, + 'default_target': {'target': '*', 'tgt_type': 'glob'}, + 'targets': {}, 'users': {'dmangot', 'jmickle', 'pcn'}} - g = {'aliases': {}, 'commands': {'cmd.run', 'pillar.get'}, - 'default_target': {'target': '*', 'tgt_type': 'glob'}, - 'targets': {}, 'users': {'dmangot', 'jmickle', 'pcn'}} + Run each of them through ``get_configured_target(('foo', f), 'pillar.get')`` and confirm a valid target - Run each of them through ``get_configured_target(("foo", f), "pillar.get")`` and confirm a valid target + ''' + # Default to targetting all minions with a type of glob + null_target = {'target': '*', 'tgt_type': 'glob'} - """ - null_target = {"target": None, "tgt_type": None} + def check_cmd_against_group(cmd): + ''' + Validate cmd against the group to return the target, or a null target + ''' + name, group_config = permitted_group + target = group_config.get('default_target') + if not target: # Empty, None, or False + target = null_target + if group_config.get('targets'): + if group_config['targets'].get(cmd): + target = group_config['targets'][cmd] + if not target.get('target'): + log.debug('Group {} is not configured to have a target for cmd {}.'.format(name, cmd)) + return target - def check_cmd_against_group(cmd): - """Validate cmd against the group to return the target, or a null target""" - name, group_config = permitted_group - target = group_config.get('default_target') - if not target: # Empty, None, or False - target = null_target - if group_config.get('targets'): - if group_config['targets'].get(cmd): - target = group_config['targets'][cmd] - if not target.get("target"): - log.debug("Group {} is not configured to have a target for cmd {}.".format(name, cmd)) - return target + for this_cl in cmdline, alias_cmdline: + _, kwargs = self.parse_args_and_kwargs(this_cl) + if 'target' in kwargs: + log.debug('target is in kwargs {}.'.format(kwargs)) + if 'tgt_type' in kwargs: + log.debug('tgt_type is in kwargs {}.'.format(kwargs)) + return {'target': kwargs['target'], 'tgt_type': kwargs['tgt_type']} + return {'target': kwargs['target'], 'tgt_type': 'glob'} - for this_cl in cmdline, alias_cmdline: - _, kwargs = parse_args_and_kwargs(this_cl) - if 'target' in kwargs: - log.debug("target is in kwargs {}.".format(kwargs)) - if 'tgt_type' in kwargs: - log.debug("tgt_type is in kwargs {}.".format(kwargs)) - return {"target": kwargs['target'], "tgt_type": kwargs['tgt_type']} - return {"target": kwargs['target'], "tgt_type": 'glob'} - - for this_cl in cmdline, alias_cmdline: - checked = check_cmd_against_group(this_cl[0]) - log.debug("this cmdline has target {}.".format(this_cl)) - if checked.get("target"): - return checked - return null_target + for this_cl in cmdline, alias_cmdline: + checked = check_cmd_against_group(this_cl[0]) + log.debug('this cmdline has target {}.'.format(this_cl)) + if checked.get('target'): + return checked + return null_target # emulate the yaml_out output formatter. It relies on a global __opts__ object which we can't # obviously pass in -def format_return_text(data, **kwargs): # pylint: disable=unused-argument - ''' - Print out YAML using the block mode - ''' - params = dict(Dumper=OrderedDumper) - if 'output_indent' not in __opts__: - # default indentation - params.update(default_flow_style=False) - elif __opts__['output_indent'] >= 0: - # custom indent - params.update(default_flow_style=False, - indent=__opts__['output_indent']) - else: # no indentation - params.update(default_flow_style=True, - indent=0) - try: - return yaml.dump(data, **params).replace("\n\n", "\n") - # pylint: disable=broad-except - except Exception as exc: - import pprint - log.exception('Exception {0} encountered when trying to serialize {1}'.format( - exc, pprint.pformat(data))) - return "Got an error trying to serialze/clean up the response" + def format_return_text(self, data, **kwargs): # pylint: disable=unused-argument + ''' + Print out YAML using the block mode + ''' + params = dict(Dumper=OrderedDumper) + if 'output_indent' not in __opts__: + # default indentation + params.update(default_flow_style=False) + elif __opts__['output_indent'] >= 0: + # custom indent + params.update(default_flow_style=False, + indent=__opts__['output_indent']) + else: # no indentation + params.update(default_flow_style=True, + indent=0) + try: + #return yaml.dump(data, **params).replace("\n\n", "\n") + return json.dumps(data, sort_keys=True, indent=1) + # pylint: disable=broad-except + except Exception as exc: + import pprint + log.exception('Exception {0} encountered when trying to serialize {1}'.format( + exc, pprint.pformat(data))) + return 'Got an error trying to serialze/clean up the response' + def parse_args_and_kwargs(self, cmdline): + ''' + cmdline: list -def parse_args_and_kwargs(cmdline): - """ - cmdline: list + returns tuple of: args (list), kwargs (dict) + ''' + # Parse args and kwargs + args = [] + kwargs = {} - returns tuple of: args (list), kwargs (dict) - """ - # Parse args and kwargs - args = [] - kwargs = {} + if len(cmdline) > 1: + for item in cmdline[1:]: + if '=' in item: + (key, value) = item.split('=', 1) + kwargs[key] = value + else: + args.append(item) + return (args, kwargs) - if len(cmdline) > 1: - for item in cmdline[1:]: - if '=' in item: - (key, value) = item.split('=', 1) - kwargs[key] = value - else: - args.append(item) - return (args, kwargs) + def get_jobs_from_runner(self, outstanding_jids): + ''' + Given a list of job_ids, return a dictionary of those job_ids that have completed and their results. + Query the salt event bus via the jobs runner. jobs.list_job will show a job in progress, + jobs.lookup_jid will return a job that has completed. -def get_jobs_from_runner(outstanding_jids): - """ - Given a list of job_ids, return a dictionary of those job_ids that have completed and their results. + returns a dictionary of job id: result + ''' + # Can't use the runner because of https://github.com/saltstack/salt/issues/40671 + runner = salt.runner.RunnerClient(__opts__) + # log.debug("Getting job IDs {} will run via runner jobs.lookup_jid".format(outstanding_jids)) + #mm = salt.minion.MasterMinion(__opts__) + source = __opts__.get('ext_job_cache') + if not source: + source = __opts__.get('master_job_cache') - Query the salt event bus via the jobs runner. jobs.list_job will show a job in progress, - jobs.lookup_jid will return a job that has completed. + results = dict() + for jid in outstanding_jids: + # results[jid] = runner.cmd('jobs.lookup_jid', [jid]) + if self.master_minion.returners['{}.get_jid'.format(source)](jid): + jid_result = runner.cmd('jobs.list_job', [jid]).get('Result', {}) + # emulate lookup_jid's return, which is just minion:return + # pylint is tripping + # pylint: disable=missing-whitespace-after-comma + job_data = json.dumps({key:val['return'] for key, val in jid_result.items()}) + results[jid] = yaml.load(job_data) - returns a dictionary of job id: result - """ - # Can't use the runner because of https://github.com/saltstack/salt/issues/40671 - runner = salt.runner.RunnerClient(__opts__) - # log.debug("Getting job IDs {} will run via runner jobs.lookup_jid".format(outstanding_jids)) - mm = salt.minion.MasterMinion(__opts__) - source = __opts__.get('ext_job_cache') - if not source: - source = __opts__.get('master_job_cache') + return results - results = dict() - for jid in outstanding_jids: - # results[jid] = runner.cmd('jobs.lookup_jid', [jid]) - if mm.returners['{}.get_jid'.format(source)](jid): - jid_result = runner.cmd('jobs.list_job', [jid]).get('Result', {}) - # emulate lookup_jid's return, which is just minion:return + def run_commands_from_slack_async(self, message_generator, fire_all, tag, control, interval=1): + ''' + Pull any pending messages from the message_generator, sending each + one to either the event bus, the command_async or both, depending on + the values of fire_all and command + ''' + + outstanding = dict() # set of job_id that we need to check for + + while True: + log.trace('Sleeping for interval of {}'.format(interval)) + time.sleep(interval) + # Drain the slack messages, up to 10 messages at a clip + count = 0 + for msg in message_generator: + # The message_generator yields dicts. Leave this loop + # on a dict that looks like {'done': True} or when we've done it + # 10 times without taking a break. + log.trace('Got a message from the generator: {}'.format(msg.keys())) + if count > 10: + log.warn('Breaking in getting messages because count is exceeded') + break + if len(msg) == 0: + count += 1 + log.warn('len(msg) is zero') + continue # This one is a dud, get the next message + if msg.get('done'): + log.trace('msg is done') + break + if fire_all: + log.debug('Firing message to the bus with tag: {}'.format(tag)) + log.debug('{} {}'.format(tag, msg)) + self.fire('{0}/{1}'.format(tag, msg['message_data'].get('type')), msg) + if control and (len(msg) > 1) and msg.get('cmdline'): + channel = self.sc.server.channels.find(msg['channel']) + jid = self.run_command_async(msg) + log.debug('Submitted a job and got jid: {}'.format(jid)) + outstanding[jid] = msg # record so we can return messages to the caller + channel.send_message("@{}'s job is submitted as salt jid {}".format(msg['user_name'], jid)) + count += 1 + start_time = time.time() + job_status = self.get_jobs_from_runner(outstanding.keys()) # dict of job_ids:results are returned + log.trace('Getting {} jobs status took {} seconds'.format(len(job_status), time.time() - start_time)) + for jid, result in job_status.items(): + if result: + log.debug('ret to send back is {}'.format(result)) + # formatting function? + this_job = outstanding[jid] + channel = self.sc.server.channels.find(this_job['channel']) + return_text = self.format_return_text(result) + return_prefix = "@{}'s job `{}` (id: {}) (target: {}) returned".format( + this_job['user_name'], this_job['cmdline'], jid, this_job['target']) + channel.send_message(return_prefix) + ts = time.time() + st = datetime.datetime.fromtimestamp(ts).strftime('%Y%m%d%H%M%S%f') + filename = 'salt-results-{0}.yaml'.format(st) + r = self.sc.api_call( + 'files.upload', channels=channel.id, filename=filename, + content=return_text) + # Handle unicode return + log.debug('Got back {} via the slack client'.format(r)) + resp = yaml.safe_load(json.dumps(r)) + if 'ok' in resp and resp['ok'] is False: + this_job['channel'].send_message('Error: {0}'.format(resp['error'])) + del outstanding[jid] + + def run_command_async(self, msg): + + ''' + :type message_generator: generator of dict + :param message_generator: Generates messages from slack that should be run + + :type fire_all: bool + :param fire_all: Whether to also fire messages to the event bus + + :type tag: str + :param tag: The tag to send to use to send to the event bus + + :type interval: int + :param interval: time to wait between ending a loop and beginning the next + + ''' + log.debug('Going to run a command async') + runner_functions = sorted(salt.runner.Runner(__opts__).functions) + # Parse args and kwargs + cmd = msg['cmdline'][0] + + args, kwargs = self.parse_args_and_kwargs(msg['cmdline']) + + # Check for pillar string representation of dict and convert it to dict + if 'pillar' in kwargs: + kwargs.update(pillar=ast.literal_eval(kwargs['pillar'])) + + # Check for target. Otherwise assume None + target = msg['target']['target'] + # Check for tgt_type. Otherwise assume glob + tgt_type = msg['target']['tgt_type'] + log.debug('target_type is: {}'.format(tgt_type)) + + if cmd in runner_functions: + runner = salt.runner.RunnerClient(__opts__) + log.debug('Command {} will run via runner_functions'.format(cmd)) # pylint is tripping # pylint: disable=missing-whitespace-after-comma - job_data = json.dumps({key:val['return'] for key, val in jid_result.items()}) - results[jid] = yaml.load(job_data) + job_id_dict = runner.async(cmd, {'args': args, 'kwargs': kwargs}) + job_id = job_id_dict['jid'] - return results - - -def run_commands_from_slack_async(message_generator, fire_all, tag, control, interval=1): - """Pull any pending messages from the message_generator, sending each - one to either the event bus, the command_async or both, depending on - the values of fire_all and command - """ - - outstanding = dict() # set of job_id that we need to check for - - while True: - log.debug("Sleeping for interval of {}".format(interval)) - time.sleep(interval) - # Drain the slack messages, up to 10 messages at a clip - count = 0 - for msg in message_generator: - # The message_generator yields dicts. Leave this loop - # on a dict that looks like {"done": True} or when we've done it - # 10 times without taking a break. - log.debug("Got a message from the generator: {}".format(msg.keys())) - if count > 10: - log.warn("Breaking in getting messages because count is exceeded") - break - if len(msg) == 0: - count += 1 - log.warn("len(msg) is zero") - continue # This one is a dud, get the next message - if msg.get("done"): - log.debug("msg is done") - break - if fire_all: - log.debug("Firing message to the bus with tag: {}".format(tag)) - fire('{0}/{1}'.format(tag, msg['message_data'].get('type')), msg) - if control and (len(msg) > 1) and msg.get('cmdline'): - jid = run_command_async(msg) - log.debug("Submitted a job and got jid: {}".format(jid)) - outstanding[jid] = msg # record so we can return messages to the caller - msg['channel'].send_message("@{}'s job is submitted as salt jid {}".format(msg['user_name'], jid)) - count += 1 - start_time = time.time() - job_status = get_jobs_from_runner(outstanding.keys()) # dict of job_ids:results are returned - log.debug("Getting {} jobs status took {} seconds".format(len(job_status), time.time() - start_time)) - for jid, result in job_status.items(): - if result: - log.debug("ret to send back is {}".format(result)) - # formatting function? - this_job = outstanding[jid] - return_text = format_return_text(result) - return_prefix = "@{}'s job `{}` (id: {}) (target: {}) returned".format( - this_job["user_name"], this_job["cmdline"], jid, this_job["target"]) - this_job['channel'].send_message(return_prefix) - r = this_job["slack_client"].api_call( - "files.upload", channels=this_job['channel'].id, files=None, - content=return_text) - # Handle unicode return - log.debug("Got back {} via the slack client".format(r)) - resp = yaml.safe_load(json.dumps(r)) - if 'ok' in resp and resp['ok'] is False: - this_job['channel'].send_message('Error: {0}'.format(resp['error'])) - del outstanding[jid] - - -def run_command_async(msg): - - """ - :type message_generator: generator of dict - :param message_generator: Generates messages from slack that should be run - - :type fire_all: bool - :param fire_all: Whether to also fire messages to the event bus - - :type tag: str - :param tag: The tag to send to use to send to the event bus - - :type interval: int - :param interval: time to wait between ending a loop and beginning the next - - """ - log.debug("Going to run a command async") - runner_functions = sorted(salt.runner.Runner(__opts__).functions) - # Parse args and kwargs - cmd = msg['cmdline'][0] - - args, kwargs = parse_args_and_kwargs(msg['cmdline']) - # Check for target. Otherwise assume None - target = msg["target"]["target"] - # Check for tgt_type. Otherwise assume glob - tgt_type = msg["target"]['tgt_type'] - log.debug("target_type is: {}".format(tgt_type)) - - if cmd in runner_functions: - runner = salt.runner.RunnerClient(__opts__) - log.debug("Command {} will run via runner_functions".format(cmd)) - # pylint is tripping - # pylint: disable=missing-whitespace-after-comma - job_id_dict = runner.async(cmd, {"args": args, "kwargs": kwargs}) - job_id = job_id_dict['jid'] - - # Default to trying to run as a client module. - else: - local = salt.client.LocalClient() - log.debug("Command {} will run via local.cmd_async, targeting {}".format(cmd, target)) - log.debug("Running {}, {}, {}, {}, {}".format(str(target), cmd, args, kwargs, str(tgt_type))) - # according to https://github.com/saltstack/salt-api/issues/164, tgt_type has changed to expr_form - job_id = local.cmd_async(str(target), cmd, arg=args, kwargs=kwargs, tgt_type=str(tgt_type)) - log.info("ret from local.cmd_async is {}".format(job_id)) - return job_id + # Default to trying to run as a client module. + else: + local = salt.client.LocalClient() + log.debug('Command {} will run via local.cmd_async, targeting {}'.format(cmd, target)) + log.debug('Running {}, {}, {}, {}, {}'.format(str(target), cmd, args, kwargs, str(tgt_type))) + # according to https://github.com/saltstack/salt-api/issues/164, tgt_type has changed to expr_form + job_id = local.cmd_async(str(target), cmd, arg=args, kwarg=kwargs, tgt_type=str(tgt_type)) + log.info('ret from local.cmd_async is {}'.format(job_id)) + return job_id def start(token, control=False, - trigger="!", + trigger='!', groups=None, groups_pillar_name=None, fire_all=False, @@ -738,11 +763,12 @@ def start(token, if (not token) or (not token.startswith('xoxb')): time.sleep(2) # don't respawn too quickly - log.error("Slack bot token not found, bailing...") + log.error('Slack bot token not found, bailing...') raise UserWarning('Slack Engine bot token not configured') try: - message_generator = generate_triggered_messages(token, trigger, groups, groups_pillar_name) - run_commands_from_slack_async(message_generator, fire_all, tag, control) + client = SlackClient(token=token) + message_generator = client.generate_triggered_messages(token, trigger, groups, groups_pillar_name) + client.run_commands_from_slack_async(message_generator, fire_all, tag, control) except Exception: - raise Exception("{}".format(traceback.format_exc())) + raise Exception('{}'.format(traceback.format_exc())) diff --git a/salt/exceptions.py b/salt/exceptions.py index 7df94e17bb..1a253dff04 100644 --- a/salt/exceptions.py +++ b/salt/exceptions.py @@ -266,6 +266,12 @@ class SaltCacheError(SaltException): ''' +class TimeoutError(SaltException): + ''' + Thrown when an opration cannot be completet within a given time limit. + ''' + + class SaltReqTimeoutError(SaltException): ''' Thrown when a salt master request call fails to return within the timeout @@ -393,7 +399,19 @@ class TemplateError(SaltException): # Validation related exceptions class InvalidConfigError(CommandExecutionError): ''' - Used when the input is invalid + Used when the config is invalid + ''' + + +class ArgumentValueError(CommandExecutionError): + ''' + Used when an invalid argument was passed to a command execution + ''' + + +class InvalidEntityError(CommandExecutionError): + ''' + Used when an entity fails validation ''' diff --git a/salt/ext/vsan/__init__.py b/salt/ext/vsan/__init__.py new file mode 100644 index 0000000000..cf17f9d75e --- /dev/null +++ b/salt/ext/vsan/__init__.py @@ -0,0 +1,7 @@ +# coding: utf-8 -*- +''' +This directory contains the object model and utils for the vsan VMware SDK +extension. + +They are governed under their respective licenses. +''' diff --git a/salt/ext/vsan/vsanapiutils.py b/salt/ext/vsan/vsanapiutils.py new file mode 100644 index 0000000000..bb7ef05556 --- /dev/null +++ b/salt/ext/vsan/vsanapiutils.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Copyright 2016 VMware, Inc. All rights reserved. + +This module defines basic helper functions used in the sampe codes +""" + +# pylint: skip-file +__author__ = 'VMware, Inc' + +from pyVmomi import vim, vmodl, SoapStubAdapter +#import the VSAN API python bindings +import vsanmgmtObjects + +VSAN_API_VC_SERVICE_ENDPOINT = '/vsanHealth' +VSAN_API_ESXI_SERVICE_ENDPOINT = '/vsan' + +#Constuct a stub for VSAN API access using VC or ESXi sessions from existing +#stubs. Correspoding VC or ESXi service endpoint is required. VC service +#endpoint is used as default +def _GetVsanStub( + stub, endpoint=VSAN_API_VC_SERVICE_ENDPOINT, + context=None, version='vim.version.version10' + ): + + hostname = stub.host.split(':')[0] + vsanStub = SoapStubAdapter( + host=hostname, + path=endpoint, + version=version, + sslContext=context + ) + vsanStub.cookie = stub.cookie + return vsanStub + +#Construct a stub for access VC side VSAN APIs +def GetVsanVcStub(stub, context=None): + return _GetVsanStub(stub, endpoint=VSAN_API_VC_SERVICE_ENDPOINT, + context=context) + +#Construct a stub for access ESXi side VSAN APIs +def GetVsanEsxStub(stub, context=None): + return _GetVsanStub(stub, endpoint=VSAN_API_ESXI_SERVICE_ENDPOINT, + context=context) + +#Construct a stub for access ESXi side VSAN APIs +def GetVsanVcMos(vcStub, context=None): + vsanStub = GetVsanVcStub(vcStub, context) + vcMos = { + 'vsan-disk-management-system' : vim.cluster.VsanVcDiskManagementSystem( + 'vsan-disk-management-system', + vsanStub + ), + 'vsan-stretched-cluster-system' : vim.cluster.VsanVcStretchedClusterSystem( + 'vsan-stretched-cluster-system', + vsanStub + ), + 'vsan-cluster-config-system' : vim.cluster.VsanVcClusterConfigSystem( + 'vsan-cluster-config-system', + vsanStub + ), + 'vsan-performance-manager' : vim.cluster.VsanPerformanceManager( + 'vsan-performance-manager', + vsanStub + ), + 'vsan-cluster-health-system' : vim.cluster.VsanVcClusterHealthSystem( + 'vsan-cluster-health-system', + vsanStub + ), + 'vsan-upgrade-systemex' : vim.VsanUpgradeSystemEx( + 'vsan-upgrade-systemex', + vsanStub + ), + 'vsan-cluster-space-report-system' : vim.cluster.VsanSpaceReportSystem( + 'vsan-cluster-space-report-system', + vsanStub + ), + + 'vsan-cluster-object-system' : vim.cluster.VsanObjectSystem( + 'vsan-cluster-object-system', + vsanStub + ), + } + + return vcMos + +#Construct a stub for access ESXi side VSAN APIs +def GetVsanEsxMos(esxStub, context=None): + vsanStub = GetVsanEsxStub(esxStub, context) + esxMos = { + 'vsan-performance-manager' : vim.cluster.VsanPerformanceManager( + 'vsan-performance-manager', + vsanStub + ), + 'ha-vsan-health-system' : vim.host.VsanHealthSystem( + 'ha-vsan-health-system', + vsanStub + ), + 'vsan-object-system' : vim.cluster.VsanObjectSystem( + 'vsan-object-system', + vsanStub + ), + } + + return esxMos + +#Convert a VSAN Task to a Task MO binding to VC service +#@param vsanTask the VSAN Task MO +#@param stub the stub for the VC API +def ConvertVsanTaskToVcTask(vsanTask, vcStub): + vcTask = vim.Task(vsanTask._moId, vcStub) + return vcTask + +def WaitForTasks(tasks, si): + """ + Given the service instance si and tasks, it returns after all the + tasks are complete + """ + + pc = si.content.propertyCollector + + taskList = [str(task) for task in tasks] + + # Create filter + objSpecs = [vmodl.query.PropertyCollector.ObjectSpec(obj=task) + for task in tasks] + propSpec = vmodl.query.PropertyCollector.PropertySpec(type=vim.Task, + pathSet=[], all=True) + filterSpec = vmodl.query.PropertyCollector.FilterSpec() + filterSpec.objectSet = objSpecs + filterSpec.propSet = [propSpec] + filter = pc.CreateFilter(filterSpec, True) + + try: + version, state = None, None + + # Loop looking for updates till the state moves to a completed state. + while len(taskList): + update = pc.WaitForUpdates(version) + for filterSet in update.filterSet: + for objSet in filterSet.objectSet: + task = objSet.obj + for change in objSet.changeSet: + if change.name == 'info': + state = change.val.state + elif change.name == 'info.state': + state = change.val + else: + continue + + if not str(task) in taskList: + continue + + if state == vim.TaskInfo.State.success: + # Remove task from taskList + taskList.remove(str(task)) + elif state == vim.TaskInfo.State.error: + raise task.info.error + # Move to next version + version = update.version + finally: + if filter: + filter.Destroy() diff --git a/salt/ext/vsan/vsanmgmtObjects.py b/salt/ext/vsan/vsanmgmtObjects.py new file mode 100644 index 0000000000..15165afbaf --- /dev/null +++ b/salt/ext/vsan/vsanmgmtObjects.py @@ -0,0 +1,143 @@ +# pylint: skip-file +from pyVmomi.VmomiSupport import CreateDataType, CreateManagedType, CreateEnumType, AddVersion, AddVersionParent, F_LINK, F_LINKABLE, F_OPTIONAL + +CreateManagedType('vim.cluster.VsanPerformanceManager', 'VsanPerformanceManager', 'vmodl.ManagedObject', 'vim.version.version9', [], [('setStatsObjectPolicy', 'VsanPerfSetStatsObjectPolicy', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ('profile', 'vim.vm.ProfileSpec', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('deleteStatsObject', 'VsanPerfDeleteStatsObject', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('createStatsObjectTask', 'VsanPerfCreateStatsObjectTask', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ('profile', 'vim.vm.ProfileSpec', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('deleteStatsObjectTask', 'VsanPerfDeleteStatsObjectTask', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('queryClusterHealth', 'VsanPerfQueryClusterHealth', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ), (0, 'vmodl.DynamicData[]', 'vmodl.DynamicData[]'), 'System.Read', None), ('queryStatsObjectInformation', 'VsanPerfQueryStatsObjectInformation', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.cluster.VsanObjectInformation', 'vim.cluster.VsanObjectInformation'), 'System.Read', None), ('queryNodeInformation', 'VsanPerfQueryNodeInformation', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0 | F_OPTIONAL, 'vim.cluster.VsanPerfNodeInformation[]', 'vim.cluster.VsanPerfNodeInformation[]'), 'System.Read', None), ('queryVsanPerf', 'VsanPerfQueryPerf', 'vim.version.version9', (('querySpecs', 'vim.cluster.VsanPerfQuerySpec[]', 'vim.version.version9', 0, None), ('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.cluster.VsanPerfEntityMetricCSV[]', 'vim.cluster.VsanPerfEntityMetricCSV[]'), 'System.Read', None), ('getSupportedEntityTypes', 'VsanPerfGetSupportedEntityTypes', 'vim.version.version9', tuple(), (0 | F_OPTIONAL, 'vim.cluster.VsanPerfEntityType[]', 'vim.cluster.VsanPerfEntityType[]'), 'System.Read', None), ('createStatsObject', 'VsanPerfCreateStatsObject', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ('profile', 'vim.vm.ProfileSpec', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'string', 'string'), 'System.Read', None), ]) +CreateManagedType('vim.cluster.VsanVcDiskManagementSystem', 'VimClusterVsanVcDiskManagementSystem', 'vmodl.ManagedObject', 'vim.version.version10', [], [('initializeDiskMappings', 'InitializeDiskMappings', 'vim.version.version10', (('spec', 'vim.vsan.host.DiskMappingCreationSpec', 'vim.version.version10', 0, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('retrieveAllFlashCapabilities', 'RetrieveAllFlashCapabilities', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ), (0 | F_OPTIONAL, 'vim.vsan.host.VsanHostCapability[]', 'vim.vsan.host.VsanHostCapability[]'), 'System.Read', None), ('queryDiskMappings', 'QueryDiskMappings', 'vim.version.version10', (('host', 'vim.HostSystem', 'vim.version.version10', 0, None), ), (0 | F_OPTIONAL, 'vim.vsan.host.DiskMapInfoEx[]', 'vim.vsan.host.DiskMapInfoEx[]'), 'System.Read', None), ]) +CreateManagedType('vim.cluster.VsanObjectSystem', 'VsanObjectSystem', 'vmodl.ManagedObject', 'vim.version.version9', [], [('setVsanObjectPolicy', 'VosSetVsanObjectPolicy', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ('vsanObjectUuid', 'string', 'vim.version.version9', 0, None), ('profile', 'vim.vm.ProfileSpec', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('queryObjectIdentities', 'VsanQueryObjectIdentities', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ('objUuids', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ('includeHealth', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ('includeObjIdentity', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ('includeSpaceSummary', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0 | F_OPTIONAL, 'vim.cluster.VsanObjectIdentityAndHealth', 'vim.cluster.VsanObjectIdentityAndHealth'), 'System.Read', None), ('queryVsanObjectInformation', 'VosQueryVsanObjectInformation', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0 | F_OPTIONAL, None), ('vsanObjectQuerySpecs', 'vim.cluster.VsanObjectQuerySpec[]', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanObjectInformation[]', 'vim.cluster.VsanObjectInformation[]'), 'System.Read', None), ]) +CreateManagedType('vim.host.VsanStretchedClusterSystem', 'VimHostVsanStretchedClusterSystem', 'vmodl.ManagedObject', 'vim.version.version10', [], [('getStretchedClusterInfoFromCmmds', 'VSANHostGetStretchedClusterInfoFromCmmds', 'vim.version.version10', tuple(), (0 | F_OPTIONAL, 'vim.host.VSANStretchedClusterHostInfo[]', 'vim.host.VSANStretchedClusterHostInfo[]'), 'System.Read', None), ('witnessJoinVsanCluster', 'VSANWitnessJoinVsanCluster', 'vim.version.version10', (('clusterUuid', 'string', 'vim.version.version10', 0, None), ('preferredFd', 'string', 'vim.version.version10', 0, None), ('disableVsanAllowed', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0, 'void', 'void'), 'System.Read', None), ('witnessSetPreferredFaultDomain', 'VSANWitnessSetPreferredFaultDomain', 'vim.version.version10', (('preferredFd', 'string', 'vim.version.version10', 0, None), ), (0, 'void', 'void'), 'System.Read', None), ('addUnicastAgent', 'VSANHostAddUnicastAgent', 'vim.version.version10', (('witnessAddress', 'string', 'vim.version.version10', 0, None), ('witnessPort', 'int', 'vim.version.version10', 0 | F_OPTIONAL, None), ('overwrite', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0, 'void', 'void'), 'System.Read', None), ('clusterGetPreferredFaultDomain', 'VSANClusterGetPreferredFaultDomain', 'vim.version.version10', tuple(), (0 | F_OPTIONAL, 'vim.host.VSANCmmdsPreferredFaultDomainInfo', 'vim.host.VSANCmmdsPreferredFaultDomainInfo'), 'System.Read', None), ('witnessLeaveVsanCluster', 'VSANWitnessLeaveVsanCluster', 'vim.version.version10', tuple(), (0, 'void', 'void'), 'System.Read', None), ('getStretchedClusterCapability', 'VSANHostGetStretchedClusterCapability', 'vim.version.version10', tuple(), (0, 'vim.host.VSANStretchedClusterHostCapability', 'vim.host.VSANStretchedClusterHostCapability'), 'System.Read', None), ('removeUnicastAgent', 'VSANHostRemoveUnicastAgent', 'vim.version.version10', (('witnessAddress', 'string', 'vim.version.version10', 0, None), ('ignoreExistence', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0, 'void', 'void'), 'System.Read', None), ('listUnicastAgent', 'VSANHostListUnicastAgent', 'vim.version.version10', tuple(), (0, 'string', 'string'), 'System.Read', None), ]) +CreateManagedType('vim.VsanUpgradeSystemEx', 'VsanUpgradeSystemEx', 'vmodl.ManagedObject', 'vim.version.version10', [], [('performUpgrade', 'PerformVsanUpgradeEx', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ('performObjectUpgrade', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL, None), ('downgradeFormat', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL, None), ('allowReducedRedundancy', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL, None), ('excludeHosts', 'vim.HostSystem[]', 'vim.version.version10', 0 | F_OPTIONAL, None), ('spec', 'vim.cluster.VsanDiskFormatConversionSpec', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('performUpgradePreflightCheck', 'PerformVsanUpgradePreflightCheckEx', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ('downgradeFormat', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL, None), ('spec', 'vim.cluster.VsanDiskFormatConversionSpec', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0, 'vim.cluster.VsanDiskFormatConversionCheckResult', 'vim.cluster.VsanDiskFormatConversionCheckResult'), 'System.Read', None), ('retrieveSupportedFormatVersion', 'RetrieveSupportedVsanFormatVersion', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ), (0, 'int', 'int'), 'System.Read', None), ]) +CreateManagedType('vim.cluster.VsanCapabilitySystem', 'VsanCapabilitySystem', 'vmodl.ManagedObject', 'vim.version.version10', [], [('getCapabilities', 'VsanGetCapabilities', 'vim.version.version10', (('targets', 'vmodl.ManagedObject[]', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0, 'vim.cluster.VsanCapability[]', 'vim.cluster.VsanCapability[]'), 'System.Read', None), ]) +CreateManagedType('vim.cluster.VsanSpaceReportSystem', 'VsanSpaceReportSystem', 'vmodl.ManagedObject', 'vim.version.version9', [], [('querySpaceUsage', 'VsanQuerySpaceUsage', 'vim.version.version9', (('cluster', 'vim.ComputeResource', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanSpaceUsage', 'vim.cluster.VsanSpaceUsage'), 'System.Read', None), ]) +CreateManagedType('vim.cluster.VsanVcClusterConfigSystem', 'VsanVcClusterConfigSystem', 'vmodl.ManagedObject', 'vim.version.version10', [], [('getConfigInfoEx', 'VsanClusterGetConfig', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ), (0, 'vim.vsan.ConfigInfoEx', 'vim.vsan.ConfigInfoEx'), 'System.Read', None), ('reconfigureEx', 'VsanClusterReconfig', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ('vsanReconfigSpec', 'vim.vsan.ReconfigSpec', 'vim.version.version10', 0, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ]) +CreateManagedType('vim.host.VsanHealthSystem', 'HostVsanHealthSystem', 'vmodl.ManagedObject', 'vim.version.version9', [], [('queryAdvCfg', 'VsanHostQueryAdvCfg', 'vim.version.version9', (('options', 'string[]', 'vim.version.version9', 0, None), ), (0, 'vim.option.OptionValue[]', 'vim.option.OptionValue[]'), 'System.Read', None), ('queryPhysicalDiskHealthSummary', 'VsanHostQueryPhysicalDiskHealthSummary', 'vim.version.version9', tuple(), (0, 'vim.host.VsanPhysicalDiskHealthSummary', 'vim.host.VsanPhysicalDiskHealthSummary'), 'System.Read', None), ('startProactiveRebalance', 'VsanStartProactiveRebalance', 'vim.version.version9', (('timeSpan', 'int', 'vim.version.version9', 0 | F_OPTIONAL, None), ('varianceThreshold', 'float', 'vim.version.version9', 0 | F_OPTIONAL, None), ('timeThreshold', 'int', 'vim.version.version9', 0 | F_OPTIONAL, None), ('rateThreshold', 'int', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('queryHostInfoByUuids', 'VsanHostQueryHostInfoByUuids', 'vim.version.version9', (('uuids', 'string[]', 'vim.version.version9', 0, None), ), (0, 'vim.host.VsanQueryResultHostInfo[]', 'vim.host.VsanQueryResultHostInfo[]'), 'System.Read', None), ('queryVersion', 'VsanHostQueryHealthSystemVersion', 'vim.version.version9', tuple(), (0, 'string', 'string'), 'System.Read', None), ('queryVerifyNetworkSettings', 'VsanHostQueryVerifyNetworkSettings', 'vim.version.version9', (('peers', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.host.VsanNetworkHealthResult', 'vim.host.VsanNetworkHealthResult'), 'System.Read', None), ('queryRunIperfClient', 'VsanHostQueryRunIperfClient', 'vim.version.version9', (('multicast', 'boolean', 'vim.version.version9', 0, None), ('serverIp', 'string', 'vim.version.version9', 0, None), ), (0, 'vim.host.VsanNetworkLoadTestResult', 'vim.host.VsanNetworkLoadTestResult'), 'System.Read', None), ('runVmdkLoadTest', 'VsanHostRunVmdkLoadTest', 'vim.version.version9', (('runname', 'string', 'vim.version.version9', 0, None), ('durationSec', 'int', 'vim.version.version9', 0, None), ('specs', 'vim.host.VsanVmdkLoadTestSpec[]', 'vim.version.version9', 0, None), ), (0, 'vim.host.VsanVmdkLoadTestResult[]', 'vim.host.VsanVmdkLoadTestResult[]'), 'System.Read', None), ('queryObjectHealthSummary', 'VsanHostQueryObjectHealthSummary', 'vim.version.version9', (('objUuids', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ('includeObjUuids', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ('localHostOnly', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.host.VsanObjectOverallHealth', 'vim.host.VsanObjectOverallHealth'), 'System.Read', None), ('getHclInfo', 'VsanGetHclInfo', 'vim.version.version9', tuple(), (0, 'vim.host.VsanHostHclInfo', 'vim.host.VsanHostHclInfo'), 'System.Read', None), ('cleanupVmdkLoadTest', 'VsanHostCleanupVmdkLoadTest', 'vim.version.version9', (('runname', 'string', 'vim.version.version9', 0, None), ('specs', 'vim.host.VsanVmdkLoadTestSpec[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'string', 'string'), 'System.Read', None), ('waitForVsanHealthGenerationIdChange', 'VsanWaitForVsanHealthGenerationIdChange', 'vim.version.version9', (('timeout', 'int', 'vim.version.version9', 0, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('stopProactiveRebalance', 'VsanStopProactiveRebalance', 'vim.version.version9', tuple(), (0, 'boolean', 'boolean'), 'System.Read', None), ('repairImmediateObjects', 'VsanHostRepairImmediateObjects', 'vim.version.version9', (('uuids', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ('repairType', 'string', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.host.VsanRepairObjectsResult', 'vim.host.VsanRepairObjectsResult'), 'System.Read', None), ('prepareVmdkLoadTest', 'VsanHostPrepareVmdkLoadTest', 'vim.version.version9', (('runname', 'string', 'vim.version.version9', 0, None), ('specs', 'vim.host.VsanVmdkLoadTestSpec[]', 'vim.version.version9', 0, None), ), (0, 'string', 'string'), 'System.Read', None), ('queryRunIperfServer', 'VsanHostQueryRunIperfServer', 'vim.version.version9', (('multicast', 'boolean', 'vim.version.version9', 0, None), ('serverIp', 'string', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.host.VsanNetworkLoadTestResult', 'vim.host.VsanNetworkLoadTestResult'), 'System.Read', None), ('queryCheckLimits', 'VsanHostQueryCheckLimits', 'vim.version.version9', tuple(), (0, 'vim.host.VsanLimitHealthResult', 'vim.host.VsanLimitHealthResult'), 'System.Read', None), ('getProactiveRebalanceInfo', 'VsanGetProactiveRebalanceInfo', 'vim.version.version9', tuple(), (0, 'vim.host.VsanProactiveRebalanceInfoEx', 'vim.host.VsanProactiveRebalanceInfoEx'), 'System.Read', None), ('checkClomdLiveness', 'VsanHostClomdLiveness', 'vim.version.version9', tuple(), (0, 'boolean', 'boolean'), 'System.Read', None), ]) +CreateManagedType('vim.cluster.VsanVcClusterHealthSystem', 'VsanVcClusterHealthSystem', 'vmodl.ManagedObject', 'vim.version.version9', [], [('queryClusterCreateVmHealthHistoryTest', 'VsanQueryVcClusterCreateVmHealthHistoryTest', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('count', 'int', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0 | F_OPTIONAL, 'vim.cluster.VsanClusterCreateVmHealthTestResult[]', 'vim.cluster.VsanClusterCreateVmHealthTestResult[]'), 'System.Read', None), ('setLogLevel', 'VsanHealthSetLogLevel', 'vim.version.version9', (('level', 'vim.cluster.VsanHealthLogLevelEnum', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'void', 'void'), 'System.Read', None), ('testVsanClusterTelemetryProxy', 'VsanHealthTestVsanClusterTelemetryProxy', 'vim.version.version9', (('proxyConfig', 'vim.cluster.VsanClusterTelemetryProxyConfig', 'vim.version.version9', 0, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('uploadHclDb', 'VsanVcUploadHclDb', 'vim.version.version9', (('db', 'string', 'vim.version.version9', 0, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('updateHclDbFromWeb', 'VsanVcUpdateHclDbFromWeb', 'vim.version.version9', (('url', 'string', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('repairClusterObjectsImmediate', 'VsanHealthRepairClusterObjectsImmediate', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('uuids', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('queryClusterNetworkPerfTest', 'VsanQueryVcClusterNetworkPerfTest', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('multicast', 'boolean', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterNetworkLoadTestResult', 'vim.cluster.VsanClusterNetworkLoadTestResult'), 'System.Read', None), ('queryClusterVmdkLoadHistoryTest', 'VsanQueryVcClusterVmdkLoadHistoryTest', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('count', 'int', 'vim.version.version9', 0 | F_OPTIONAL, None), ('taskId', 'string', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0 | F_OPTIONAL, 'vim.cluster.VsanClusterVmdkLoadTestResult[]', 'vim.cluster.VsanClusterVmdkLoadTestResult[]'), 'System.Read', None), ('queryVsanClusterHealthCheckInterval', 'VsanHealthQueryVsanClusterHealthCheckInterval', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ), (0, 'int', 'int'), 'System.Read', None), ('queryClusterCreateVmHealthTest', 'VsanQueryVcClusterCreateVmHealthTest', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('timeout', 'int', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterCreateVmHealthTestResult', 'vim.cluster.VsanClusterCreateVmHealthTestResult'), 'System.Read', None), ('getClusterHclInfo', 'VsanVcClusterGetHclInfo', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('includeHostsResult', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.cluster.VsanClusterHclInfo', 'vim.cluster.VsanClusterHclInfo'), 'System.Read', None), ('queryAttachToSrHistory', 'VsanQueryAttachToSrHistory', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('count', 'int', 'vim.version.version9', 0 | F_OPTIONAL, None), ('taskId', 'string', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0 | F_OPTIONAL, 'vim.cluster.VsanAttachToSrOperation[]', 'vim.cluster.VsanAttachToSrOperation[]'), 'System.Read', None), ('rebalanceCluster', 'VsanRebalanceCluster', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('targetHosts', 'vim.HostSystem[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('runVmdkLoadTest', 'VsanVcClusterRunVmdkLoadTest', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('runname', 'string', 'vim.version.version9', 0, None), ('durationSec', 'int', 'vim.version.version9', 0 | F_OPTIONAL, None), ('specs', 'vim.host.VsanVmdkLoadTestSpec[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ('action', 'string', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('sendVsanTelemetry', 'VsanHealthSendVsanTelemetry', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ), (0, 'void', 'void'), 'System.Read', None), ('queryClusterNetworkPerfHistoryTest', 'VsanQueryVcClusterNetworkPerfHistoryTest', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('count', 'int', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0 | F_OPTIONAL, 'vim.cluster.VsanClusterNetworkLoadTestResult[]', 'vim.cluster.VsanClusterNetworkLoadTestResult[]'), 'System.Read', None), ('queryClusterHealthSummary', 'VsanQueryVcClusterHealthSummary', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('vmCreateTimeout', 'int', 'vim.version.version9', 0 | F_OPTIONAL, None), ('objUuids', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ('includeObjUuids', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ('fields', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ('fetchFromCache', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.cluster.VsanClusterHealthSummary', 'vim.cluster.VsanClusterHealthSummary'), 'System.Read', None), ('stopRebalanceCluster', 'VsanStopRebalanceCluster', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('targetHosts', 'vim.HostSystem[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('queryVsanClusterHealthConfig', 'VsanHealthQueryVsanClusterHealthConfig', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterHealthConfigs', 'vim.cluster.VsanClusterHealthConfigs'), 'System.Read', None), ('attachVsanSupportBundleToSr', 'VsanAttachVsanSupportBundleToSr', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('srNumber', 'string', 'vim.version.version9', 0, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('queryClusterVmdkWorkloadTypes', 'VsanQueryVcClusterVmdkWorkloadTypes', 'vim.version.version9', tuple(), (0, 'vim.cluster.VsanStorageWorkloadType[]', 'vim.cluster.VsanStorageWorkloadType[]'), 'System.Read', None), ('queryVerifyClusterHealthSystemVersions', 'VsanVcClusterQueryVerifyHealthSystemVersions', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterHealthSystemVersionResult', 'vim.cluster.VsanClusterHealthSystemVersionResult'), 'System.Read', None), ('isRebalanceRunning', 'VsanHealthIsRebalanceRunning', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('targetHosts', 'vim.HostSystem[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('setVsanClusterHealthCheckInterval', 'VsanHealthSetVsanClusterHealthCheckInterval', 'vim.version.version9', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version9', 0, None), ('vsanClusterHealthCheckInterval', 'int', 'vim.version.version9', 0, None), ), (0, 'void', 'void'), 'System.Read', None), ]) +CreateManagedType('vim.cluster.VsanVcStretchedClusterSystem', 'VimClusterVsanVcStretchedClusterSystem', 'vmodl.ManagedObject', 'vim.version.version10', [], [('isWitnessHost', 'VSANVcIsWitnessHost', 'vim.version.version10', (('host', 'vim.HostSystem', 'vim.version.version10', 0, None), ), (0, 'boolean', 'boolean'), 'System.Read', None), ('setPreferredFaultDomain', 'VSANVcSetPreferredFaultDomain', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ('preferredFd', 'string', 'vim.version.version10', 0, None), ('witnessHost', 'vim.HostSystem', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('getPreferredFaultDomain', 'VSANVcGetPreferredFaultDomain', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ), (0 | F_OPTIONAL, 'vim.cluster.VSANPreferredFaultDomainInfo', 'vim.cluster.VSANPreferredFaultDomainInfo'), 'System.Read', None), ('getWitnessHosts', 'VSANVcGetWitnessHosts', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ), (0 | F_OPTIONAL, 'vim.cluster.VSANWitnessHostInfo[]', 'vim.cluster.VSANWitnessHostInfo[]'), 'System.Read', None), ('retrieveStretchedClusterVcCapability', 'VSANVcRetrieveStretchedClusterVcCapability', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ('verifyAllConnected', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0 | F_OPTIONAL, 'vim.cluster.VSANStretchedClusterCapability[]', 'vim.cluster.VSANStretchedClusterCapability[]'), 'System.Read', None), ('convertToStretchedCluster', 'VSANVcConvertToStretchedCluster', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ('faultDomainConfig', 'vim.cluster.VSANStretchedClusterFaultDomainConfig', 'vim.version.version10', 0, None), ('witnessHost', 'vim.HostSystem', 'vim.version.version10', 0, None), ('preferredFd', 'string', 'vim.version.version10', 0, None), ('diskMapping', 'vim.vsan.host.DiskMapping', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ('removeWitnessHost', 'VSANVcRemoveWitnessHost', 'vim.version.version10', (('cluster', 'vim.ClusterComputeResource', 'vim.version.version10', 0, None), ('witnessHost', 'vim.HostSystem', 'vim.version.version10', 0 | F_OPTIONAL, None), ('witnessAddress', 'string', 'vim.version.version10', 0 | F_OPTIONAL, None), ), (0, 'vim.Task', 'vim.Task'), 'System.Read', None), ]) +CreateManagedType('vim.cluster.VsanClusterHealthSystem', 'VsanClusterHealthSystem', 'vmodl.ManagedObject', 'vim.version.version9', [], [('queryPhysicalDiskHealthSummary', 'VsanQueryClusterPhysicalDiskHealthSummary', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ), (0, 'vim.host.VsanPhysicalDiskHealthSummary[]', 'vim.host.VsanPhysicalDiskHealthSummary[]'), 'System.Read', None), ('queryClusterNetworkPerfTest', 'VsanQueryClusterNetworkPerfTest', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ('multicast', 'boolean', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterNetworkLoadTestResult', 'vim.cluster.VsanClusterNetworkLoadTestResult'), 'System.Read', None), ('queryAdvCfgSync', 'VsanQueryClusterAdvCfgSync', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterAdvCfgSyncResult[]', 'vim.cluster.VsanClusterAdvCfgSyncResult[]'), 'System.Read', None), ('repairClusterImmediateObjects', 'VsanRepairClusterImmediateObjects', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ('uuids', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.cluster.VsanClusterHealthSystemObjectsRepairResult', 'vim.cluster.VsanClusterHealthSystemObjectsRepairResult'), 'System.Read', None), ('queryVerifyClusterNetworkSettings', 'VsanQueryVerifyClusterNetworkSettings', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterNetworkHealthResult', 'vim.cluster.VsanClusterNetworkHealthResult'), 'System.Read', None), ('queryClusterCreateVmHealthTest', 'VsanQueryClusterCreateVmHealthTest', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ('timeout', 'int', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterCreateVmHealthTestResult', 'vim.cluster.VsanClusterCreateVmHealthTestResult'), 'System.Read', None), ('queryClusterHealthSystemVersions', 'VsanQueryClusterHealthSystemVersions', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterHealthSystemVersionResult', 'vim.cluster.VsanClusterHealthSystemVersionResult'), 'System.Read', None), ('getClusterHclInfo', 'VsanClusterGetHclInfo', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterHclInfo', 'vim.cluster.VsanClusterHclInfo'), 'System.Read', None), ('queryCheckLimits', 'VsanQueryClusterCheckLimits', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterLimitHealthResult', 'vim.cluster.VsanClusterLimitHealthResult'), 'System.Read', None), ('queryCaptureVsanPcap', 'VsanQueryClusterCaptureVsanPcap', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ('duration', 'int', 'vim.version.version9', 0, None), ('vmknic', 'vim.cluster.VsanClusterHostVmknicMapping[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ('includeRawPcap', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ('includeIgmp', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL, None), ('cmmdsMsgTypeFilter', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ('cmmdsPorts', 'int[]', 'vim.version.version9', 0 | F_OPTIONAL, None), ('clusterUuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL, None), ), (0, 'vim.cluster.VsanVsanClusterPcapResult', 'vim.cluster.VsanVsanClusterPcapResult'), 'System.Read', None), ('checkClusterClomdLiveness', 'VsanCheckClusterClomdLiveness', 'vim.version.version9', (('hosts', 'string[]', 'vim.version.version9', 0, None), ('esxRootPassword', 'string', 'vim.version.version9', 0, None), ), (0, 'vim.cluster.VsanClusterClomdLivenessResult', 'vim.cluster.VsanClusterClomdLivenessResult'), 'System.Read', None), ]) +CreateDataType('vim.host.VSANCmmdsNodeInfo', 'VimHostVSANCmmdsNodeInfo', 'vmodl.DynamicData', 'vim.version.version10', [('nodeUuid', 'string', 'vim.version.version10', 0), ('isWitness', 'boolean', 'vim.version.version10', 0)]) +CreateDataType('vim.host.VsanPhysicalDiskHealth', 'VsanPhysicalDiskHealth', 'vmodl.DynamicData', 'vim.version.version9', [('name', 'string', 'vim.version.version9', 0), ('uuid', 'string', 'vim.version.version9', 0), ('inCmmds', 'boolean', 'vim.version.version9', 0), ('inVsi', 'boolean', 'vim.version.version9', 0), ('dedupScope', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('formatVersion', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('isAllFlash', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('congestionValue', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('congestionArea', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('congestionHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('metadataHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('operationalHealthDescription', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('operationalHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('dedupUsageHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('capacityHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('summaryHealth', 'string', 'vim.version.version9', 0), ('capacity', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('usedCapacity', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('reservedCapacity', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('totalBytes', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('freeBytes', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('hashedBytes', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('dedupedBytes', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('scsiDisk', 'vim.host.ScsiDisk', 'vim.version.version9', 0 | F_OPTIONAL), ('usedComponents', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('maxComponents', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('compLimitHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.DataEfficiencyConfig', 'VsanDataEfficiencyConfig', 'vmodl.DynamicData', 'vim.version.version10', [('dedupEnabled', 'boolean', 'vim.version.version10', 0), ('compressionEnabled', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.StorageComplianceResult', 'VsanStorageComplianceResult', 'vmodl.DynamicData', 'vim.version.version9', [('checkTime', 'vmodl.DateTime', 'vim.version.version9', 0 | F_OPTIONAL), ('profile', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('objectUUID', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('complianceStatus', 'vim.cluster.StorageComplianceStatus', 'vim.version.version9', 0), ('mismatch', 'boolean', 'vim.version.version9', 0), ('violatedPolicies', 'vim.cluster.StoragePolicyStatus[]', 'vim.version.version9', 0 | F_OPTIONAL), ('operationalStatus', 'vim.cluster.StorageOperationalStatus', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthGroup', 'VsanClusterHealthGroup', 'vmodl.DynamicData', 'vim.version.version9', [('groupId', 'string', 'vim.version.version9', 0), ('groupName', 'string', 'vim.version.version9', 0), ('groupHealth', 'string', 'vim.version.version9', 0), ('groupTests', 'vim.cluster.VsanClusterHealthTest[]', 'vim.version.version9', 0 | F_OPTIONAL), ('groupDetails', 'vim.cluster.VsanClusterHealthResultBase[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanSpaceUsageDetailResult', 'VsanSpaceUsageDetailResult', 'vmodl.DynamicData', 'vim.version.version9', [('spaceUsageByObjectType', 'vim.cluster.VsanObjectSpaceSummary[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanAttachToSrOperation', 'VsanAttachToSrOperation', 'vmodl.DynamicData', 'vim.version.version9', [('task', 'vim.Task', 'vim.version.version9', 0 | F_OPTIONAL), ('success', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('timestamp', 'vmodl.DateTime', 'vim.version.version9', 0 | F_OPTIONAL), ('srNumber', 'string', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanObjectSpaceSummary', 'VsanObjectSpaceSummary', 'vmodl.DynamicData', 'vim.version.version9', [('objType', 'vim.cluster.VsanObjectTypeEnum', 'vim.version.version9', 0 | F_OPTIONAL), ('overheadB', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('temporaryOverheadB', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('primaryCapacityB', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('provisionCapacityB', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('reservedCapacityB', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('overReservedB', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('physicalUsedB', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('usedB', 'long', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHclInfo', 'VsanClusterHclInfo', 'vmodl.DynamicData', 'vim.version.version9', [('hclDbLastUpdate', 'vmodl.DateTime', 'vim.version.version9', 0 | F_OPTIONAL), ('hclDbAgeHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('hostResults', 'vim.host.VsanHostHclInfo[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanPerfGraph', 'VsanPerfGraph', 'vmodl.DynamicData', 'vim.version.version9', [('id', 'string', 'vim.version.version9', 0), ('metrics', 'vim.cluster.VsanPerfMetricId[]', 'vim.version.version9', 0), ('unit', 'vim.cluster.VsanPerfStatsUnitType', 'vim.version.version9', 0), ('threshold', 'vim.cluster.VsanPerfThreshold', 'vim.version.version9', 0 | F_OPTIONAL), ('name', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('description', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthResultBase', 'VsanClusterHealthResultBase', 'vmodl.DynamicData', 'vim.version.version9', [('label', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanPerfTopEntity', 'VsanPerfTopEntity', 'vmodl.DynamicData', 'vim.version.version9', [('entityRefId', 'string', 'vim.version.version9', 0), ('value', 'string', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanClusterBalancePerDiskInfo', 'VsanClusterBalancePerDiskInfo', 'vmodl.DynamicData', 'vim.version.version9', [('uuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('fullness', 'long', 'vim.version.version9', 0), ('variance', 'long', 'vim.version.version9', 0), ('fullnessAboveThreshold', 'long', 'vim.version.version9', 0), ('dataToMoveB', 'long', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanClusterHealthTest', 'VsanClusterHealthTest', 'vmodl.DynamicData', 'vim.version.version9', [('testId', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('testName', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('testDescription', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('testShortDescription', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('testHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('testDetails', 'vim.cluster.VsanClusterHealthResultBase[]', 'vim.version.version9', 0 | F_OPTIONAL), ('testActions', 'vim.cluster.VsanClusterHealthAction[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.StoragePolicyStatus', 'VsanStoragePolicyStatus', 'vmodl.DynamicData', 'vim.version.version9', [('id', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('expectedValue', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('currentValue', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanPerfMemberInfo', 'VsanPerfMemberInfo', 'vmodl.DynamicData', 'vim.version.version9', [('thumbprint', 'string', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanPerfMetricId', 'VsanPerfMetricId', 'vmodl.DynamicData', 'vim.version.version9', [('label', 'string', 'vim.version.version9', 0), ('group', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('rollupType', 'vim.cluster.VsanPerfSummaryType', 'vim.version.version9', 0 | F_OPTIONAL), ('statsType', 'vim.cluster.VsanPerfStatsType', 'vim.version.version9', 0 | F_OPTIONAL), ('name', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('description', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('metricsCollectInterval', 'int', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VSANWitnessHostInfo', 'VimClusterVSANWitnessHostInfo', 'vmodl.DynamicData', 'vim.version.version10', [('nodeUuid', 'string', 'vim.version.version10', 0), ('faultDomainName', 'string', 'vim.version.version10', 0 | F_OPTIONAL), ('preferredFdName', 'string', 'vim.version.version10', 0 | F_OPTIONAL), ('preferredFdUuid', 'string', 'vim.version.version10', 0 | F_OPTIONAL), ('unicastAgentAddr', 'string', 'vim.version.version10', 0 | F_OPTIONAL), ('host', 'vim.HostSystem', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanHealthExtMgmtPreCheckResult', 'VsanHealthExtMgmtPreCheckResult', 'vmodl.DynamicData', 'vim.version.version9', [('overallResult', 'boolean', 'vim.version.version9', 0), ('esxVersionCheckPassed', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('drsCheckPassed', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('eamConnectionCheckPassed', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('installStateCheckPassed', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('results', 'vim.cluster.VsanClusterHealthTest[]', 'vim.version.version9', 0), ('vumRegistered', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.upgradesystem.HostWithHybridDiskgroupIssue', 'VsanHostWithHybridDiskgroupIssue', 'vim.VsanUpgradeSystem.PreflightCheckIssue', 'vim.version.version10', [('hosts', 'vim.HostSystem[]', 'vim.version.version10', 0)]) +CreateDataType('vim.cluster.VsanPerfMetricSeriesCSV', 'VsanPerfMetricSeriesCSV', 'vmodl.DynamicData', 'vim.version.version9', [('metricId', 'vim.cluster.VsanPerfMetricId', 'vim.version.version9', 0), ('values', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanPerfQuerySpec', 'VsanPerfQuerySpec', 'vmodl.DynamicData', 'vim.version.version9', [('entityRefId', 'string', 'vim.version.version9', 0), ('startTime', 'vmodl.DateTime', 'vim.version.version9', 0 | F_OPTIONAL), ('endTime', 'vmodl.DateTime', 'vim.version.version9', 0 | F_OPTIONAL), ('group', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('labels', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('interval', 'int', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanRepairObjectsResult', 'VsanRepairObjectsResult', 'vmodl.DynamicData', 'vim.version.version9', [('inQueueObjects', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('failedRepairObjects', 'vim.host.VsanFailedRepairObjectResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('notInQueueObjects', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterNetworkPartitionInfo', 'VsanClusterNetworkPartitionInfo', 'vmodl.DynamicData', 'vim.version.version9', [('hosts', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.upgradesystem.MixedEsxVersionIssue', 'VsanMixedEsxVersionIssue', 'vim.VsanUpgradeSystem.PreflightCheckIssue', 'vim.version.version10', []) +CreateDataType('vim.cluster.VsanClusterClomdLivenessResult', 'VsanClusterClomdLivenessResult', 'vmodl.DynamicData', 'vim.version.version9', [('clomdLivenessResult', 'vim.cluster.VsanHostClomdLivenessResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('issueFound', 'boolean', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanVsanClusterPcapResult', 'VsanVsanClusterPcapResult', 'vmodl.DynamicData', 'vim.version.version9', [('pkts', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('groups', 'vim.cluster.VsanVsanClusterPcapGroup[]', 'vim.version.version9', 0 | F_OPTIONAL), ('issues', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('hostResults', 'vim.host.VsanVsanPcapResult[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanPerfMasterInformation', 'VsanPerfMasterInformation', 'vmodl.DynamicData', 'vim.version.version9', [('secSinceLastStatsWrite', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('secSinceLastStatsCollect', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('statsIntervalSec', 'long', 'vim.version.version9', 0), ('collectionFailureHostUuids', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('renamedStatsDirectories', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('statsDirectoryPercentFree', 'long', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanHostCreateVmHealthTestResult', 'VsanHostCreateVmHealthTestResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostname', 'string', 'vim.version.version9', 0), ('state', 'string', 'vim.version.version9', 0), ('fault', 'vmodl.MethodFault', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanDiskFormatConversionCheckResult', 'VsanDiskFormatConversionCheckResult', 'vim.VsanUpgradeSystem.PreflightCheckResult', 'vim.version.version10', [('isSupported', 'boolean', 'vim.version.version10', 0), ('targetVersion', 'int', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthSystemObjectsRepairResult', 'VsanClusterHealthSystemObjectsRepairResult', 'vmodl.DynamicData', 'vim.version.version9', [('inRepairingQueueObjects', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('failedRepairObjects', 'vim.host.VsanFailedRepairObjectResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('issueFound', 'boolean', 'vim.version.version9', 0)]) +CreateDataType('vim.host.VsanHostHclInfo', 'VsanHostHclInfo', 'vmodl.DynamicData', 'vim.version.version9', [('hostname', 'string', 'vim.version.version9', 0), ('hclChecked', 'boolean', 'vim.version.version9', 0), ('releaseName', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('error', 'vmodl.MethodFault', 'vim.version.version9', 0 | F_OPTIONAL), ('controllers', 'vim.host.VsanHclControllerInfo[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VSANStretchedClusterCapability', 'VimClusterVSANStretchedClusterCapability', 'vmodl.DynamicData', 'vim.version.version10', [('hostMoId', 'string', 'vim.version.version10', 0), ('connStatus', 'string', 'vim.version.version10', 0 | F_OPTIONAL), ('isSupported', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL), ('hostCapability', 'vim.host.VSANStretchedClusterHostCapability', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanDiskMappingsConfigSpec', 'VimClusterVsanDiskMappingsConfigSpec', 'vmodl.DynamicData', 'vim.version.version10', [('hostDiskMappings', 'vim.cluster.VsanHostDiskMapping[]', 'vim.version.version10', 0)]) +CreateDataType('vim.host.VsanHostVmdkLoadTestResult', 'VsanHostVmdkLoadTestResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostname', 'string', 'vim.version.version9', 0), ('issueFound', 'boolean', 'vim.version.version9', 0), ('faultMessage', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('vmdkResults', 'vim.host.VsanVmdkLoadTestResult[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.ReconfigSpec', 'VimVsanReconfigSpec', 'vmodl.DynamicData', 'vim.version.version10', [('vsanClusterConfig', 'vim.vsan.cluster.ConfigInfo', 'vim.version.version10', 0 | F_OPTIONAL), ('dataEfficiencyConfig', 'vim.vsan.DataEfficiencyConfig', 'vim.version.version10', 0 | F_OPTIONAL), ('diskMappingSpec', 'vim.cluster.VsanDiskMappingsConfigSpec', 'vim.version.version10', 0 | F_OPTIONAL), ('faultDomainsSpec', 'vim.cluster.VsanFaultDomainsConfigSpec', 'vim.version.version10', 0 | F_OPTIONAL), ('modify', 'boolean', 'vim.version.version10', 0), ('allowReducedRedundancy', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanNetworkPeerHealthResult', 'VsanNetworkPeerHealthResult', 'vmodl.DynamicData', 'vim.version.version9', [('peer', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('peerHostname', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('peerVmknicName', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('smallPingTestSuccessPct', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('largePingTestSuccessPct', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('maxLatencyUs', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('onSameIpSubnet', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('sourceVmknicName', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanWitnessSpec', 'VimClusterVsanWitnessSpec', 'vmodl.DynamicData', 'vim.version.version10', [('host', 'vim.HostSystem', 'vim.version.version10', 0), ('preferredFaultDomainName', 'string', 'vim.version.version10', 0), ('diskMapping', 'vim.vsan.host.DiskMapping', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.host.DiskMappingCreationSpec', 'VimVsanHostDiskMappingCreationSpec', 'vmodl.DynamicData', 'vim.version.version10', [('host', 'vim.HostSystem', 'vim.version.version10', 0), ('cacheDisks', 'vim.host.ScsiDisk[]', 'vim.version.version10', 0 | F_OPTIONAL), ('capacityDisks', 'vim.host.ScsiDisk[]', 'vim.version.version10', 0), ('creationType', 'vim.vsan.host.DiskMappingCreationType', 'vim.version.version10', 0)]) +CreateDataType('vim.host.VsanLimitHealthResult', 'VsanLimitHealthResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostname', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('issueFound', 'boolean', 'vim.version.version9', 0), ('maxComponents', 'int', 'vim.version.version9', 0), ('freeComponents', 'int', 'vim.version.version9', 0), ('componentLimitHealth', 'string', 'vim.version.version9', 0), ('lowestFreeDiskSpacePct', 'int', 'vim.version.version9', 0), ('usedDiskSpaceB', 'long', 'vim.version.version9', 0), ('totalDiskSpaceB', 'long', 'vim.version.version9', 0), ('diskFreeSpaceHealth', 'string', 'vim.version.version9', 0), ('reservedRcSizeB', 'long', 'vim.version.version9', 0), ('totalRcSizeB', 'long', 'vim.version.version9', 0), ('rcFreeReservationHealth', 'string', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VSANPreferredFaultDomainInfo', 'VimClusterVSANPreferredFaultDomainInfo', 'vmodl.DynamicData', 'vim.version.version10', [('preferredFaultDomainName', 'string', 'vim.version.version10', 0), ('preferredFaultDomainId', 'string', 'vim.version.version10', 0)]) +CreateDataType('vim.host.VsanObjectOverallHealth', 'VsanObjectOverallHealth', 'vmodl.DynamicData', 'vim.version.version9', [('objectHealthDetail', 'vim.host.VsanObjectHealth[]', 'vim.version.version9', 0 | F_OPTIONAL), ('objectVersionCompliance', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanVsanClusterPcapGroup', 'VsanVsanClusterPcapGroup', 'vmodl.DynamicData', 'vim.version.version9', [('master', 'string', 'vim.version.version9', 0), ('members', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthResultColumnInfo', 'VsanClusterHealthResultColumnInfo', 'vmodl.DynamicData', 'vim.version.version9', [('label', 'string', 'vim.version.version9', 0), ('type', 'string', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanClusterNetworkHealthResult', 'VsanClusterNetworkHealthResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostResults', 'vim.host.VsanNetworkHealthResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('issueFound', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('vsanVmknicPresent', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('matchingMulticastConfig', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('matchingIpSubnets', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('pingTestSuccess', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('largePingTestSuccess', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('potentialMulticastIssue', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('otherHostsInVsanCluster', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('partitions', 'vim.cluster.VsanClusterNetworkPartitionInfo[]', 'vim.version.version9', 0 | F_OPTIONAL), ('hostsWithVsanDisabled', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('hostsDisconnected', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('hostsCommFailure', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('hostsInEsxMaintenanceMode', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('hostsInVsanMaintenanceMode', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('infoAboutUnexpectedHosts', 'vim.host.VsanQueryResultHostInfo[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanPerfNodeInformation', 'VsanPerfNodeInformation', 'vmodl.DynamicData', 'vim.version.version9', [('version', 'string', 'vim.version.version9', 0), ('hostname', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('error', 'vmodl.MethodFault', 'vim.version.version9', 0 | F_OPTIONAL), ('isCmmdsMaster', 'boolean', 'vim.version.version9', 0), ('isStatsMaster', 'boolean', 'vim.version.version9', 0), ('vsanMasterUuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('vsanNodeUuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('masterInfo', 'vim.cluster.VsanPerfMasterInformation', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanPerfEntityMetricCSV', 'VsanPerfEntityMetricCSV', 'vmodl.DynamicData', 'vim.version.version9', [('entityRefId', 'string', 'vim.version.version9', 0), ('sampleInfo', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('value', 'vim.cluster.VsanPerfMetricSeriesCSV[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.upgradesystem.DiskUnhealthIssue', 'VsanDiskUnhealthIssue', 'vim.VsanUpgradeSystem.PreflightCheckIssue', 'vim.version.version10', [('uuids', 'string[]', 'vim.version.version10', 0)]) +CreateDataType('vim.cluster.VsanFaultDomainSpec', 'VimClusterVsanFaultDomainSpec', 'vmodl.DynamicData', 'vim.version.version10', [('hosts', 'vim.HostSystem[]', 'vim.version.version10', 0), ('name', 'string', 'vim.version.version10', 0)]) +CreateDataType('vim.vsan.upgradesystem.ObjectInaccessibleIssue', 'VsanObjectInaccessibleIssue', 'vim.VsanUpgradeSystem.PreflightCheckIssue', 'vim.version.version10', [('uuids', 'string[]', 'vim.version.version10', 0)]) +CreateDataType('vim.cluster.VsanDiskFormatConversionSpec', 'VsanDiskFormatConversionSpec', 'vmodl.DynamicData', 'vim.version.version10', [('dataEfficiencyConfig', 'vim.vsan.DataEfficiencyConfig', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthAction', 'VsanClusterHealthAction', 'vmodl.DynamicData', 'vim.version.version9', [('actionId', 'vim.cluster.VsanClusterHealthActionIdEnum', 'vim.version.version9', 0), ('actionLabel', 'vmodl.LocalizableMessage', 'vim.version.version9', 0), ('actionDescription', 'vmodl.LocalizableMessage', 'vim.version.version9', 0), ('enabled', 'boolean', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanClusterHealthSystemVersionResult', 'VsanClusterHealthSystemVersionResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostResults', 'vim.cluster.VsanHostHealthSystemVersionResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('vcVersion', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('issueFound', 'boolean', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanClusterHealthResultRow', 'VsanClusterHealthResultRow', 'vmodl.DynamicData', 'vim.version.version9', [('values', 'string[]', 'vim.version.version9', 0), ('nestedRows', 'vim.cluster.VsanClusterHealthResultRow[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthSystemStatusResult', 'VsanClusterHealthSystemStatusResult', 'vmodl.DynamicData', 'vim.version.version9', [('status', 'string', 'vim.version.version9', 0), ('goalState', 'string', 'vim.version.version9', 0), ('untrackedHosts', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('trackedHostsStatus', 'vim.host.VsanHostHealthSystemStatusResult[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanHostDiskMapping', 'VimClusterVsanHostDiskMapping', 'vmodl.DynamicData', 'vim.version.version10', [('host', 'vim.HostSystem', 'vim.version.version10', 0), ('cacheDisks', 'vim.host.ScsiDisk[]', 'vim.version.version10', 0 | F_OPTIONAL), ('capacityDisks', 'vim.host.ScsiDisk[]', 'vim.version.version10', 0), ('type', 'vim.cluster.VsanDiskGroupCreationType', 'vim.version.version10', 0)]) +CreateDataType('vim.cluster.VSANStretchedClusterFaultDomainConfig', 'VimClusterVSANStretchedClusterFaultDomainConfig', 'vmodl.DynamicData', 'vim.version.version10', [('firstFdName', 'string', 'vim.version.version10', 0), ('firstFdHosts', 'vim.HostSystem[]', 'vim.version.version10', 0), ('secondFdName', 'string', 'vim.version.version10', 0), ('secondFdHosts', 'vim.HostSystem[]', 'vim.version.version10', 0)]) +CreateDataType('vim.host.VSANStretchedClusterHostInfo', 'VimHostVSANStretchedClusterHostInfo', 'vmodl.DynamicData', 'vim.version.version10', [('nodeInfo', 'vim.host.VSANCmmdsNodeInfo', 'vim.version.version10', 0), ('faultDomainInfo', 'vim.host.VSANCmmdsFaultDomainInfo', 'vim.version.version10', 0 | F_OPTIONAL), ('preferredFaultDomainInfo', 'vim.host.VSANCmmdsPreferredFaultDomainInfo', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.upgradesystem.HigherObjectsPresentDuringDowngradeIssue', 'VsanHigherObjectsPresentDuringDowngradeIssue', 'vim.VsanUpgradeSystem.PreflightCheckIssue', 'vim.version.version10', [('uuids', 'string[]', 'vim.version.version10', 0)]) +CreateDataType('vim.host.VSANCmmdsFaultDomainInfo', 'VimHostVSANCmmdsFaultDomainInfo', 'vmodl.DynamicData', 'vim.version.version10', [('faultDomainId', 'string', 'vim.version.version10', 0), ('faultDomainName', 'string', 'vim.version.version10', 0)]) +CreateDataType('vim.fault.VsanNodeNotMaster', 'VsanNodeNotMaster', 'vim.fault.VimFault', 'vim.version.version9', [('vsanMasterUuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('cmmdsMasterButNotStatsMaster', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanHostHealthSystemVersionResult', 'VsanHostHealthSystemVersionResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostname', 'string', 'vim.version.version9', 0), ('version', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('error', 'vmodl.MethodFault', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthConfigs', 'VsanClusterHealthConfigs', 'vmodl.DynamicData', 'vim.version.version9', [('enableVsanTelemetry', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('vsanTelemetryInterval', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('vsanTelemetryProxy', 'vim.cluster.VsanClusterTelemetryProxyConfig', 'vim.version.version9', 0 | F_OPTIONAL), ('configs', 'vim.cluster.VsanClusterHealthResultKeyValuePair[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterWhatifHostFailuresResult', 'VsanClusterWhatifHostFailuresResult', 'vmodl.DynamicData', 'vim.version.version9', [('numFailures', 'long', 'vim.version.version9', 0), ('totalUsedCapacityB', 'long', 'vim.version.version9', 0), ('totalCapacityB', 'long', 'vim.version.version9', 0), ('totalRcReservationB', 'long', 'vim.version.version9', 0), ('totalRcSizeB', 'long', 'vim.version.version9', 0), ('usedComponents', 'long', 'vim.version.version9', 0), ('totalComponents', 'long', 'vim.version.version9', 0), ('componentLimitHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('diskFreeSpaceHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('rcFreeReservationHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanObjectIdentityAndHealth', 'VsanObjectIdentityAndHealth', 'vmodl.DynamicData', 'vim.version.version9', [('identities', 'vim.cluster.VsanObjectIdentity[]', 'vim.version.version9', 0 | F_OPTIONAL), ('health', 'vim.host.VsanObjectOverallHealth', 'vim.version.version9', 0 | F_OPTIONAL), ('spaceSummary', 'vim.cluster.VsanObjectSpaceSummary[]', 'vim.version.version9', 0 | F_OPTIONAL), ('rawData', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanHclControllerInfo', 'VsanHclControllerInfo', 'vmodl.DynamicData', 'vim.version.version9', [('deviceName', 'string', 'vim.version.version9', 0), ('deviceDisplayName', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('driverName', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('driverVersion', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('vendorId', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('deviceId', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('subVendorId', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('subDeviceId', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('extraInfo', 'vim.KeyValue[]', 'vim.version.version9', 0 | F_OPTIONAL), ('deviceOnHcl', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('releaseSupported', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('releasesOnHcl', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('driverVersionsOnHcl', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('driverVersionSupported', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('fwVersionSupported', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('fwVersionOnHcl', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('cacheConfigSupported', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('cacheConfigOnHcl', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('raidConfigSupported', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('raidConfigOnHcl', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('fwVersion', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('raidConfig', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('cacheConfig', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('cimProviderInfo', 'vim.host.VsanHostCimProviderInfo', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthResultKeyValuePair', 'VsanClusterHealthResultKeyValuePair', 'vmodl.DynamicData', 'vim.version.version9', [('key', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('value', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.StorageOperationalStatus', 'VsanStorageOperationalStatus', 'vmodl.DynamicData', 'vim.version.version9', [('healthy', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('operationETA', 'vmodl.DateTime', 'vim.version.version9', 0 | F_OPTIONAL), ('operationProgress', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('transitional', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanSpaceUsage', 'VsanSpaceUsage', 'vmodl.DynamicData', 'vim.version.version9', [('totalCapacityB', 'long', 'vim.version.version9', 0), ('freeCapacityB', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('spaceOverview', 'vim.cluster.VsanObjectSpaceSummary', 'vim.version.version9', 0 | F_OPTIONAL), ('spaceDetail', 'vim.cluster.VsanSpaceUsageDetailResult', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthResultTable', 'VsanClusterHealthResultTable', 'vim.cluster.VsanClusterHealthResultBase', 'vim.version.version9', [('columns', 'vim.cluster.VsanClusterHealthResultColumnInfo[]', 'vim.version.version9', 0 | F_OPTIONAL), ('rows', 'vim.cluster.VsanClusterHealthResultRow[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterConfig', 'VsanClusterConfig', 'vmodl.DynamicData', 'vim.version.version9', [('config', 'vim.vsan.cluster.ConfigInfo', 'vim.version.version9', 0), ('name', 'string', 'vim.version.version9', 0), ('hosts', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.host.VsanHostCapability', 'VimVsanHostVsanHostCapability', 'vmodl.DynamicData', 'vim.version.version10', [('host', 'vim.HostSystem', 'vim.version.version10', 0), ('isSupported', 'boolean', 'vim.version.version10', 0), ('isLicensed', 'boolean', 'vim.version.version10', 0)]) +CreateDataType('vim.cluster.VsanPerfThreshold', 'VsanPerfThreshold', 'vmodl.DynamicData', 'vim.version.version9', [('direction', 'vim.cluster.VsanPerfThresholdDirectionType', 'vim.version.version9', 0), ('yellow', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('red', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanNetworkHealthResult', 'VsanNetworkHealthResult', 'vmodl.DynamicData', 'vim.version.version9', [('host', 'vim.HostSystem', 'vim.version.version9', 0 | F_OPTIONAL), ('hostname', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('vsanVmknicPresent', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('ipSubnets', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('issueFound', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('peerHealth', 'vim.host.VsanNetworkPeerHealthResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('multicastConfig', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.ConfigInfoEx', 'VsanConfigInfoEx', 'vim.vsan.cluster.ConfigInfo', 'vim.version.version10', [('dataEfficiencyConfig', 'vim.vsan.DataEfficiencyConfig', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanVmdkLoadTestResult', 'VsanVmdkLoadTestResult', 'vmodl.DynamicData', 'vim.version.version9', [('success', 'boolean', 'vim.version.version9', 0), ('faultMessage', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('spec', 'vim.host.VsanVmdkLoadTestSpec', 'vim.version.version9', 0), ('actualDurationSec', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('totalBytes', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('iops', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('tputBps', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('avgLatencyUs', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('maxLatencyUs', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('numIoAboveLatencyThreshold', 'long', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterVMsHealthOverallResult', 'VsanClusterVMsHealthOverAllResult', 'vmodl.DynamicData', 'vim.version.version9', [('healthStateList', 'vim.cluster.VsanClusterVMsHealthSummaryResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('overallHealthState', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanHostHealthSystemStatusResult', 'VsanHostHealthSystemStatusResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostname', 'string', 'vim.version.version9', 0), ('status', 'string', 'vim.version.version9', 0), ('issues', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterAdvCfgSyncResult', 'VsanClusterAdvCfgSyncResult', 'vmodl.DynamicData', 'vim.version.version9', [('inSync', 'boolean', 'vim.version.version9', 0), ('name', 'string', 'vim.version.version9', 0), ('hostValues', 'vim.cluster.VsanClusterAdvCfgSyncHostResult[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanQueryResultHostInfo', 'VsanQueryResultHostInfo', 'vmodl.DynamicData', 'vim.version.version9', [('uuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('hostnameInCmmds', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('vsanIpv4Addresses', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.host.DiskMapInfoEx', 'VimVsanHostDiskMapInfoEx', 'vmodl.DynamicData', 'vim.version.version10', [('mapping', 'vim.vsan.host.DiskMapping', 'vim.version.version10', 0), ('isMounted', 'boolean', 'vim.version.version10', 0), ('isAllFlash', 'boolean', 'vim.version.version10', 0), ('isDataEfficiency', 'boolean', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanVmdkLoadTestSpec', 'VsanVmdkLoadTestSpec', 'vmodl.DynamicData', 'vim.version.version9', [('vmdkCreateSpec', 'vim.VirtualDiskManager.FileBackedVirtualDiskSpec', 'vim.version.version9', 0 | F_OPTIONAL), ('vmdkIOSpec', 'vim.host.VsanVmdkIOLoadSpec', 'vim.version.version9', 0 | F_OPTIONAL), ('vmdkIOSpecSequence', 'vim.host.VsanVmdkIOLoadSpec[]', 'vim.version.version9', 0 | F_OPTIONAL), ('stepDurationSec', 'long', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHealthSummary', 'VsanClusterHealthSummary', 'vmodl.DynamicData', 'vim.version.version9', [('clusterStatus', 'vim.cluster.VsanClusterHealthSystemStatusResult', 'vim.version.version9', 0 | F_OPTIONAL), ('timestamp', 'vmodl.DateTime', 'vim.version.version9', 0 | F_OPTIONAL), ('clusterVersions', 'vim.cluster.VsanClusterHealthSystemVersionResult', 'vim.version.version9', 0 | F_OPTIONAL), ('objectHealth', 'vim.host.VsanObjectOverallHealth', 'vim.version.version9', 0 | F_OPTIONAL), ('vmHealth', 'vim.cluster.VsanClusterVMsHealthOverallResult', 'vim.version.version9', 0 | F_OPTIONAL), ('networkHealth', 'vim.cluster.VsanClusterNetworkHealthResult', 'vim.version.version9', 0 | F_OPTIONAL), ('limitHealth', 'vim.cluster.VsanClusterLimitHealthResult', 'vim.version.version9', 0 | F_OPTIONAL), ('advCfgSync', 'vim.cluster.VsanClusterAdvCfgSyncResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('createVmHealth', 'vim.cluster.VsanHostCreateVmHealthTestResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('physicalDisksHealth', 'vim.host.VsanPhysicalDiskHealthSummary[]', 'vim.version.version9', 0 | F_OPTIONAL), ('hclInfo', 'vim.cluster.VsanClusterHclInfo', 'vim.version.version9', 0 | F_OPTIONAL), ('groups', 'vim.cluster.VsanClusterHealthGroup[]', 'vim.version.version9', 0 | F_OPTIONAL), ('overallHealth', 'string', 'vim.version.version9', 0), ('overallHealthDescription', 'string', 'vim.version.version9', 0), ('clomdLiveness', 'vim.cluster.VsanClusterClomdLivenessResult', 'vim.version.version9', 0 | F_OPTIONAL), ('diskBalance', 'vim.cluster.VsanClusterBalanceSummary', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanPerfEntityType', 'VsanPerfEntityType', 'vmodl.DynamicData', 'vim.version.version9', [('name', 'string', 'vim.version.version9', 0), ('id', 'string', 'vim.version.version9', 0), ('graphs', 'vim.cluster.VsanPerfGraph[]', 'vim.version.version9', 0), ('description', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanNetworkLoadTestResult', 'VsanNetworkLoadTestResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostname', 'string', 'vim.version.version9', 0), ('status', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('client', 'boolean', 'vim.version.version9', 0), ('bandwidthBps', 'long', 'vim.version.version9', 0), ('totalBytes', 'long', 'vim.version.version9', 0), ('lostDatagrams', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('lossPct', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('sentDatagrams', 'long', 'vim.version.version9', 0 | F_OPTIONAL), ('jitterMs', 'float', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanPhysicalDiskHealthSummary', 'VsanPhysicalDiskHealthSummary', 'vmodl.DynamicData', 'vim.version.version9', [('overallHealth', 'string', 'vim.version.version9', 0), ('heapsWithIssues', 'vim.host.VsanResourceHealth[]', 'vim.version.version9', 0 | F_OPTIONAL), ('slabsWithIssues', 'vim.host.VsanResourceHealth[]', 'vim.version.version9', 0 | F_OPTIONAL), ('disks', 'vim.host.VsanPhysicalDiskHealth[]', 'vim.version.version9', 0 | F_OPTIONAL), ('componentsWithIssues', 'vim.host.VsanResourceHealth[]', 'vim.version.version9', 0 | F_OPTIONAL), ('hostname', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('hostDedupScope', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('error', 'vmodl.MethodFault', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.host.VsanDiskManagementSystemCapability', 'VimVsanHostVsanDiskManagementSystemCapability', 'vmodl.DynamicData', 'vim.version.version10', [('version', 'string', 'vim.version.version10', 0)]) +CreateDataType('vim.host.VsanHostCimProviderInfo', 'VsanHostCimProviderInfo', 'vmodl.DynamicData', 'vim.version.version9', [('cimProviderSupported', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('installedCIMProvider', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('cimProviderOnHcl', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanObjectInformation', 'VsanObjectInformation', 'vmodl.DynamicData', 'vim.version.version9', [('directoryName', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('vsanObjectUuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('vsanHealth', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('policyAttributes', 'vim.KeyValue[]', 'vim.version.version9', 0 | F_OPTIONAL), ('spbmProfileUuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('spbmProfileGenerationId', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('spbmComplianceResult', 'vim.cluster.StorageComplianceResult', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanObjectIdentity', 'VsanObjectIdentity', 'vmodl.DynamicData', 'vim.version.version9', [('uuid', 'string', 'vim.version.version9', 0), ('type', 'string', 'vim.version.version9', 0), ('vmInstanceUuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('vmNsObjectUuid', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('vm', 'vim.VirtualMachine', 'vim.version.version9', 0 | F_OPTIONAL), ('description', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanResourceHealth', 'VsanResourceHealth', 'vmodl.DynamicData', 'vim.version.version9', [('resource', 'string', 'vim.version.version9', 0), ('health', 'string', 'vim.version.version9', 0), ('description', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanCapability', 'VsanCapability', 'vmodl.DynamicData', 'vim.version.version10', [('target', 'vmodl.ManagedObject', 'vim.version.version10', 0 | F_OPTIONAL), ('capabilities', 'string[]', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanHostClomdLivenessResult', 'VsanHostClomdLivenessResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostname', 'string', 'vim.version.version9', 0), ('clomdStat', 'string', 'vim.version.version9', 0), ('error', 'vmodl.MethodFault', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanObjectQuerySpec', 'VsanObjectQuerySpec', 'vmodl.DynamicData', 'vim.version.version9', [('uuid', 'string', 'vim.version.version9', 0), ('spbmProfileGenerationId', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterLimitHealthResult', 'VsanClusterLimitHealthResult', 'vmodl.DynamicData', 'vim.version.version9', [('issueFound', 'boolean', 'vim.version.version9', 0), ('componentLimitHealth', 'string', 'vim.version.version9', 0), ('diskFreeSpaceHealth', 'string', 'vim.version.version9', 0), ('rcFreeReservationHealth', 'string', 'vim.version.version9', 0), ('hostResults', 'vim.host.VsanLimitHealthResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('whatifHostFailures', 'vim.cluster.VsanClusterWhatifHostFailuresResult[]', 'vim.version.version9', 0 | F_OPTIONAL), ('hostsCommFailure', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanStorageWorkloadType', 'VsanStorageWorkloadType', 'vmodl.DynamicData', 'vim.version.version9', [('specs', 'vim.host.VsanVmdkLoadTestSpec[]', 'vim.version.version9', 0), ('typeId', 'string', 'vim.version.version9', 0), ('name', 'string', 'vim.version.version9', 0), ('description', 'string', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanClusterAdvCfgSyncHostResult', 'VsanClusterAdvCfgSyncHostResult', 'vmodl.DynamicData', 'vim.version.version9', [('hostname', 'string', 'vim.version.version9', 0), ('value', 'string', 'vim.version.version9', 0)]) +CreateDataType('vim.vsan.upgradesystem.ObjectPolicyIssue', 'VsanObjectPolicyIssue', 'vim.VsanUpgradeSystem.PreflightCheckIssue', 'vim.version.version10', [('uuids', 'string[]', 'vim.version.version10', 0)]) +CreateDataType('vim.cluster.VsanPerfTopEntities', 'VsanPerfTopEntities', 'vmodl.DynamicData', 'vim.version.version9', [('metricId', 'vim.cluster.VsanPerfMetricId', 'vim.version.version9', 0), ('entities', 'vim.cluster.VsanPerfTopEntity[]', 'vim.version.version9', 0)]) +CreateDataType('vim.host.VsanProactiveRebalanceInfoEx', 'VsanProactiveRebalanceInfoEx', 'vmodl.DynamicData', 'vim.version.version9', [('running', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL), ('startTs', 'vmodl.DateTime', 'vim.version.version9', 0 | F_OPTIONAL), ('stopTs', 'vmodl.DateTime', 'vim.version.version9', 0 | F_OPTIONAL), ('varianceThreshold', 'float', 'vim.version.version9', 0 | F_OPTIONAL), ('timeThreshold', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('rateThreshold', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('hostname', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('error', 'vmodl.MethodFault', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterProactiveTestResult', 'VsanClusterProactiveTestResult', 'vmodl.DynamicData', 'vim.version.version9', [('overallStatus', 'string', 'vim.version.version9', 0), ('overallStatusDescription', 'string', 'vim.version.version9', 0), ('timestamp', 'vmodl.DateTime', 'vim.version.version9', 0), ('healthTest', 'vim.cluster.VsanClusterHealthTest', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VSANCmmdsPreferredFaultDomainInfo', 'VimHostVSANCmmdsPreferredFaultDomainInfo', 'vmodl.DynamicData', 'vim.version.version10', [('preferredFaultDomainId', 'string', 'vim.version.version10', 0), ('preferredFaultDomainName', 'string', 'vim.version.version10', 0)]) +CreateDataType('vim.cluster.VsanFaultDomainsConfigSpec', 'VimClusterVsanFaultDomainsConfigSpec', 'vmodl.DynamicData', 'vim.version.version10', [('faultDomains', 'vim.cluster.VsanFaultDomainSpec[]', 'vim.version.version10', 0), ('witness', 'vim.cluster.VsanWitnessSpec', 'vim.version.version10', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterHostVmknicMapping', 'VsanClusterHostVmknicMapping', 'vmodl.DynamicData', 'vim.version.version9', [('host', 'string', 'vim.version.version9', 0), ('vmknic', 'string', 'vim.version.version9', 0)]) +CreateDataType('vim.cluster.VsanClusterVmdkLoadTestResult', 'VsanClusterVmdkLoadTestResult', 'vmodl.DynamicData', 'vim.version.version9', [('task', 'vim.Task', 'vim.version.version9', 0 | F_OPTIONAL), ('clusterResult', 'vim.cluster.VsanClusterProactiveTestResult', 'vim.version.version9', 0 | F_OPTIONAL), ('hostResults', 'vim.host.VsanHostVmdkLoadTestResult[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterVMsHealthSummaryResult', 'VsanClusterVMsHealthSummaryResult', 'vmodl.DynamicData', 'vim.version.version9', [('numVMs', 'int', 'vim.version.version9', 0), ('state', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('health', 'string', 'vim.version.version9', 0), ('vmInstanceUuids', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VSANStretchedClusterHostCapability', 'VimHostVSANStretchedClusterHostCapability', 'vmodl.DynamicData', 'vim.version.version10', [('featureVersion', 'string', 'vim.version.version10', 0)]) +CreateDataType('vim.host.VsanFailedRepairObjectResult', 'VsanFailedRepairObjectResult', 'vmodl.DynamicData', 'vim.version.version9', [('uuid', 'string', 'vim.version.version9', 0), ('errMessage', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterCreateVmHealthTestResult', 'VsanClusterCreateVmHealthTestResult', 'vmodl.DynamicData', 'vim.version.version9', [('clusterResult', 'vim.cluster.VsanClusterProactiveTestResult', 'vim.version.version9', 0), ('hostResults', 'vim.cluster.VsanHostCreateVmHealthTestResult[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanObjectHealth', 'VsanObjectHealth', 'vmodl.DynamicData', 'vim.version.version9', [('numObjects', 'int', 'vim.version.version9', 0), ('health', 'vim.host.VsanObjectHealthState', 'vim.version.version9', 0), ('objUuids', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterBalanceSummary', 'VsanClusterBalanceSummary', 'vmodl.DynamicData', 'vim.version.version9', [('varianceThreshold', 'long', 'vim.version.version9', 0), ('disks', 'vim.cluster.VsanClusterBalancePerDiskInfo[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterTelemetryProxyConfig', 'VsanClusterTelemetryProxyConfig', 'vmodl.DynamicData', 'vim.version.version9', [('host', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('port', 'int', 'vim.version.version9', 0 | F_OPTIONAL), ('user', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('password', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('autoDiscovered', 'boolean', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanVmdkIOLoadSpec', 'VsanVmdkIOLoadSpec', 'vmodl.DynamicData', 'vim.version.version9', [('readPct', 'int', 'vim.version.version9', 0), ('oio', 'int', 'vim.version.version9', 0), ('iosizeB', 'int', 'vim.version.version9', 0), ('dataSizeMb', 'long', 'vim.version.version9', 0), ('random', 'boolean', 'vim.version.version9', 0), ('startOffsetB', 'long', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.host.VsanVsanPcapResult', 'VsanVsanPcapResult', 'vmodl.DynamicData', 'vim.version.version9', [('calltime', 'float', 'vim.version.version9', 0), ('vmknic', 'string', 'vim.version.version9', 0), ('tcpdumpFilter', 'string', 'vim.version.version9', 0), ('snaplen', 'int', 'vim.version.version9', 0), ('pkts', 'string[]', 'vim.version.version9', 0 | F_OPTIONAL), ('pcap', 'string', 'vim.version.version9', 0 | F_OPTIONAL), ('error', 'vmodl.MethodFault', 'vim.version.version9', 0 | F_OPTIONAL), ('hostname', 'string', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.cluster.VsanClusterNetworkLoadTestResult', 'VsanClusterNetworkLoadTestResult', 'vmodl.DynamicData', 'vim.version.version9', [('clusterResult', 'vim.cluster.VsanClusterProactiveTestResult', 'vim.version.version9', 0), ('hostResults', 'vim.host.VsanNetworkLoadTestResult[]', 'vim.version.version9', 0 | F_OPTIONAL)]) +CreateDataType('vim.vsan.upgradesystem.HostPropertyRetrieveIssue', 'VsanHostPropertyRetrieveIssue', 'vim.VsanUpgradeSystem.PreflightCheckIssue', 'vim.version.version10', [('hosts', 'vim.HostSystem[]', 'vim.version.version10', 0)]) +CreateEnumType('vim.host.VsanObjectHealthState', 'VsanObjectHealthState', 'vim.version.version9', ['inaccessible' ,'reducedavailabilitywithnorebuild' ,'reducedavailabilitywithnorebuilddelaytimer' ,'reducedavailabilitywithactiverebuild' ,'datamove' ,'nonavailabilityrelatedreconfig' ,'nonavailabilityrelatedincompliance' ,'healthy' ,]) +CreateEnumType('vim.cluster.VsanObjectTypeEnum', 'VsanObjectTypeEnum', 'vim.version.version9', ['vmswap' ,'vdisk' ,'namespace' ,'vmem' ,'statsdb' ,'iscsi' ,'other' ,'fileSystemOverhead' ,'dedupOverhead' ,'checksumOverhead' ,]) +CreateEnumType('vim.cluster.VsanCapabilityType', 'VsanCapabilityType', 'vim.version.version10', ['capability' ,'allflash' ,'stretchedcluster' ,'dataefficiency' ,'clusterconfig' ,'upgrade' ,'objectidentities' ,]) +CreateEnumType('vim.cluster.VsanHealthLogLevelEnum', 'VsanHealthLogLevelEnum', 'vim.version.version9', ['INFO' ,'WARNING' ,'ERROR' ,'DEBUG' ,'CRITICAL' ,]) +CreateEnumType('vim.cluster.VsanPerfSummaryType', 'VsanPerfSummaryType', 'vim.version.version9', ['average' ,'maximum' ,'minimum' ,'latest' ,'summation' ,'none' ,]) +CreateEnumType('vim.cluster.StorageComplianceStatus', 'VsanStorageComplianceStatus', 'vim.version.version9', ['compliant' ,'nonCompliant' ,'unknown' ,'notApplicable' ,]) +CreateEnumType('vim.cluster.VsanPerfStatsUnitType', 'VsanPerfStatsUnitType', 'vim.version.version9', ['number' ,'time_ms' ,'percentage' ,'size_bytes' ,'rate_bytes' ,]) +CreateEnumType('vim.cluster.VsanPerfThresholdDirectionType', 'VsanPerfThresholdDirectionType', 'vim.version.version9', ['upper' ,'lower' ,]) +CreateEnumType('vim.cluster.VsanPerfStatsType', 'VsanPerfStatsType', 'vim.version.version9', ['absolute' ,'delta' ,'rate' ,]) +CreateEnumType('vim.vsan.host.DiskMappingCreationType', 'VimVsanHostDiskMappingCreationType', 'vim.version.version10', ['hybrid' ,'allFlash' ,]) +CreateEnumType('vim.cluster.VsanClusterHealthActionIdEnum', 'VsanClusterHealthActionIdEnum', 'vim.version.version9', ['RepairClusterObjectsAction' ,'UploadHclDb' ,'UpdateHclDbFromInternet' ,'EnableHealthService' ,'DiskBalance' ,'StopDiskBalance' ,'RemediateDedup' ,'UpgradeVsanDiskFormat' ,]) +CreateEnumType('vim.cluster.VsanDiskGroupCreationType', 'VimClusterVsanDiskGroupCreationType', 'vim.version.version10', ['allflash' ,'hybrid' ,]) diff --git a/salt/fileclient.py b/salt/fileclient.py index 0c0050ebc0..35c63b2cb1 100644 --- a/salt/fileclient.py +++ b/salt/fileclient.py @@ -185,12 +185,13 @@ class Client(object): ''' raise NotImplementedError - def cache_file(self, path, saltenv=u'base', cachedir=None): + def cache_file(self, path, saltenv=u'base', cachedir=None, source_hash=None): ''' Pull a file down from the file server and store it in the minion file cache ''' - return self.get_url(path, u'', True, saltenv, cachedir=cachedir) + return self.get_url( + path, u'', True, saltenv, cachedir=cachedir, source_hash=source_hash) def cache_files(self, paths, saltenv=u'base', cachedir=None): ''' @@ -470,7 +471,7 @@ class Client(object): return ret def get_url(self, url, dest, makedirs=False, saltenv=u'base', - no_cache=False, cachedir=None): + no_cache=False, cachedir=None, source_hash=None): ''' Get a single file from a URL. ''' @@ -525,6 +526,18 @@ class Client(object): return u'' elif not no_cache: dest = self._extrn_path(url, saltenv, cachedir=cachedir) + if source_hash is not None: + try: + source_hash = source_hash.split('=')[-1] + form = salt.utils.files.HASHES_REVMAP[len(source_hash)] + if salt.utils.get_hash(dest, form) == source_hash: + log.debug( + 'Cached copy of %s (%s) matches source_hash %s, ' + 'skipping download', url, dest, source_hash + ) + return dest + except (AttributeError, KeyError, IOError, OSError): + pass destdir = os.path.dirname(dest) if not os.path.isdir(destdir): os.makedirs(destdir) @@ -532,7 +545,9 @@ class Client(object): if url_data.scheme == u's3': try: def s3_opt(key, default=None): - u'''Get value of s3. from Minion config or from Pillar''' + ''' + Get value of s3. from Minion config or from Pillar + ''' if u's3.' + key in self.opts: return self.opts[u's3.' + key] try: @@ -744,12 +759,7 @@ class Client(object): Cache a file then process it as a template ''' if u'env' in kwargs: - salt.utils.versions.warn_until( - u'Oxygen', - u'Parameter \'env\' has been detected in the argument list. This ' - u'parameter is no longer used and has been replaced by \'saltenv\' ' - u'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop(u'env') kwargs[u'saltenv'] = saltenv @@ -790,7 +800,7 @@ class Client(object): def _extrn_path(self, url, saltenv, cachedir=None): ''' - Return the extn_filepath for a given url + Return the extrn_filepath for a given url ''' url_data = urlparse(url) if salt.utils.platform.is_windows(): @@ -1300,10 +1310,10 @@ class RemoteClient(Client): hash_type = self.opts.get(u'hash_type', u'md5') ret[u'hsum'] = salt.utils.get_hash(path, form=hash_type) ret[u'hash_type'] = hash_type - return ret, list(os.stat(path)) + return ret load = {u'path': path, u'saltenv': saltenv, - u'cmd': u'_file_hash_and_stat'} + u'cmd': u'_file_hash'} return self.channel.send(load) def hash_file(self, path, saltenv=u'base'): @@ -1312,14 +1322,33 @@ class RemoteClient(Client): master file server prepend the path with salt:// otherwise, prepend the file with / for a local file. ''' - return self.__hash_and_stat_file(path, saltenv)[0] + return self.__hash_and_stat_file(path, saltenv) def hash_and_stat_file(self, path, saltenv=u'base'): ''' The same as hash_file, but also return the file's mode, or None if no mode data is present. ''' - return self.__hash_and_stat_file(path, saltenv) + hash_result = self.hash_file(path, saltenv) + try: + path = self._check_proto(path) + except MinionError as err: + if not os.path.isfile(path): + return hash_result, None + else: + try: + return hash_result, list(os.stat(path)) + except Exception: + return hash_result, None + load = {'path': path, + 'saltenv': saltenv, + 'cmd': '_file_find'} + fnd = self.channel.send(load) + try: + stat_result = fnd.get('stat') + except AttributeError: + stat_result = None + return hash_result, stat_result def list_env(self, saltenv=u'base'): ''' diff --git a/salt/fileserver/__init__.py b/salt/fileserver/__init__.py index c3f046fc98..9ba52e6e15 100644 --- a/salt/fileserver/__init__.py +++ b/salt/fileserver/__init__.py @@ -553,12 +553,7 @@ class Fileserver(object): kwargs[args[0]] = args[1] if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') if 'saltenv' in kwargs: saltenv = kwargs.pop('saltenv') @@ -583,12 +578,7 @@ class Fileserver(object): 'dest': ''} if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if 'path' not in load or 'loc' not in load or 'saltenv' not in load: @@ -609,13 +599,7 @@ class Fileserver(object): Common code for hashing and stating files ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. ' - 'This parameter is no longer used and has been replaced by ' - '\'saltenv\' as of Salt 2016.11.0. This warning will be removed ' - 'in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if 'path' not in load or 'saltenv' not in load: @@ -656,12 +640,7 @@ class Fileserver(object): Deletes the file_lists cache files ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') saltenv = load.get('saltenv', []) @@ -738,12 +717,7 @@ class Fileserver(object): Return a list of files from the dominant environment ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = set() @@ -769,12 +743,7 @@ class Fileserver(object): List all emptydirs in the given environment ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = set() @@ -800,12 +769,7 @@ class Fileserver(object): List all directories in the given environment ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = set() @@ -831,12 +795,7 @@ class Fileserver(object): Return a list of symlinked files and dirs ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = {} diff --git a/salt/fileserver/hgfs.py b/salt/fileserver/hgfs.py index bf7c82dcd8..e386a9e6e8 100644 --- a/salt/fileserver/hgfs.py +++ b/salt/fileserver/hgfs.py @@ -736,12 +736,7 @@ def serve_file(load, fnd): Return a chunk from a file based on the data received ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = {'data': '', @@ -770,12 +765,7 @@ def file_hash(load, fnd): Return a file hash, the hash type is set in the master config file ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if not all(x in load for x in ('path', 'saltenv')): @@ -804,12 +794,7 @@ def _file_lists(load, form): Return a dict containing the file lists for files and dirs ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') list_cachedir = os.path.join(__opts__['cachedir'], 'file_lists/hgfs') @@ -852,12 +837,7 @@ def _get_file_list(load): Get a list of all files on the file server in a specified environment ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if 'saltenv' not in load or load['saltenv'] not in envs(): @@ -897,12 +877,7 @@ def _get_dir_list(load): Get a list of all directories on the master ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if 'saltenv' not in load or load['saltenv'] not in envs(): diff --git a/salt/fileserver/minionfs.py b/salt/fileserver/minionfs.py index 52a8fd0616..292bf0f85e 100644 --- a/salt/fileserver/minionfs.py +++ b/salt/fileserver/minionfs.py @@ -165,12 +165,7 @@ def file_hash(load, fnd): ret = {} if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if load['saltenv'] not in envs(): @@ -235,12 +230,7 @@ def file_list(load): Return a list of all files on the file server in a specified environment ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if load['saltenv'] not in envs(): @@ -319,12 +309,7 @@ def dir_list(load): - source-minion/absolute/path ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if load['saltenv'] not in envs(): diff --git a/salt/fileserver/roots.py b/salt/fileserver/roots.py index 9aefb46e25..c795163b9d 100644 --- a/salt/fileserver/roots.py +++ b/salt/fileserver/roots.py @@ -40,12 +40,7 @@ def find_file(path, saltenv='base', **kwargs): Search the environment for the relative path. ''' if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') path = os.path.normpath(path) @@ -117,12 +112,7 @@ def serve_file(load, fnd): Return a chunk from a file based on the data received ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = {'data': '', @@ -218,12 +208,7 @@ def file_hash(load, fnd): Return a file hash, the hash type is set in the master config file ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if 'path' not in load or 'saltenv' not in load: @@ -298,12 +283,7 @@ def _file_lists(load, form): Return a dict containing the file lists for files, dirs, emtydirs and symlinks ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if load['saltenv'] not in __opts__['file_roots']: @@ -444,12 +424,7 @@ def symlink_list(load): Return a dict of all symlinks based on a given path on the Master ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = {} diff --git a/salt/fileserver/s3fs.py b/salt/fileserver/s3fs.py index 7f262aa3bb..04f0b5e51c 100644 --- a/salt/fileserver/s3fs.py +++ b/salt/fileserver/s3fs.py @@ -126,12 +126,7 @@ def find_file(path, saltenv='base', **kwargs): is missing, or if the MD5 does not match. ''' if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') fnd = {'bucket': None, @@ -168,12 +163,7 @@ def file_hash(load, fnd): Return an MD5 file hash ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = {} @@ -201,12 +191,7 @@ def serve_file(load, fnd): Return a chunk from a file based on the data received ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = {'data': '', @@ -245,12 +230,7 @@ def file_list(load): Return a list of all files on the file server in a specified environment ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = [] @@ -286,12 +266,7 @@ def dir_list(load): Return a list of all directories on the master ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = [] diff --git a/salt/fileserver/svnfs.py b/salt/fileserver/svnfs.py index ac7681ec23..2ba3cc227e 100644 --- a/salt/fileserver/svnfs.py +++ b/salt/fileserver/svnfs.py @@ -631,12 +631,7 @@ def serve_file(load, fnd): Return a chunk from a file based on the data received ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = {'data': '', @@ -665,12 +660,7 @@ def file_hash(load, fnd): Return a file hash, the hash type is set in the master config file ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if not all(x in load for x in ('path', 'saltenv')): @@ -723,12 +713,7 @@ def _file_lists(load, form): Return a dict containing the file lists for files, dirs, emptydirs and symlinks ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if 'saltenv' not in load or load['saltenv'] not in envs(): diff --git a/salt/grains/cimc.py b/salt/grains/cimc.py new file mode 100644 index 0000000000..e1fba64947 --- /dev/null +++ b/salt/grains/cimc.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +''' +Generate baseline proxy minion grains for cimc hosts. + +''' + +# Import Python Libs +from __future__ import absolute_import +import logging + +# Import Salt Libs +import salt.utils.platform +import salt.proxy.cimc + +__proxyenabled__ = ['cimc'] +__virtualname__ = 'cimc' + +log = logging.getLogger(__file__) + +GRAINS_CACHE = {'os_family': 'Cisco UCS'} + + +def __virtual__(): + try: + if salt.utils.platform.is_proxy() and __opts__['proxy']['proxytype'] == 'cimc': + return __virtualname__ + except KeyError: + pass + + return False + + +def cimc(proxy=None): + if not proxy: + return {} + if proxy['cimc.initialized']() is False: + return {} + return {'cimc': proxy['cimc.grains']()} diff --git a/salt/grains/core.py b/salt/grains/core.py index 56ec468ed2..57142ded3f 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -1205,6 +1205,10 @@ _OS_FAMILY_MAP = { 'Raspbian': 'Debian', 'Devuan': 'Debian', 'antiX': 'Debian', + 'Kali': 'Debian', + 'neon': 'Debian', + 'Cumulus': 'Debian', + 'Deepin': 'Debian', 'NILinuxRT': 'NILinuxRT', 'NILinuxRT-XFCE': 'NILinuxRT', 'KDE neon': 'Debian', @@ -2425,4 +2429,46 @@ def get_master(): # master return {'master': __opts__.get('master', '')} -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 + +def default_gateway(): + ''' + Populates grains which describe whether a server has a default gateway + configured or not. Uses `ip -4 route show` and `ip -6 route show` and greps + for a `default` at the beginning of any line. Assuming the standard + `default via ` format for default gateways, it will also parse out the + ip address of the default gateway, and put it in ip4_gw or ip6_gw. + + If the `ip` command is unavailable, no grains will be populated. + + Currently does not support multiple default gateways. The grains will be + set to the first default gateway found. + + List of grains: + + ip4_gw: True # ip/True/False if default ipv4 gateway + ip6_gw: True # ip/True/False if default ipv6 gateway + ip_gw: True # True if either of the above is True, False otherwise + ''' + grains = {} + if not salt.utils.path.which('ip'): + return {} + grains['ip_gw'] = False + grains['ip4_gw'] = False + grains['ip6_gw'] = False + if __salt__['cmd.run']('ip -4 route show | grep "^default"', python_shell=True): + grains['ip_gw'] = True + grains['ip4_gw'] = True + try: + gateway_ip = __salt__['cmd.run']('ip -4 route show | grep "^default via"', python_shell=True).split(' ')[2].strip() + grains['ip4_gw'] = gateway_ip if gateway_ip else True + except Exception as exc: + pass + if __salt__['cmd.run']('ip -6 route show | grep "^default"', python_shell=True): + grains['ip_gw'] = True + grains['ip6_gw'] = True + try: + gateway_ip = __salt__['cmd.run']('ip -6 route show | grep "^default via"', python_shell=True).split(' ')[2].strip() + grains['ip6_gw'] = gateway_ip if gateway_ip else True + except Exception as exc: + pass + return grains diff --git a/salt/grains/extra.py b/salt/grains/extra.py index 8db1b016b8..e89386ba21 100644 --- a/salt/grains/extra.py +++ b/salt/grains/extra.py @@ -11,6 +11,7 @@ import logging # Import salt libs import salt.utils.files +import salt.utils.platform log = logging.getLogger(__name__) @@ -21,7 +22,14 @@ def shell(): ''' # Provides: # shell - return {'shell': os.environ.get('SHELL', '/bin/sh')} + if salt.utils.platform.is_windows(): + env_var = 'COMSPEC' + default = r'C:\Windows\system32\cmd.exe' + else: + env_var = 'SHELL' + default = '/bin/sh' + + return {'shell': os.environ.get(env_var, default)} def config(): diff --git a/salt/grains/metadata.py b/salt/grains/metadata.py index b878463b11..15ed571a76 100644 --- a/salt/grains/metadata.py +++ b/salt/grains/metadata.py @@ -17,6 +17,7 @@ metadata server set `metadata_server_grains: True`. from __future__ import absolute_import # Import python libs +import json import os import socket @@ -47,16 +48,30 @@ def _search(prefix="latest/"): Recursively look up all grains in the metadata server ''' ret = {} - for line in http.query(os.path.join(HOST, prefix))['body'].split('\n'): + linedata = http.query(os.path.join(HOST, prefix)) + if 'body' not in linedata: + return ret + for line in linedata['body'].split('\n'): if line.endswith('/'): ret[line[:-1]] = _search(prefix=os.path.join(prefix, line)) + elif prefix == 'latest/': + # (gtmanfred) The first level should have a forward slash since + # they have stuff underneath. This will not be doubled up though, + # because lines ending with a slash are checked first. + ret[line] = _search(prefix=os.path.join(prefix, line + '/')) elif line.endswith(('dynamic', 'meta-data')): ret[line] = _search(prefix=os.path.join(prefix, line)) elif '=' in line: key, value = line.split('=') ret[value] = _search(prefix=os.path.join(prefix, key)) else: - ret[line] = http.query(os.path.join(HOST, prefix, line))['body'] + retdata = http.query(os.path.join(HOST, prefix, line)).get('body', None) + # (gtmanfred) This try except block is slightly faster than + # checking if the string starts with a curly brace + try: + ret[line] = json.loads(retdata) + except ValueError: + ret[line] = retdata return ret diff --git a/salt/grains/napalm.py b/salt/grains/napalm.py index 6ac52464c8..fcfbdcfe9f 100644 --- a/salt/grains/napalm.py +++ b/salt/grains/napalm.py @@ -447,8 +447,8 @@ def optional_args(proxy=None): device2: True ''' - opt_args = _get_device_grain('optional_args', proxy=proxy) - if _FORBIDDEN_OPT_ARGS: + opt_args = _get_device_grain('optional_args', proxy=proxy) or {} + if opt_args and _FORBIDDEN_OPT_ARGS: for arg in _FORBIDDEN_OPT_ARGS: opt_args.pop(arg, None) return {'optional_args': opt_args} diff --git a/salt/key.py b/salt/key.py index 98f58b5c59..52b2d268e1 100644 --- a/salt/key.py +++ b/salt/key.py @@ -496,7 +496,7 @@ class Key(object): minions = [] for key, val in six.iteritems(keys): minions.extend(val) - if not self.opts.get(u'preserve_minion_cache', False) or not preserve_minions: + if not self.opts.get(u'preserve_minion_cache', False): m_cache = os.path.join(self.opts[u'cachedir'], self.ACC) if os.path.isdir(m_cache): for minion in os.listdir(m_cache): @@ -743,7 +743,7 @@ class Key(object): def delete_key(self, match=None, match_dict=None, - preserve_minions=False, + preserve_minions=None, revoke_auth=False): ''' Delete public keys. If "match" is passed, it is evaluated as a glob. @@ -781,11 +781,10 @@ class Key(object): salt.utils.event.tagify(prefix=u'key')) except (OSError, IOError): pass - if preserve_minions: - preserve_minions_list = matches.get(u'minions', []) + if self.opts.get(u'preserve_minions') is True: + self.check_minion_cache(preserve_minions=matches.get(u'minions', [])) else: - preserve_minions_list = [] - self.check_minion_cache(preserve_minions=preserve_minions_list) + self.check_minion_cache() if self.opts.get(u'rotate_aes_key'): salt.crypt.dropfile(self.opts[u'cachedir'], self.opts[u'user']) return ( @@ -976,16 +975,17 @@ class RaetKey(Key): minions.extend(val) m_cache = os.path.join(self.opts[u'cachedir'], u'minions') - if os.path.isdir(m_cache): - for minion in os.listdir(m_cache): - if minion not in minions: - shutil.rmtree(os.path.join(m_cache, minion)) - cache = salt.cache.factory(self.opts) - clist = cache.list(self.ACC) - if clist: - for minion in clist: + if not self.opts.get('preserve_minion_cache', False): + if os.path.isdir(m_cache): + for minion in os.listdir(m_cache): if minion not in minions and minion not in preserve_minions: - cache.flush(u'{0}/{1}'.format(self.ACC, minion)) + shutil.rmtree(os.path.join(m_cache, minion)) + cache = salt.cache.factory(self.opts) + clist = cache.list(self.ACC) + if clist: + for minion in clist: + if minion not in minions and minion not in preserve_minions: + cache.flush(u'{0}/{1}'.format(self.ACC, minion)) kind = self.opts.get(u'__role', u'') # application kind if kind not in kinds.APPL_KINDS: @@ -1227,7 +1227,7 @@ class RaetKey(Key): def delete_key(self, match=None, match_dict=None, - preserve_minions=False, + preserve_minions=None, revoke_auth=False): ''' Delete public keys. If "match" is passed, it is evaluated as a glob. @@ -1258,7 +1258,10 @@ class RaetKey(Key): os.remove(os.path.join(self.opts[u'pki_dir'], status, key)) except (OSError, IOError): pass - self.check_minion_cache(preserve_minions=matches.get(u'minions', [])) + if self.opts.get('preserve_minions') is True: + self.check_minion_cache(preserve_minions=matches.get(u'minions', [])) + else: + self.check_minion_cache() return ( self.name_match(match) if match is not None else self.dict_match(matches) diff --git a/salt/loader.py b/salt/loader.py index 9399437fee..81e44ebb27 100644 --- a/salt/loader.py +++ b/salt/loader.py @@ -270,7 +270,7 @@ def raw_mod(opts, name, functions, mod=u'modules'): testmod['test.ping']() ''' loader = LazyLoader( - _module_dirs(opts, mod, u'rawmodule'), + _module_dirs(opts, mod, u'module'), opts, tag=u'rawmodule', virtual_enable=False, diff --git a/salt/log/handlers/fluent_mod.py b/salt/log/handlers/fluent_mod.py index dc4103d141..88b3e2cfdb 100644 --- a/salt/log/handlers/fluent_mod.py +++ b/salt/log/handlers/fluent_mod.py @@ -11,7 +11,18 @@ Fluent Logging Handler ------------------- - In the salt configuration file: + In the `fluent` configuration file: + + .. code-block:: text + + + type forward + bind localhost + port 24224 + + + Then, to send logs via fluent in Logstash format, add the + following to the salt (master and/or minion) configuration file: .. code-block:: yaml @@ -19,14 +30,32 @@ host: localhost port: 24224 - In the `fluent`_ configuration file: + To send logs via fluent in the Graylog raw json format, add the + following to the salt (master and/or minion) configuration file: - .. code-block:: text + .. code-block:: yaml - - type forward - port 24224 - + fluent_handler: + host: localhost + port: 24224 + payload_type: graylog + tags: + - salt_master.SALT + + The above also illustrates the `tags` option, which allows + one to set descriptive (or useful) tags on records being + sent. If not provided, this defaults to the single tag: + 'salt'. Also note that, via Graylog "magic", the 'facility' + of the logged message is set to 'SALT' (the portion of the + tag after the first period), while the tag itself will be + set to simply 'salt_master'. This is a feature, not a bug :) + + Note: + There is a third emitter, for the GELF format, but it is + largely untested, and I don't currently have a setup supporting + this config, so while it runs cleanly and outputs what LOOKS to + be valid GELF, any real-world feedback on its usefulness, and + correctness, will be appreciated. Log Level ......... @@ -53,7 +82,7 @@ import time import datetime import socket import threading - +import types # Import salt libs from salt.log.setup import LOG_LEVELS @@ -91,6 +120,19 @@ __virtualname__ = 'fluent' _global_sender = None +# Python logger's idea of "level" is wildly at variance with +# Graylog's (and, incidentally, the rest of the civilized world). +syslog_levels = { + 'EMERG': 0, + 'ALERT': 2, + 'CRIT': 2, + 'ERR': 3, + 'WARNING': 4, + 'NOTICE': 5, + 'INFO': 6, + 'DEBUG': 7 +} + def setup(tag, **kwargs): host = kwargs.get('host', 'localhost') @@ -116,55 +158,133 @@ def __virtual__(): def setup_handlers(): - host = port = address = None + host = port = None if 'fluent_handler' in __opts__: host = __opts__['fluent_handler'].get('host', None) port = __opts__['fluent_handler'].get('port', None) - version = __opts__['fluent_handler'].get('version', 1) + payload_type = __opts__['fluent_handler'].get('payload_type', None) + # in general, you want the value of tag to ALSO be a member of tags + tags = __opts__['fluent_handler'].get('tags', ['salt']) + tag = tags[0] if len(tags) else 'salt' + if payload_type == 'graylog': + version = 0 + elif payload_type == 'gelf': + # We only support version 1.1 (the latest) of GELF... + version = 1.1 + else: + # Default to logstash for backwards compat + payload_type = 'logstash' + version = __opts__['fluent_handler'].get('version', 1) if host is None and port is None: log.debug( 'The required \'fluent_handler\' configuration keys, ' '\'host\' and/or \'port\', are not properly configured. Not ' - 'configuring the fluent logging handler.' + 'enabling the fluent logging handler.' ) else: - logstash_formatter = LogstashFormatter(version=version) - fluent_handler = FluentHandler('salt', host=host, port=port) - fluent_handler.setFormatter(logstash_formatter) + formatter = MessageFormatter(payload_type=payload_type, version=version, tags=tags) + fluent_handler = FluentHandler(tag, host=host, port=port) + fluent_handler.setFormatter(formatter) fluent_handler.setLevel( - LOG_LEVELS[ - __opts__['fluent_handler'].get( - 'log_level', - # Not set? Get the main salt log_level setting on the - # configuration file - __opts__.get( - 'log_level', - # Also not set?! Default to 'error' - 'error' - ) - ) - ] + LOG_LEVELS[__opts__['fluent_handler'].get('log_level', __opts__.get('log_level', 'error'))] ) yield fluent_handler - if host is None and port is None and address is None: + if host is None and port is None: yield False -class LogstashFormatter(logging.Formatter, NewStyleClassMixIn): - def __init__(self, msg_type='logstash', msg_path='logstash', version=1): - self.msg_path = msg_path - self.msg_type = msg_type +class MessageFormatter(logging.Formatter, NewStyleClassMixIn): + def __init__(self, payload_type, version, tags, msg_type=None, msg_path=None): + self.payload_type = payload_type self.version = version - self.format = getattr(self, 'format_v{0}'.format(version)) - super(LogstashFormatter, self).__init__(fmt=None, datefmt=None) + self.tag = tags[0] if len(tags) else 'salt' # 'salt' for backwards compat + self.tags = tags + self.msg_path = msg_path if msg_path else payload_type + self.msg_type = msg_type if msg_type else payload_type + format_func = 'format_{0}_v{1}'.format(payload_type, version).replace('.', '_') + self.format = getattr(self, format_func) + super(MessageFormatter, self).__init__(fmt=None, datefmt=None) def formatTime(self, record, datefmt=None): + if self.payload_type == 'gelf': # GELF uses epoch times + return record.created return datetime.datetime.utcfromtimestamp(record.created).isoformat()[:-3] + 'Z' - def format_v0(self, record): + def format_graylog_v0(self, record): + ''' + Graylog 'raw' format is essentially the raw record, minimally munged to provide + the bare minimum that td-agent requires to accept and route the event. This is + well suited to a config where the client td-agents log directly to Graylog. + ''' + message_dict = { + 'message': record.getMessage(), + 'timestamp': self.formatTime(record), + # Graylog uses syslog levels, not whatever it is Python does... + 'level': syslog_levels.get(record.levelname, 'ALERT'), + 'tag': self.tag + } + + if record.exc_info: + exc_info = self.formatException(record.exc_info) + message_dict.update({'full_message': exc_info}) + + # Add any extra attributes to the message field + for key, value in six.iteritems(record.__dict__): + if key in ('args', 'asctime', 'bracketlevel', 'bracketname', 'bracketprocess', + 'created', 'exc_info', 'exc_text', 'id', 'levelname', 'levelno', 'msecs', + 'msecs', 'message', 'msg', 'relativeCreated', 'version'): + # These are already handled above or explicitly pruned. + continue + + if isinstance(value, (six.string_types, bool, dict, float, int, list, types.NoneType)): # pylint: disable=W1699 + val = value + else: + val = repr(value) + message_dict.update({'{0}'.format(key): val}) + return message_dict + + def format_gelf_v1_1(self, record): + ''' + If your agent is (or can be) configured to forward pre-formed GELF to Graylog + with ZERO fluent processing, this function is for YOU, pal... + ''' + message_dict = { + 'version': self.version, + 'host': salt.utils.network.get_fqhostname(), + 'short_message': record.getMessage(), + 'timestamp': self.formatTime(record), + 'level': syslog_levels.get(record.levelname, 'ALERT'), + "_tag": self.tag + } + + if record.exc_info: + exc_info = self.formatException(record.exc_info) + message_dict.update({'full_message': exc_info}) + + # Add any extra attributes to the message field + for key, value in six.iteritems(record.__dict__): + if key in ('args', 'asctime', 'bracketlevel', 'bracketname', 'bracketprocess', + 'created', 'exc_info', 'exc_text', 'id', 'levelname', 'levelno', 'msecs', + 'msecs', 'message', 'msg', 'relativeCreated', 'version'): + # These are already handled above or explicitly avoided. + continue + + if isinstance(value, (six.string_types, bool, dict, float, int, list, types.NoneType)): # pylint: disable=W1699 + val = value + else: + val = repr(value) + # GELF spec require "non-standard" fields to be prefixed with '_' (underscore). + message_dict.update({'_{0}'.format(key): val}) + + return message_dict + + def format_logstash_v0(self, record): + ''' + Messages are formatted in logstash's expected format. + ''' host = salt.utils.network.get_fqhostname() message_dict = { '@timestamp': self.formatTime(record), @@ -186,7 +306,7 @@ class LogstashFormatter(logging.Formatter, NewStyleClassMixIn): ), '@source_host': host, '@source_path': self.msg_path, - '@tags': ['salt'], + '@tags': self.tags, '@type': self.msg_type, } @@ -216,7 +336,10 @@ class LogstashFormatter(logging.Formatter, NewStyleClassMixIn): message_dict['@fields'][key] = repr(value) return message_dict - def format_v1(self, record): + def format_logstash_v1(self, record): + ''' + Messages are formatted in logstash's expected format. + ''' message_dict = { '@version': 1, '@timestamp': self.formatTime(record), @@ -230,7 +353,7 @@ class LogstashFormatter(logging.Formatter, NewStyleClassMixIn): 'funcName': record.funcName, 'processName': record.processName, 'message': record.getMessage(), - 'tags': ['salt'], + 'tags': self.tags, 'type': self.msg_type } diff --git a/salt/master.py b/salt/master.py index f3f697bf83..19e11002c2 100644 --- a/salt/master.py +++ b/salt/master.py @@ -1311,7 +1311,8 @@ class AESFuncs(object): load.get(u'saltenv', load.get(u'env')), ext=load.get(u'ext'), pillar_override=load.get(u'pillar_override', {}), - pillarenv=load.get(u'pillarenv')) + pillarenv=load.get(u'pillarenv'), + extra_minion_data=load.get(u'extra_minion_data')) data = pillar.compile_pillar() self.fs_.update_opts() if self.opts.get(u'minion_data_cache', False): @@ -1667,49 +1668,36 @@ class ClearFuncs(object): Send a master control function back to the runner system ''' # All runner ops pass through eauth - if u'token' in clear_load: - # Authenticate - token = self.loadauth.authenticate_token(clear_load) + auth_type, err_name, key, sensitive_load_keys = self._prep_auth_info(clear_load) - if not token: - return dict(error=dict(name=u'TokenAuthenticationError', - message=u'Authentication failure of type "token" occurred.')) + # Authenticate + auth_check = self.loadauth.check_authentication(clear_load, auth_type, key=key) + error = auth_check.get(u'error') - # Authorize - if self.opts[u'keep_acl_in_token'] and u'auth_list' in token: - auth_list = token[u'auth_list'] - else: - clear_load[u'eauth'] = token[u'eauth'] - clear_load[u'username'] = token[u'name'] - auth_list = self.loadauth.get_auth_list(clear_load) + if error: + # Authentication error occurred: do not continue. + return {u'error': error} - if not self.ckminions.runner_check(auth_list, clear_load[u'fun'], clear_load.get(u'kwarg', {})): - return dict(error=dict(name=u'TokenAuthenticationError', - message=(u'Authentication failure of type "token" occurred for ' - u'user {0}.').format(token[u'name']))) - clear_load.pop(u'token') - username = token[u'name'] - elif u'eauth' in clear_load: - if not self.loadauth.authenticate_eauth(clear_load): - return dict(error=dict(name=u'EauthAuthenticationError', - message=(u'Authentication failure of type "eauth" occurred for ' - u'user {0}.').format(clear_load.get(u'username', u'UNKNOWN')))) + # Authorize + username = auth_check.get(u'username') + if auth_type != u'user': + runner_check = self.ckminions.runner_check( + auth_check.get(u'auth_list', []), + clear_load[u'fun'], + clear_load.get(u'kwarg', {}) + ) + if not runner_check: + return {u'error': {u'name': err_name, + u'message': u'Authentication failure of type "{0}" occurred for ' + u'user {1}.'.format(auth_type, username)}} + elif isinstance(runner_check, dict) and u'error' in runner_check: + # A dictionary with an error name/message was handled by ckminions.runner_check + return runner_check - auth_list = self.loadauth.get_auth_list(clear_load) - if not self.ckminions.runner_check(auth_list, clear_load[u'fun'], clear_load.get(u'kwarg', {})): - return dict(error=dict(name=u'EauthAuthenticationError', - message=(u'Authentication failure of type "eauth" occurred for ' - u'user {0}.').format(clear_load.get(u'username', u'UNKNOWN')))) - - # No error occurred, consume the password from the clear_load if - # passed - username = clear_load.pop(u'username', u'UNKNOWN') - clear_load.pop(u'password', None) + # No error occurred, consume sensitive settings from the clear_load if passed. + for item in sensitive_load_keys: + clear_load.pop(item, None) else: - if not self.loadauth.authenticate_key(clear_load, self.key): - return dict(error=dict(name=u'UserAuthenticationError', - message=u'Authentication failure of type "user" occurred')) - if u'user' in clear_load: username = clear_load[u'user'] if salt.auth.AuthUser(username).is_sudo(): @@ -1726,57 +1714,45 @@ class ClearFuncs(object): username) except Exception as exc: log.error(u'Exception occurred while introspecting %s: %s', fun, exc) - return dict(error=dict(name=exc.__class__.__name__, - args=exc.args, - message=str(exc))) + return {u'error': {u'name': exc.__class__.__name__, + u'args': exc.args, + u'message': str(exc)}} def wheel(self, clear_load): ''' Send a master control function back to the wheel system ''' # All wheel ops pass through eauth - username = None - if u'token' in clear_load: - # Authenticate - token = self.loadauth.authenticate_token(clear_load) - if not token: - return dict(error=dict(name=u'TokenAuthenticationError', - message=u'Authentication failure of type "token" occurred.')) + auth_type, err_name, key, sensitive_load_keys = self._prep_auth_info(clear_load) - # Authorize - if self.opts[u'keep_acl_in_token'] and u'auth_list' in token: - auth_list = token[u'auth_list'] - else: - clear_load[u'eauth'] = token[u'eauth'] - clear_load[u'username'] = token[u'name'] - auth_list = self.loadauth.get_auth_list(clear_load) - if not self.ckminions.wheel_check(auth_list, clear_load[u'fun'], clear_load.get(u'kwarg', {})): - return dict(error=dict(name=u'TokenAuthenticationError', - message=(u'Authentication failure of type "token" occurred for ' - u'user {0}.').format(token[u'name']))) - clear_load.pop(u'token') - username = token[u'name'] - elif u'eauth' in clear_load: - if not self.loadauth.authenticate_eauth(clear_load): - return dict(error=dict(name=u'EauthAuthenticationError', - message=(u'Authentication failure of type "eauth" occurred for ' - u'user {0}.').format(clear_load.get(u'username', u'UNKNOWN')))) + # Authenticate + auth_check = self.loadauth.check_authentication(clear_load, auth_type, key=key) + error = auth_check.get(u'error') - auth_list = self.loadauth.get_auth_list(clear_load) - if not self.ckminions.wheel_check(auth_list, clear_load[u'fun'], clear_load.get(u'kwarg', {})): - return dict(error=dict(name=u'EauthAuthenticationError', - message=(u'Authentication failure of type "eauth" occurred for ' - u'user {0}.').format(clear_load.get(u'username', u'UNKNOWN')))) + if error: + # Authentication error occurred: do not continue. + return {u'error': error} - # No error occurred, consume the password from the clear_load if - # passed - clear_load.pop(u'password', None) - username = clear_load.pop(u'username', u'UNKNOWN') + # Authorize + username = auth_check.get(u'username') + if auth_type != u'user': + wheel_check = self.ckminions.wheel_check( + auth_check.get(u'auth_list', []), + clear_load[u'fun'], + clear_load.get(u'kwarg', {}) + ) + if not wheel_check: + return {u'error': {u'name': err_name, + u'message': u'Authentication failure of type "{0}" occurred for ' + u'user {1}.'.format(auth_type, username)}} + elif isinstance(wheel_check, dict) and u'error' in wheel_check: + # A dictionary with an error name/message was handled by ckminions.wheel_check + return wheel_check + + # No error occurred, consume sensitive settings from the clear_load if passed. + for item in sensitive_load_keys: + clear_load.pop(item, None) else: - if not self.loadauth.authenticate_key(clear_load, self.key): - return dict(error=dict(name=u'UserAuthenticationError', - message=u'Authentication failure of type "user" occurred')) - if u'user' in clear_load: username = clear_load[u'user'] if salt.auth.AuthUser(username).is_sudo(): @@ -1786,7 +1762,7 @@ class ClearFuncs(object): # Authorized. Do the job! try: - jid = salt.utils.jid.gen_jid() + jid = salt.utils.jid.gen_jid(self.opts) fun = clear_load.pop(u'fun') tag = tagify(jid, prefix=u'wheel') data = {u'fun': u"wheel.{0}".format(fun), @@ -1852,11 +1828,13 @@ class ClearFuncs(object): # Retrieve the minions list delimiter = clear_load.get(u'kwargs', {}).get(u'delimiter', DEFAULT_TARGET_DELIM) - minions = self.ckminions.check_minions( + _res = self.ckminions.check_minions( clear_load[u'tgt'], clear_load.get(u'tgt_type', u'glob'), delimiter ) + minions = _res.get('minions', list()) + missing = _res.get('missing', list()) # Check for external auth calls if extra.get(u'token', False): @@ -1866,12 +1844,7 @@ class ClearFuncs(object): return u'' # Get acl - if self.opts[u'keep_acl_in_token'] and u'auth_list' in token: - auth_list = token[u'auth_list'] - else: - extra[u'eauth'] = token[u'eauth'] - extra[u'username'] = token[u'name'] - auth_list = self.loadauth.get_auth_list(extra) + auth_list = self.loadauth.get_auth_list(extra, token) # Authorize the request if not self.ckminions.auth_check( @@ -1961,7 +1934,7 @@ class ClearFuncs(object): if jid is None: return {u'enc': u'clear', u'load': {u'error': u'Master failed to assign jid'}} - payload = self._prep_pub(minions, jid, clear_load, extra) + payload = self._prep_pub(minions, jid, clear_load, extra, missing) # Send it! self._send_pub(payload) @@ -1970,10 +1943,29 @@ class ClearFuncs(object): u'enc': u'clear', u'load': { u'jid': clear_load[u'jid'], - u'minions': minions + u'minions': minions, + u'missing': missing } } + def _prep_auth_info(self, clear_load): + sensitive_load_keys = [] + key = None + if u'token' in clear_load: + auth_type = u'token' + err_name = u'TokenAuthenticationError' + sensitive_load_keys = [u'token'] + elif u'eauth' in clear_load: + auth_type = u'eauth' + err_name = u'EauthAuthenticationError' + sensitive_load_keys = [u'username', u'password'] + else: + auth_type = u'user' + err_name = u'UserAuthenticationError' + key = self.key + + return auth_type, err_name, key, sensitive_load_keys + def _prep_jid(self, clear_load, extra): ''' Return a jid for this publication @@ -2007,7 +1999,7 @@ class ClearFuncs(object): chan = salt.transport.server.PubServerChannel.factory(opts) chan.publish(load) - def _prep_pub(self, minions, jid, clear_load, extra): + def _prep_pub(self, minions, jid, clear_load, extra, missing): ''' Take a given load and perform the necessary steps to prepare a publication. @@ -2028,6 +2020,7 @@ class ClearFuncs(object): u'fun': clear_load[u'fun'], u'arg': clear_load[u'arg'], u'minions': minions, + u'missing': missing, } # Announce the job on the event bus diff --git a/salt/minion.py b/salt/minion.py index 5713a0edb6..6316f76eec 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -21,6 +21,7 @@ import multiprocessing from random import randint, shuffle from stat import S_IMODE import salt.serializers.msgpack +from binascii import crc32 # Import Salt Libs # pylint: disable=import-error,no-name-in-module,redefined-builtin @@ -102,6 +103,7 @@ import salt.defaults.exitcodes import salt.cli.daemons import salt.log.setup +import salt.utils.dictupdate from salt.config import DEFAULT_MINION_OPTS from salt.defaults import DEFAULT_TARGET_DELIM from salt.utils.debug import enable_sigusr1_handler @@ -443,13 +445,30 @@ class MinionBase(object): if opts[u'master_type'] == u'func': eval_master_func(opts) - # if failover is set, master has to be of type list - elif opts[u'master_type'] == u'failover': + # if failover or distributed is set, master has to be of type list + elif opts[u'master_type'] in (u'failover', u'distributed'): if isinstance(opts[u'master'], list): log.info( u'Got list of available master addresses: %s', opts[u'master'] ) + + if opts[u'master_type'] == u'distributed': + master_len = len(opts[u'master']) + if master_len > 1: + secondary_masters = opts[u'master'][1:] + master_idx = crc32(opts[u'id']) % master_len + try: + preferred_masters = opts[u'master'] + preferred_masters[0] = opts[u'master'][master_idx] + preferred_masters[1:] = [m for m in opts[u'master'] if m != preferred_masters[0]] + opts[u'master'] = preferred_masters + log.info(u'Distributed to the master at \'{0}\'.'.format(opts[u'master'][0])) + except (KeyError, AttributeError, TypeError): + log.warning(u'Failed to distribute to a specific master.') + else: + log.warning(u'master_type = distributed needs more than 1 master.') + if opts[u'master_shuffle']: if opts[u'master_failback']: secondary_masters = opts[u'master'][1:] @@ -497,7 +516,7 @@ class MinionBase(object): sys.exit(salt.defaults.exitcodes.EX_GENERIC) # If failover is set, minion have to failover on DNS errors instead of retry DNS resolve. # See issue 21082 for details - if opts[u'retry_dns']: + if opts[u'retry_dns'] and opts[u'master_type'] == u'failover': msg = (u'\'master_type\' set to \'failover\' but \'retry_dns\' is not 0. ' u'Setting \'retry_dns\' to 0 to failover to the next master on DNS errors.') log.critical(msg) @@ -845,7 +864,7 @@ class MinionManager(MinionBase): Spawn all the coroutines which will sign in to masters ''' masters = self.opts[u'master'] - if self.opts[u'master_type'] == u'failover' or not isinstance(self.opts[u'master'], list): + if (self.opts[u'master_type'] in (u'failover', u'distributed')) or not isinstance(self.opts[u'master'], list): masters = [masters] for master in masters: @@ -1624,13 +1643,24 @@ class Minion(MinionBase): minion side execution. ''' salt.utils.appendproctitle(u'{0}._thread_multi_return {1}'.format(cls.__name__, data[u'jid'])) - ret = { - u'return': {}, - u'retcode': {}, - u'success': {} - } - for ind in range(0, len(data[u'fun'])): - ret[u'success'][data[u'fun'][ind]] = False + multifunc_ordered = opts.get(u'multifunc_ordered', False) + num_funcs = len(data[u'fun']) + if multifunc_ordered: + ret = { + u'return': [None] * num_funcs, + u'retcode': [None] * num_funcs, + u'success': [False] * num_funcs + } + else: + ret = { + u'return': {}, + u'retcode': {}, + u'success': {} + } + + for ind in range(0, num_funcs): + if not multifunc_ordered: + ret[u'success'][data[u'fun'][ind]] = False try: minion_blackout_violation = False if minion_instance.connected and minion_instance.opts[u'pillar'].get(u'minion_blackout', False): @@ -1654,16 +1684,27 @@ class Minion(MinionBase): data[u'arg'][ind], data) minion_instance.functions.pack[u'__context__'][u'retcode'] = 0 - ret[u'return'][data[u'fun'][ind]] = func(*args, **kwargs) - ret[u'retcode'][data[u'fun'][ind]] = minion_instance.functions.pack[u'__context__'].get( - u'retcode', - 0 - ) - ret[u'success'][data[u'fun'][ind]] = True + if multifunc_ordered: + ret[u'return'][ind] = func(*args, **kwargs) + ret[u'retcode'][ind] = minion_instance.functions.pack[u'__context__'].get( + u'retcode', + 0 + ) + ret[u'success'][ind] = True + else: + ret[u'return'][data[u'fun'][ind]] = func(*args, **kwargs) + ret[u'retcode'][data[u'fun'][ind]] = minion_instance.functions.pack[u'__context__'].get( + u'retcode', + 0 + ) + ret[u'success'][data[u'fun'][ind]] = True except Exception as exc: trb = traceback.format_exc() log.warning(u'The minion function caused an exception: %s', exc) - ret[u'return'][data[u'fun'][ind]] = trb + if multifunc_ordered: + ret[u'return'][ind] = trb + else: + ret[u'return'][data[u'fun'][ind]] = trb ret[u'jid'] = data[u'jid'] ret[u'fun'] = data[u'fun'] ret[u'fun_args'] = data[u'arg'] @@ -1930,6 +1971,10 @@ class Minion(MinionBase): self.beacons.disable_beacon(name) elif func == u'list': self.beacons.list_beacons() + elif func == u'list_available': + self.beacons.list_available_beacons() + elif func == u'validate_beacon': + self.beacons.validate_beacon(name, beacon_data) def environ_setenv(self, tag, data): ''' @@ -2651,6 +2696,8 @@ class SyndicManager(MinionBase): ''' if kwargs is None: kwargs = {} + successful = False + # Call for each master for master, syndic_future in self.iter_master_options(master_id): if not syndic_future.done() or syndic_future.exception(): log.error( @@ -2661,15 +2708,15 @@ class SyndicManager(MinionBase): try: getattr(syndic_future.result(), func)(*args, **kwargs) - return + successful = True except SaltClientError: log.error( u'Unable to call %s on %s, trying another...', func, master ) self._mark_master_dead(master) - continue - log.critical(u'Unable to call %s on any masters!', func) + if not successful: + log.critical(u'Unable to call %s on any masters!', func) def _return_pub_syndic(self, values, master_id=None): ''' @@ -3191,6 +3238,26 @@ class ProxyMinion(Minion): if u'proxy' not in self.opts: self.opts[u'proxy'] = self.opts[u'pillar'][u'proxy'] + if self.opts.get(u'proxy_merge_pillar_in_opts'): + # Override proxy opts with pillar data when the user required. + self.opts = salt.utils.dictupdate.merge(self.opts, + self.opts[u'pillar'], + strategy=self.opts.get(u'proxy_merge_pillar_in_opts_strategy'), + merge_lists=self.opts.get(u'proxy_deep_merge_pillar_in_opts', False)) + elif self.opts.get(u'proxy_mines_pillar'): + # Even when not required, some details such as mine configuration + # should be merged anyway whenever possible. + if u'mine_interval' in self.opts[u'pillar']: + self.opts[u'mine_interval'] = self.opts[u'pillar'][u'mine_interval'] + if u'mine_functions' in self.opts[u'pillar']: + general_proxy_mines = self.opts.get(u'mine_functions', []) + specific_proxy_mines = self.opts[u'pillar'][u'mine_functions'] + try: + self.opts[u'mine_functions'] = general_proxy_mines + specific_proxy_mines + except TypeError as terr: + log.error(u'Unable to merge mine functions from the pillar in the opts, for proxy {}'.format( + self.opts[u'id'])) + fq_proxyname = self.opts[u'proxy'][u'proxytype'] # Need to load the modules so they get all the dunder variables diff --git a/salt/modules/alternatives.py b/salt/modules/alternatives.py index b1a5ecb4a8..ad21785fc3 100644 --- a/salt/modules/alternatives.py +++ b/salt/modules/alternatives.py @@ -12,6 +12,7 @@ import logging # Import Salt libs import salt.utils.files +import salt.utils.path # Import 3rd-party libs from salt.ext import six @@ -241,4 +242,4 @@ def _read_link(name): Throws an OSError if the link does not exist ''' alt_link_path = '/etc/alternatives/{0}'.format(name) - return os.readlink(alt_link_path) + return salt.utils.path.readlink(alt_link_path) diff --git a/salt/modules/apache.py b/salt/modules/apache.py index e44e496709..c236f1c74f 100644 --- a/salt/modules/apache.py +++ b/salt/modules/apache.py @@ -447,11 +447,15 @@ def config(name, config, edit=True): salt '*' apache.config /etc/httpd/conf.d/ports.conf config="[{'Listen': '22'}]" ''' + configs = [] for entry in config: key = next(six.iterkeys(entry)) - configs = _parse_config(entry[key], key) - if edit: - with salt.utils.files.fopen(name, 'w') as configfile: - configfile.write('# This file is managed by Salt.\n') - configfile.write(configs) - return configs + configs.append(_parse_config(entry[key], key)) + + # Python auto-correct line endings + configstext = "\n".join(configs) + if edit: + with salt.utils.files.fopen(name, 'w') as configfile: + configfile.write('# This file is managed by Salt.\n') + configfile.write(configstext) + return configstext diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py index 6892dc4625..97f2c7f885 100644 --- a/salt/modules/aptpkg.py +++ b/salt/modules/aptpkg.py @@ -97,11 +97,15 @@ __virtualname__ = 'pkg' def __virtual__(): ''' - Confirm this module is on a Debian based system + Confirm this module is on a Debian-based system ''' - if __grains__.get('os_family') in ('Kali', 'Debian', 'neon'): - return __virtualname__ - elif __grains__.get('os_family', False) == 'Cumulus': + # If your minion is running an OS which is Debian-based but does not have + # an "os_family" grain of Debian, then the proper fix is NOT to check for + # the minion's "os_family" grain here in the __virtual__. The correct fix + # is to add the value from the minion's "os" grain to the _OS_FAMILY_MAP + # dict in salt/grains/core.py, so that we assign the correct "os_family" + # grain to the minion. + if __grains__.get('os_family') == 'Debian': return __virtualname__ return (False, 'The pkg module could not be loaded: unsupported OS family') diff --git a/salt/modules/archive.py b/salt/modules/archive.py index 70ef0bdecc..7d627f7fdb 100644 --- a/salt/modules/archive.py +++ b/salt/modules/archive.py @@ -60,7 +60,8 @@ def list_(name, strip_components=None, clean=False, verbose=False, - saltenv='base'): + saltenv='base', + source_hash=None): ''' .. versionadded:: 2016.11.0 .. versionchanged:: 2016.11.2 @@ -149,6 +150,14 @@ def list_(name, ``archive``. This is only applicable when ``archive`` is a file from the ``salt://`` fileserver. + source_hash + If ``name`` is an http(s)/ftp URL and the file exists in the minion's + file cache, this option can be passed to keep the minion from + re-downloading the archive if the cached copy matches the specified + hash. + + .. versionadded:: Oxygen + .. _tarfile: https://docs.python.org/2/library/tarfile.html .. _xz: http://tukaani.org/xz/ @@ -160,6 +169,7 @@ def list_(name, salt '*' archive.list /path/to/myfile.tar.gz strip_components=1 salt '*' archive.list salt://foo.tar.gz salt '*' archive.list https://domain.tld/myfile.zip + salt '*' archive.list https://domain.tld/myfile.zip source_hash=f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 salt '*' archive.list ftp://10.1.2.3/foo.rar ''' def _list_tar(name, cached, decompress_cmd, failhard=False): @@ -309,7 +319,7 @@ def list_(name, ) return dirs, files, [] - cached = __salt__['cp.cache_file'](name, saltenv) + cached = __salt__['cp.cache_file'](name, saltenv, source_hash=source_hash) if not cached: raise CommandExecutionError('Failed to cache {0}'.format(name)) @@ -1094,7 +1104,7 @@ def unzip(zip_file, return _trim_files(cleaned_files, trim_output) -def is_encrypted(name, clean=False, saltenv='base'): +def is_encrypted(name, clean=False, saltenv='base', source_hash=None): ''' .. versionadded:: 2016.11.0 @@ -1113,6 +1123,18 @@ def is_encrypted(name, clean=False, saltenv='base'): If there is an error listing the archive's contents, the cached file will not be removed, to allow for troubleshooting. + saltenv : base + Specifies the fileserver environment from which to retrieve + ``archive``. This is only applicable when ``archive`` is a file from + the ``salt://`` fileserver. + + source_hash + If ``name`` is an http(s)/ftp URL and the file exists in the minion's + file cache, this option can be passed to keep the minion from + re-downloading the archive if the cached copy matches the specified + hash. + + .. versionadded:: Oxygen CLI Examples: @@ -1122,9 +1144,10 @@ def is_encrypted(name, clean=False, saltenv='base'): salt '*' archive.is_encrypted salt://foo.zip salt '*' archive.is_encrypted salt://foo.zip saltenv=dev salt '*' archive.is_encrypted https://domain.tld/myfile.zip clean=True + salt '*' archive.is_encrypted https://domain.tld/myfile.zip source_hash=f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 salt '*' archive.is_encrypted ftp://10.1.2.3/foo.zip ''' - cached = __salt__['cp.cache_file'](name, saltenv) + cached = __salt__['cp.cache_file'](name, saltenv, source_hash=source_hash) if not cached: raise CommandExecutionError('Failed to cache {0}'.format(name)) diff --git a/salt/modules/augeas_cfg.py b/salt/modules/augeas_cfg.py index 9631958667..e0f78329ec 100644 --- a/salt/modules/augeas_cfg.py +++ b/salt/modules/augeas_cfg.py @@ -200,7 +200,7 @@ def execute(context=None, lens=None, commands=(), load_path=None): method = METHOD_MAP[cmd] nargs = arg_map[method] - parts = salt.utils.args.shlex_split(arg, posix=False) + parts = salt.utils.args.shlex_split(arg) if len(parts) not in nargs: err = '{0} takes {1} args: {2}'.format(method, nargs, parts) diff --git a/salt/modules/beacons.py b/salt/modules/beacons.py index 0b6cd342f4..7e095ed656 100644 --- a/salt/modules/beacons.py +++ b/salt/modules/beacons.py @@ -14,6 +14,7 @@ import os import yaml # Import Salt libs +import salt.ext.six as six import salt.utils.event import salt.utils.files from salt.ext.six.moves import map @@ -69,6 +70,47 @@ def list_(return_yaml=True): return {'beacons': {}} +def list_available(return_yaml=True): + ''' + List the beacons currently available on the minion + + :param return_yaml: Whether to return YAML formatted output, default True + :return: List of currently configured Beacons. + + CLI Example: + + .. code-block:: bash + + salt '*' beacons.list_available + + ''' + beacons = None + + try: + eventer = salt.utils.event.get_event('minion', opts=__opts__) + res = __salt__['event.fire']({'func': 'list_available'}, 'manage_beacons') + if res: + event_ret = eventer.get_event(tag='/salt/minion/minion_beacons_list_available_complete', wait=30) + if event_ret and event_ret['complete']: + beacons = event_ret['beacons'] + except KeyError: + # Effectively a no-op, since we can't really return without an event system + ret = {} + ret['result'] = False + ret['comment'] = 'Event module not available. Beacon add failed.' + return ret + + if beacons: + if return_yaml: + tmp = {'beacons': beacons} + yaml_out = yaml.safe_dump(tmp, default_flow_style=False) + return yaml_out + else: + return beacons + else: + return {'beacons': {}} + + def add(name, beacon_data, **kwargs): ''' Add a beacon on the minion @@ -95,37 +137,34 @@ def add(name, beacon_data, **kwargs): ret['result'] = True ret['comment'] = 'Beacon: {0} would be added.'.format(name) else: - # Attempt to load the beacon module so we have access to the validate function - try: - beacon_module = __import__('salt.beacons.' + name, fromlist=['validate']) - log.debug('Successfully imported beacon.') - except ImportError: - ret['comment'] = 'Beacon {0} does not exist'.format(name) - return ret - - # Attempt to validate - if hasattr(beacon_module, 'validate'): - _beacon_data = beacon_data - if 'enabled' in _beacon_data: - del _beacon_data['enabled'] - valid, vcomment = beacon_module.validate(_beacon_data) - else: - log.info('Beacon {0} does not have a validate' - ' function, skipping validation.'.format(name)) - valid = True - - if not valid: - ret['result'] = False - ret['comment'] = ('Beacon {0} configuration invalid, ' - 'not adding.\n{1}'.format(name, vcomment)) - return ret - try: + # Attempt to load the beacon module so we have access to the validate function eventer = salt.utils.event.get_event('minion', opts=__opts__) - res = __salt__['event.fire']({'name': name, 'beacon_data': beacon_data, 'func': 'add'}, 'manage_beacons') + res = __salt__['event.fire']({'name': name, + 'beacon_data': beacon_data, + 'func': 'validate_beacon'}, + 'manage_beacons') + if res: + event_ret = eventer.get_event(tag='/salt/minion/minion_beacon_validation_complete', wait=30) + valid = event_ret['valid'] + vcomment = event_ret['vcomment'] + + if not valid: + ret['result'] = False + ret['comment'] = ('Beacon {0} configuration invalid, ' + 'not adding.\n{1}'.format(name, vcomment)) + return ret + + except KeyError: + # Effectively a no-op, since we can't really return without an event system + ret['comment'] = 'Event module not available. Beacon add failed.' + + try: + res = __salt__['event.fire']({'name': name, + 'beacon_data': beacon_data, + 'func': 'add'}, 'manage_beacons') if res: event_ret = eventer.get_event(tag='/salt/minion/minion_beacon_add_complete', wait=30) - log.debug('=== event_ret {} ==='.format(event_ret)) if event_ret and event_ret['complete']: beacons = event_ret['beacons'] if name in beacons and beacons[name] == beacon_data: @@ -165,29 +204,32 @@ def modify(name, beacon_data, **kwargs): ret['result'] = True ret['comment'] = 'Beacon: {0} would be added.'.format(name) else: - # Attempt to load the beacon module so we have access to the validate function try: - beacon_module = __import__('salt.beacons.' + name, fromlist=['validate']) - log.debug('Successfully imported beacon.') - except ImportError: - ret['comment'] = 'Beacon {0} does not exist'.format(name) - return ret + # Attempt to load the beacon module so we have access to the validate function + eventer = salt.utils.event.get_event('minion', opts=__opts__) + res = __salt__['event.fire']({'name': name, + 'beacon_data': beacon_data, + 'func': 'validate_beacon'}, + 'manage_beacons') + if res: + event_ret = eventer.get_event(tag='/salt/minion/minion_beacon_validation_complete', wait=30) + valid = event_ret['valid'] + vcomment = event_ret['vcomment'] - # Attempt to validate - if hasattr(beacon_module, 'validate'): - _beacon_data = beacon_data - if 'enabled' in _beacon_data: - del _beacon_data['enabled'] - valid, vcomment = beacon_module.validate(_beacon_data) - else: - log.info('Beacon {0} does not have a validate' - ' function, skipping validation.'.format(name)) - valid = True + if not valid: + ret['result'] = False + ret['comment'] = ('Beacon {0} configuration invalid, ' + 'not adding.\n{1}'.format(name, vcomment)) + return ret + + except KeyError: + # Effectively a no-op, since we can't really return without an event system + ret['comment'] = 'Event module not available. Beacon modify failed.' if not valid: ret['result'] = False ret['comment'] = ('Beacon {0} configuration invalid, ' - 'not adding.\n{1}'.format(name, vcomment)) + 'not modifying.\n{1}'.format(name, vcomment)) return ret _current = current_beacons[name] @@ -197,10 +239,14 @@ def modify(name, beacon_data, **kwargs): ret['comment'] = 'Job {0} in correct state'.format(name) return ret - _current_lines = ['{0}:{1}\n'.format(key, value) - for (key, value) in sorted(_current.items())] - _new_lines = ['{0}:{1}\n'.format(key, value) - for (key, value) in sorted(_new.items())] + _current_lines = [] + for _item in _current: + _current_lines.extend(['{0}:{1}\n'.format(key, value) + for (key, value) in six.iteritems(_item)]) + _new_lines = [] + for _item in _new: + _new_lines.extend(['{0}:{1}\n'.format(key, value) + for (key, value) in six.iteritems(_item)]) _diff = difflib.unified_diff(_current_lines, _new_lines) ret['changes'] = {} diff --git a/salt/modules/boto_cloudfront.py b/salt/modules/boto_cloudfront.py new file mode 100644 index 0000000000..aa932884bf --- /dev/null +++ b/salt/modules/boto_cloudfront.py @@ -0,0 +1,462 @@ +# -*- coding: utf-8 -*- +''' +Connection module for Amazon CloudFront + +.. versionadded:: Oxygen + +:depends: boto3 + +:configuration: This module accepts explicit AWS credentials but can also + utilize IAM roles assigned to the instance through Instance Profiles or + it can read them from the ~/.aws/credentials file or from these + environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY. + Dynamic credentials are then automatically obtained from AWS API and no + further configuration is necessary. More information available at: + + .. code-block:: text + + http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ + iam-roles-for-amazon-ec2.html + + http://boto3.readthedocs.io/en/latest/guide/ + configuration.html#guide-configuration + + If IAM roles are not used you need to specify them either in a pillar or + in the minion's config file: + + .. code-block:: yaml + + cloudfront.keyid: GKTADJGHEIQSXMKKRBJ08H + cloudfront.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs + + A region may also be specified in the configuration: + + .. code-block:: yaml + + cloudfront.region: us-east-1 + + If a region is not specified, the default is us-east-1. + + It's also possible to specify key, keyid and region via a profile, either + as a passed in dict, or as a string to pull from pillars or minion config: + + .. code-block:: yaml + + myprofile: + keyid: GKTADJGHEIQSXMKKRBJ08H + key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs + region: us-east-1 +''' +# keep lint from choking on _get_conn and _cache_id +# pylint: disable=E0602 + +# Import Python libs +from __future__ import absolute_import +import logging + +# Import Salt libs +import salt.ext.six as six +from salt.utils.odict import OrderedDict + +import yaml + +# Import third party libs +try: + # pylint: disable=unused-import + import boto3 + import botocore + # pylint: enable=unused-import + logging.getLogger('boto3').setLevel(logging.CRITICAL) + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only load if boto3 libraries exist. + ''' + if not HAS_BOTO: + msg = 'The boto_cloudfront module could not be loaded: {}.' + return (False, msg.format('boto3 libraries not found')) + __utils__['boto3.assign_funcs'](__name__, 'cloudfront') + return True + + +def _list_distributions( + conn, + name=None, + region=None, + key=None, + keyid=None, + profile=None, +): + ''' + Private function that returns an iterator over all CloudFront distributions. + The caller is responsible for all boto-related error handling. + + name + (Optional) Only yield the distribution with the given name + ''' + for dl_ in conn.get_paginator('list_distributions').paginate(): + distribution_list = dl_['DistributionList'] + if 'Items' not in distribution_list: + # If there are no items, AWS omits the `Items` key for some reason + continue + for partial_dist in distribution_list['Items']: + tags = conn.list_tags_for_resource(Resource=partial_dist['ARN']) + tags = dict( + (kv['Key'], kv['Value']) for kv in tags['Tags']['Items'] + ) + + id_ = partial_dist['Id'] + if 'Name' not in tags: + log.warning( + 'CloudFront distribution {0} has no Name tag.'.format(id_), + ) + continue + distribution_name = tags.pop('Name', None) + if name is not None and distribution_name != name: + continue + + # NOTE: list_distributions() returns a DistributionList, + # which nominally contains a list of Distribution objects. + # However, they are mangled in that they are missing values + # (`Logging`, `ActiveTrustedSigners`, and `ETag` keys) + # and moreover flatten the normally nested DistributionConfig + # attributes to the top level. + # Hence, we must call get_distribution() to get the full object, + # and we cache these objects to help lessen API calls. + distribution = _cache_id( + 'cloudfront', + sub_resource=distribution_name, + region=region, + key=key, + keyid=keyid, + profile=profile, + ) + if distribution: + yield (distribution_name, distribution) + continue + + dist_with_etag = conn.get_distribution(Id=id_) + distribution = { + 'distribution': dist_with_etag['Distribution'], + 'etag': dist_with_etag['ETag'], + 'tags': tags, + } + _cache_id( + 'cloudfront', + sub_resource=distribution_name, + resource_id=distribution, + region=region, + key=key, + keyid=keyid, + profile=profile, + ) + yield (distribution_name, distribution) + + +def get_distribution(name, region=None, key=None, keyid=None, profile=None): + ''' + Get information about a CloudFront distribution (configuration, tags) with a given name. + + name + Name of the CloudFront distribution + + region + Region to connect to + + key + Secret key to use + + keyid + Access key to use + + profile + A dict with region, key, and keyid, + or a pillar key (string) that contains such a dict. + + CLI Example: + + .. code-block:: bash + + salt myminion boto_cloudfront.get_distribution name=mydistribution profile=awsprofile + + ''' + distribution = _cache_id( + 'cloudfront', + sub_resource=name, + region=region, + key=key, + keyid=keyid, + profile=profile, + ) + if distribution: + return {'result': distribution} + + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + try: + for _, dist in _list_distributions( + conn, + name=name, + region=region, + key=key, + keyid=keyid, + profile=profile, + ): + # _list_distributions should only return the one distribution + # that we want (with the given name). + # In case of multiple distributions with the same name tag, + # our use of caching means list_distributions will just + # return the first one over and over again, + # so only the first result is useful. + if distribution is not None: + msg = 'More than one distribution found with name {0}' + return {'error': msg.format(name)} + distribution = dist + except botocore.exceptions.ClientError as err: + return {'error': __utils__['boto3.get_error'](err)} + if not distribution: + return {'result': None} + + _cache_id( + 'cloudfront', + sub_resource=name, + resource_id=distribution, + region=region, + key=key, + keyid=keyid, + profile=profile, + ) + return {'result': distribution} + + +def export_distributions(region=None, key=None, keyid=None, profile=None): + ''' + Get details of all CloudFront distributions. + Produces results that can be used to create an SLS file. + + CLI Example: + + .. code-block:: bash + + salt-call boto_cloudfront.export_distributions --out=txt |\ + sed "s/local: //" > cloudfront_distributions.sls + + ''' + results = OrderedDict() + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + try: + for name, distribution in _list_distributions( + conn, + region=region, + key=key, + keyid=keyid, + profile=profile, + ): + config = distribution['distribution']['DistributionConfig'] + tags = distribution['tags'] + + distribution_sls_data = [ + {'name': name}, + {'config': config}, + {'tags': tags}, + ] + results['Manage CloudFront distribution {0}'.format(name)] = { + 'boto_cloudfront.present': distribution_sls_data, + } + except botocore.exceptions.ClientError as err: + # Raise an exception, as this is meant to be user-invoked at the CLI + # as opposed to being called from execution or state modules + raise err + + dumper = __utils__['yamldumper.get_dumper']('IndentedSafeOrderedDumper') + return yaml.dump( + results, + default_flow_style=False, + Dumper=dumper, + ) + + +def create_distribution( + name, + config, + tags=None, + region=None, + key=None, + keyid=None, + profile=None, +): + ''' + Create a CloudFront distribution with the given name, config, and (optionally) tags. + + name + Name for the CloudFront distribution + + config + Configuration for the distribution + + tags + Tags to associate with the distribution + + region + Region to connect to + + key + Secret key to use + + keyid + Access key to use + + profile + A dict with region, key, and keyid, + or a pillar key (string) that contains such a dict. + + CLI Example: + + .. code-block:: bash + + salt myminion boto_cloudfront.create_distribution name=mydistribution profile=awsprofile \ + config='{"Comment":"partial configuration","Enabled":true}' + ''' + if tags is None: + tags = {} + if 'Name' in tags: + # Be lenient and silently accept if names match, else error + if tags['Name'] != name: + return {'error': 'Must not pass `Name` in `tags` but as `name`'} + tags['Name'] = name + tags = { + 'Items': [{'Key': k, 'Value': v} for k, v in six.iteritems(tags)] + } + + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + try: + conn.create_distribution_with_tags( + DistributionConfigWithTags={ + 'DistributionConfig': config, + 'Tags': tags, + }, + ) + _cache_id( + 'cloudfront', + sub_resource=name, + invalidate=True, + region=region, + key=key, + keyid=keyid, + profile=profile, + ) + except botocore.exceptions.ClientError as err: + return {'error': __utils__['boto3.get_error'](err)} + + return {'result': True} + + +def update_distribution( + name, + config, + tags=None, + region=None, + key=None, + keyid=None, + profile=None, +): + ''' + Update the config (and optionally tags) for the CloudFront distribution with the given name. + + name + Name of the CloudFront distribution + + config + Configuration for the distribution + + tags + Tags to associate with the distribution + + region + Region to connect to + + key + Secret key to use + + keyid + Access key to use + + profile + A dict with region, key, and keyid, + or a pillar key (string) that contains such a dict. + + CLI Example: + + .. code-block:: bash + + salt myminion boto_cloudfront.update_distribution name=mydistribution profile=awsprofile \ + config='{"Comment":"partial configuration","Enabled":true}' + ''' + distribution_ret = get_distribution( + name, + region=region, + key=key, + keyid=keyid, + profile=profile + ) + if 'error' in distribution_result: + return distribution_result + dist_with_tags = distribution_result['result'] + + current_distribution = dist_with_tags['distribution'] + current_config = current_distribution['DistributionConfig'] + current_tags = dist_with_tags['tags'] + etag = dist_with_tags['etag'] + + config_diff = __utils__['dictdiffer.deep_diff'](current_config, config) + if tags: + tags_diff = __utils__['dictdiffer.deep_diff'](current_tags, tags) + + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + try: + if 'old' in config_diff or 'new' in config_diff: + conn.update_distribution( + DistributionConfig=config, + Id=current_distribution['Id'], + IfMatch=etag, + ) + if tags: + arn = current_distribution['ARN'] + if 'new' in tags_diff: + tags_to_add = { + 'Items': [ + {'Key': k, 'Value': v} + for k, v in six.iteritems(tags_diff['new']) + ], + } + conn.tag_resource( + Resource=arn, + Tags=tags_to_add, + ) + if 'old' in tags_diff: + tags_to_remove = { + 'Items': list(tags_diff['old'].keys()), + } + conn.untag_resource( + Resource=arn, + TagKeys=tags_to_remove, + ) + except botocore.exceptions.ClientError as err: + return {'error': __utils__['boto3.get_error'](err)} + finally: + _cache_id( + 'cloudfront', + sub_resource=name, + invalidate=True, + region=region, + key=key, + keyid=keyid, + profile=profile, + ) + + return {'result': True} diff --git a/salt/modules/boto_elb.py b/salt/modules/boto_elb.py index 9b300d368f..6f6bb4c6e9 100644 --- a/salt/modules/boto_elb.py +++ b/salt/modules/boto_elb.py @@ -161,10 +161,9 @@ def get_elb_config(name, region=None, key=None, keyid=None, profile=None): salt myminion boto_elb.exists myelb region=us-east-1 ''' conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) - wait = 60 - orig_wait = wait + retries = 30 - while True: + while retries: try: lb = conn.get_all_load_balancers(load_balancer_names=[name]) lb = lb[0] @@ -205,16 +204,15 @@ def get_elb_config(name, region=None, key=None, keyid=None, profile=None): ret['policies'] = policies return ret except boto.exception.BotoServerError as error: - if getattr(error, 'error_code', '') == 'Throttling': - if wait > 0: - sleep = wait if wait % 5 == wait else 5 - log.info('Throttled by AWS API, will retry in 5 seconds.') - time.sleep(sleep) - wait -= sleep - continue - log.error('API still throttling us after {0} seconds!'.format(orig_wait)) + if error.error_code == 'Throttling': + log.debug('Throttled by AWS API, will retry in 5 seconds.') + time.sleep(5) + retries -= 1 + continue + log.error('Error fetching config for ELB {0}: {1}'.format(name, error.message)) log.error(error) return {} + return {} def listener_dict_to_tuple(listener): @@ -515,31 +513,38 @@ def get_attributes(name, region=None, key=None, keyid=None, profile=None): salt myminion boto_elb.get_attributes myelb ''' conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + retries = 30 - try: - lbattrs = conn.get_all_lb_attributes(name) - ret = odict.OrderedDict() - ret['access_log'] = odict.OrderedDict() - ret['cross_zone_load_balancing'] = odict.OrderedDict() - ret['connection_draining'] = odict.OrderedDict() - ret['connecting_settings'] = odict.OrderedDict() - al = lbattrs.access_log - czlb = lbattrs.cross_zone_load_balancing - cd = lbattrs.connection_draining - cs = lbattrs.connecting_settings - ret['access_log']['enabled'] = al.enabled - ret['access_log']['s3_bucket_name'] = al.s3_bucket_name - ret['access_log']['s3_bucket_prefix'] = al.s3_bucket_prefix - ret['access_log']['emit_interval'] = al.emit_interval - ret['cross_zone_load_balancing']['enabled'] = czlb.enabled - ret['connection_draining']['enabled'] = cd.enabled - ret['connection_draining']['timeout'] = cd.timeout - ret['connecting_settings']['idle_timeout'] = cs.idle_timeout - return ret - except boto.exception.BotoServerError as error: - log.debug(error) - log.error('ELB {0} does not exist: {1}'.format(name, error)) - return {} + while retries: + try: + lbattrs = conn.get_all_lb_attributes(name) + ret = odict.OrderedDict() + ret['access_log'] = odict.OrderedDict() + ret['cross_zone_load_balancing'] = odict.OrderedDict() + ret['connection_draining'] = odict.OrderedDict() + ret['connecting_settings'] = odict.OrderedDict() + al = lbattrs.access_log + czlb = lbattrs.cross_zone_load_balancing + cd = lbattrs.connection_draining + cs = lbattrs.connecting_settings + ret['access_log']['enabled'] = al.enabled + ret['access_log']['s3_bucket_name'] = al.s3_bucket_name + ret['access_log']['s3_bucket_prefix'] = al.s3_bucket_prefix + ret['access_log']['emit_interval'] = al.emit_interval + ret['cross_zone_load_balancing']['enabled'] = czlb.enabled + ret['connection_draining']['enabled'] = cd.enabled + ret['connection_draining']['timeout'] = cd.timeout + ret['connecting_settings']['idle_timeout'] = cs.idle_timeout + return ret + except boto.exception.BotoServerError as e: + if e.error_code == 'Throttling': + log.debug("Throttled by AWS API, will retry in 5 seconds...") + time.sleep(5) + retries -= 1 + continue + log.error('ELB {0} does not exist: {1}'.format(name, e.message)) + return {} + return {} def set_attributes(name, attributes, region=None, key=None, keyid=None, diff --git a/salt/modules/boto_sqs.py b/salt/modules/boto_sqs.py index eb44f4a949..034ba11802 100644 --- a/salt/modules/boto_sqs.py +++ b/salt/modules/boto_sqs.py @@ -78,7 +78,7 @@ def __virtual__(): ''' if not HAS_BOTO3: return (False, 'The boto_sqs module could not be loaded: boto3 libraries not found') - __utils__['boto3.assign_funcs'](__name__, 'sqs', pack=__salt__) + __utils__['boto3.assign_funcs'](__name__, 'sqs') return True diff --git a/salt/modules/boto_vpc.py b/salt/modules/boto_vpc.py index 7dbe9b1eb1..fa9a93c59a 100644 --- a/salt/modules/boto_vpc.py +++ b/salt/modules/boto_vpc.py @@ -2456,11 +2456,10 @@ def describe_route_table(route_table_id=None, route_table_name=None, salt myminion boto_vpc.describe_route_table route_table_id='rtb-1f382e7d' ''' - salt.utils.versions.warn_until( - 'Oxygen', - 'The \'describe_route_table\' method has been deprecated and ' - 'replaced by \'describe_route_tables\'.' + 'Neon', + 'The \'describe_route_table\' method has been deprecated and ' + 'replaced by \'describe_route_tables\'.' ) if not any((route_table_id, route_table_name, tags)): raise SaltInvocationError('At least one of the following must be specified: ' diff --git a/salt/modules/cimc.py b/salt/modules/cimc.py new file mode 100644 index 0000000000..ddadaec6a4 --- /dev/null +++ b/salt/modules/cimc.py @@ -0,0 +1,710 @@ +# -*- coding: utf-8 -*- +''' +Module to provide Cisco UCS compatibility to Salt. + +:codeauthor: :email:`Spencer Ervin ` +:maturity: new +:depends: none +:platform: unix + + +Configuration +============= +This module accepts connection configuration details either as +parameters, or as configuration settings in pillar as a Salt proxy. +Options passed into opts will be ignored if options are passed into pillar. + +.. seealso:: + :prox:`Cisco UCS Proxy Module ` + +About +===== +This execution module was designed to handle connections to a Cisco UCS server. This module adds support to send +connections directly to the device through the rest API. + +''' + +# Import Python Libs +from __future__ import absolute_import +import logging + +# Import Salt Libs +import salt.utils.platform +import salt.proxy.cimc + +log = logging.getLogger(__name__) + +__virtualname__ = 'cimc' + + +def __virtual__(): + ''' + Will load for the cimc proxy minions. + ''' + try: + if salt.utils.platform.is_proxy() and \ + __opts__['proxy']['proxytype'] == 'cimc': + return __virtualname__ + except KeyError: + pass + + return False, 'The cimc execution module can only be loaded for cimc proxy minions.' + + +def activate_backup_image(reset=False): + ''' + Activates the firmware backup image. + + CLI Example: + + Args: + reset(bool): Reset the CIMC device on activate. + + .. code-block:: bash + + salt '*' cimc.activate_backup_image + salt '*' cimc.activate_backup_image reset=True + + ''' + + dn = "sys/rack-unit-1/mgmt/fw-boot-def/bootunit-combined" + + r = "no" + + if reset is True: + r = "yes" + + inconfig = """""".format(r) + + ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False) + + return ret + + +def create_user(uid=None, username=None, password=None, priv=None): + ''' + Create a CIMC user with username and password. + + Args: + uid(int): The user ID slot to create the user account in. + + username(str): The name of the user. + + password(str): The clear text password of the user. + + priv(str): The privilege level of the user. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.create_user 11 username=admin password=foobar priv=admin + + ''' + + if not uid: + raise salt.exceptions.CommandExecutionError("The user ID must be specified.") + + if not username: + raise salt.exceptions.CommandExecutionError("The username must be specified.") + + if not password: + raise salt.exceptions.CommandExecutionError("The password must be specified.") + + if not priv: + raise salt.exceptions.CommandExecutionError("The privilege level must be specified.") + + dn = "sys/user-ext/user-{0}".format(uid) + + inconfig = """""".format(uid, + username, + priv, + password) + + ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False) + + return ret + + +def get_bios_defaults(): + ''' + Get the default values of BIOS tokens. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_bios_defaults + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('biosPlatformDefaults', True) + + return ret + + +def get_bios_settings(): + ''' + Get the C240 server BIOS token values. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_bios_settings + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('biosSettings', True) + + return ret + + +def get_boot_order(): + ''' + Retrieves the configured boot order table. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_boot_order + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('lsbootDef', True) + + return ret + + +def get_cpu_details(): + ''' + Get the CPU product ID details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_cpu_details + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('pidCatalogCpu', True) + + return ret + + +def get_disks(): + ''' + Get the HDD product ID details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_disks + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('pidCatalogHdd', True) + + return ret + + +def get_ethernet_interfaces(): + ''' + Get the adapter Ethernet interface details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_ethernet_interfaces + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('adaptorHostEthIf', True) + + return ret + + +def get_fibre_channel_interfaces(): + ''' + Get the adapter fibre channel interface details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_fibre_channel_interfaces + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('adaptorHostFcIf', True) + + return ret + + +def get_firmware(): + ''' + Retrieves the current running firmware versions of server components. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_firmware + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('firmwareRunning', False) + + return ret + + +def get_ldap(): + ''' + Retrieves LDAP server details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_ldap + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('aaaLdap', True) + + return ret + + +def get_management_interface(): + ''' + Retrieve the management interface details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_management_interface + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('mgmtIf', False) + + return ret + + +def get_memory_token(): + ''' + Get the memory RAS BIOS token. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_memory_token + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('biosVfSelectMemoryRASConfiguration', False) + + return ret + + +def get_memory_unit(): + ''' + Get the IMM/Memory unit product ID details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_memory_unit + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('pidCatalogDimm', True) + + return ret + + +def get_network_adapters(): + ''' + Get the list of network adapaters and configuration details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_network_adapters + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('networkAdapterEthIf', True) + + return ret + + +def get_ntp(): + ''' + Retrieves the current running NTP configuration. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_ntp + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('commNtpProvider', False) + + return ret + + +def get_pci_adapters(): + ''' + Get the PCI adapter product ID details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_disks + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('pidCatalogPCIAdapter', True) + + return ret + + +def get_power_supplies(): + ''' + Retrieves the power supply unit details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_power_supplies + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('equipmentPsu', False) + + return ret + + +def get_snmp_config(): + ''' + Get the snmp configuration details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_snmp_config + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('commSnmp', False) + + return ret + + +def get_syslog(): + ''' + Get the Syslog client-server details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_syslog + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('commSyslogClient', False) + + return ret + + +def get_system_info(): + ''' + Get the system information. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_system_info + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('computeRackUnit', False) + + return ret + + +def get_users(): + ''' + Get the CIMC users. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_users + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('aaaUser', False) + + return ret + + +def get_vic_adapters(): + ''' + Get the VIC adapter general profile details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_vic_adapters + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('adaptorGenProfile', True) + + return ret + + +def get_vic_uplinks(): + ''' + Get the VIC adapter uplink port details. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.get_vic_uplinks + + ''' + ret = __proxy__['cimc.get_config_resolver_class']('adaptorExtEthIf', True) + + return ret + + +def mount_share(name=None, + remote_share=None, + remote_file=None, + mount_type="nfs", + username=None, + password=None): + ''' + Mounts a remote file through a remote share. Currently, this feature is supported in version 1.5 or greater. + The remote share can be either NFS, CIFS, or WWW. + + Some of the advantages of CIMC Mounted vMedia include: + Communication between mounted media and target stays local (inside datacenter) + Media mounts can be scripted/automated + No vKVM requirements for media connection + Multiple share types supported + Connections supported through all CIMC interfaces + + Note: CIMC Mounted vMedia is enabled through BIOS configuration. + + Args: + name(str): The name of the volume on the CIMC device. + + remote_share(str): The file share link that will be used to mount the share. This can be NFS, CIFS, or WWW. This + must be the directory path and not the full path to the remote file. + + remote_file(str): The name of the remote file to mount. It must reside within remote_share. + + mount_type(str): The type of share to mount. Valid options are nfs, cifs, and www. + + username(str): An optional requirement to pass credentials to the remote share. If not provided, an + unauthenticated connection attempt will be made. + + password(str): An optional requirement to pass a password to the remote share. If not provided, an + unauthenticated connection attempt will be made. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.mount_share name=WIN7 remote_share=10.xxx.27.xxx:/nfs remote_file=sl1huu.iso + + salt '*' cimc.mount_share name=WIN7 remote_share=10.xxx.27.xxx:/nfs remote_file=sl1huu.iso username=bob password=badpassword + + ''' + + if not name: + raise salt.exceptions.CommandExecutionError("The share name must be specified.") + + if not remote_share: + raise salt.exceptions.CommandExecutionError("The remote share path must be specified.") + + if not remote_file: + raise salt.exceptions.CommandExecutionError("The remote file name must be specified.") + + if username and password: + mount_options = " mountOptions='username={0},password={1}'".format(username, password) + else: + mount_options = "" + + dn = 'sys/svc-ext/vmedia-svc/vmmap-{0}'.format(name) + inconfig = """""".format(name, mount_type, mount_options, remote_file, remote_share) + + ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False) + + return ret + + +def reboot(): + ''' + Power cycling the server. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.reboot + + ''' + + dn = "sys/rack-unit-1" + + inconfig = """""" + + ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False) + + return ret + + +def set_ntp_server(server1='', server2='', server3='', server4=''): + ''' + Sets the NTP servers configuration. This will also enable the client NTP service. + + Args: + server1(str): The first IP address or FQDN of the NTP servers. + + server2(str): The second IP address or FQDN of the NTP servers. + + server3(str): The third IP address or FQDN of the NTP servers. + + server4(str): The fourth IP address or FQDN of the NTP servers. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.set_ntp_server 10.10.10.1 + + salt '*' cimc.set_ntp_server 10.10.10.1 foo.bar.com + + ''' + + dn = "sys/svc-ext/ntp-svc" + inconfig = """""".format(server1, server2, server3, server4) + + ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False) + + return ret + + +def set_syslog_server(server=None, type="primary"): + ''' + Set the SYSLOG server on the host. + + Args: + server(str): The hostname or IP address of the SYSLOG server. + + type(str): Specifies the type of SYSLOG server. This can either be primary (default) or secondary. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.set_syslog_server foo.bar.com + + salt '*' cimc.set_syslog_server foo.bar.com primary + + salt '*' cimc.set_syslog_server foo.bar.com secondary + + ''' + + if not server: + raise salt.exceptions.CommandExecutionError("The SYSLOG server must be specified.") + + if type == "primary": + dn = "sys/svc-ext/syslog/client-primary" + inconfig = """ """.format(server) + elif type == "secondary": + dn = "sys/svc-ext/syslog/client-secondary" + inconfig = """ """.format(server) + else: + raise salt.exceptions.CommandExecutionError("The SYSLOG type must be either primary or secondary.") + + ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False) + + return ret + + +def tftp_update_bios(server=None, path=None): + ''' + Update the BIOS firmware through TFTP. + + Args: + server(str): The IP address or hostname of the TFTP server. + + path(str): The TFTP path and filename for the BIOS image. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.tftp_update_bios foo.bar.com HP-SL2.cap + + ''' + + if not server: + raise salt.exceptions.CommandExecutionError("The server name must be specified.") + + if not path: + raise salt.exceptions.CommandExecutionError("The TFTP path must be specified.") + + dn = "sys/rack-unit-1/bios/fw-updatable" + + inconfig = """""".format(server, path) + + ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False) + + return ret + + +def tftp_update_cimc(server=None, path=None): + ''' + Update the CIMC firmware through TFTP. + + Args: + server(str): The IP address or hostname of the TFTP server. + + path(str): The TFTP path and filename for the CIMC image. + + CLI Example: + + .. code-block:: bash + + salt '*' cimc.tftp_update_cimc foo.bar.com HP-SL2.bin + + ''' + + if not server: + raise salt.exceptions.CommandExecutionError("The server name must be specified.") + + if not path: + raise salt.exceptions.CommandExecutionError("The TFTP path must be specified.") + + dn = "sys/rack-unit-1/mgmt/fw-updatable" + + inconfig = """""".format(server, path) + + ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False) + + return ret diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index d961b8490c..653e9a170b 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -2120,12 +2120,7 @@ def script(source, ) if '__env__' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'__env__\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('__env__') if salt.utils.platform.is_windows() and runas and cwd is None: @@ -2336,12 +2331,7 @@ def script_retcode(source, salt '*' cmd.script_retcode salt://scripts/runme.sh stdin='one\\ntwo\\nthree\\nfour\\nfive\\n' ''' if '__env__' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'__env__\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('__env__') return script(source=source, diff --git a/salt/modules/cp.py b/salt/modules/cp.py index 86634d559c..cdbeb4434e 100644 --- a/salt/modules/cp.py +++ b/salt/modules/cp.py @@ -352,7 +352,7 @@ def get_dir(path, dest, saltenv='base', template=None, gzip=None, **kwargs): return _client().get_dir(path, dest, saltenv, gzip) -def get_url(path, dest='', saltenv='base', makedirs=False): +def get_url(path, dest='', saltenv='base', makedirs=False, source_hash=None): ''' .. versionchanged:: Oxygen ``dest`` can now be a directory @@ -386,6 +386,13 @@ def get_url(path, dest='', saltenv='base', makedirs=False): Salt fileserver envrionment from which to retrieve the file. Ignored if ``path`` is not a ``salt://`` URL. + source_hash + If ``path`` is an http(s) or ftp URL and the file exists in the + minion's file cache, this option can be passed to keep the minion from + re-downloading the file if the cached copy matches the specified hash. + + .. versionadded:: Oxygen + CLI Example: .. code-block:: bash @@ -394,9 +401,11 @@ def get_url(path, dest='', saltenv='base', makedirs=False): salt '*' cp.get_url http://www.slashdot.org /tmp/index.html ''' if isinstance(dest, six.string_types): - result = _client().get_url(path, dest, makedirs, saltenv) + result = _client().get_url( + path, dest, makedirs, saltenv, source_hash=source_hash) else: - result = _client().get_url(path, None, makedirs, saltenv, no_cache=True) + result = _client().get_url( + path, None, makedirs, saltenv, no_cache=True, source_hash=source_hash) if not result: log.error( 'Unable to fetch file {0} from saltenv {1}.'.format( @@ -429,11 +438,18 @@ def get_file_str(path, saltenv='base'): return fn_ -def cache_file(path, saltenv='base'): +def cache_file(path, saltenv='base', source_hash=None): ''' Used to cache a single file on the Minion - Returns the location of the new cached file on the Minion. + Returns the location of the new cached file on the Minion + + source_hash + If ``name`` is an http(s) or ftp URL and the file exists in the + minion's file cache, this option can be passed to keep the minion from + re-downloading the file if the cached copy matches the specified hash. + + .. versionadded:: Oxygen CLI Example: @@ -485,7 +501,7 @@ def cache_file(path, saltenv='base'): if senv: saltenv = senv - result = _client().cache_file(path, saltenv) + result = _client().cache_file(path, saltenv, source_hash=source_hash) if not result: log.error( u'Unable to cache file \'%s\' from saltenv \'%s\'.', diff --git a/salt/modules/debconfmod.py b/salt/modules/debconfmod.py index 2185b2af21..18e19d1cce 100644 --- a/salt/modules/debconfmod.py +++ b/salt/modules/debconfmod.py @@ -186,12 +186,7 @@ def set_file(path, saltenv='base', **kwargs): salt '*' debconf.set_file salt://pathto/pkg.selections ''' if '__env__' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'__env__\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('__env__') path = __salt__['cp.cache_file'](path, saltenv) diff --git a/salt/modules/debian_ip.py b/salt/modules/debian_ip.py index 60b60f893c..6b5c760c92 100644 --- a/salt/modules/debian_ip.py +++ b/salt/modules/debian_ip.py @@ -2038,19 +2038,12 @@ def build_network_settings(**settings): # Write settings _write_file_network(network, _DEB_NETWORKING_FILE, True) - # Write hostname to /etc/hostname + # Get hostname and domain from opts sline = opts['hostname'].split('.', 1) opts['hostname'] = sline[0] - hostname = '{0}\n' . format(opts['hostname']) current_domainname = current_network_settings['domainname'] current_searchdomain = current_network_settings['searchdomain'] - # Only write the hostname if it has changed - if not opts['hostname'] == current_network_settings['hostname']: - if not ('test' in settings and settings['test']): - # TODO replace wiht a call to network.mod_hostname instead - _write_file_network(hostname, _DEB_HOSTNAME_FILE) - new_domain = False if len(sline) > 1: new_domainname = sline[1] diff --git a/salt/modules/dockermod.py b/salt/modules/dockermod.py index 31b7c83422..5e9ef7d3b4 100644 --- a/salt/modules/dockermod.py +++ b/salt/modules/dockermod.py @@ -234,6 +234,7 @@ except ImportError: # pylint: enable=import-error HAS_NSENTER = bool(salt.utils.path.which('nsenter')) +HUB_PREFIX = 'docker.io/' # Set up logging log = logging.getLogger(__name__) @@ -1489,6 +1490,43 @@ def list_tags(): return sorted(ret) +def resolve_tag(name, tags=None): + ''' + .. versionadded:: 2017.7.2,Oxygen + + Given an image tag, check the locally-pulled tags (using + :py:func:`docker.list_tags `) and return + the matching tag. This helps disambiguate differences on some platforms + where images from the Docker Hub are prefixed with ``docker.io/``. If an + image name with no tag is passed, a tag of ``latest`` is assumed. + + If the specified image is not pulled locally, this function will return + ``False``. + + tags + An optional Python list of tags to check against. If passed, then + :py:func:`docker.list_tags ` will not + be run to get a list of tags. This is useful when resolving a number of + tags at the same time. + + CLI Examples: + + .. code-block:: bash + + salt myminion docker.resolve_tag busybox + salt myminion docker.resolve_tag busybox:latest + ''' + tag_name = ':'.join(salt.utils.docker.get_repo_tag(name)) + if tags is None: + tags = list_tags() + if tag_name in tags: + return tag_name + full_name = HUB_PREFIX + tag_name + if not name.startswith(HUB_PREFIX) and full_name in tags: + return full_name + return False + + def logs(name): ''' Returns the logs for the container. Equivalent to running the ``docker @@ -5420,7 +5458,7 @@ def sls(name, mods=None, saltenv='base', **kwargs): ) if not isinstance(ret, dict): __context__['retcode'] = 1 - elif not salt.utils.check_state_result(ret): + elif not __utils__['state.check_result'](ret): __context__['retcode'] = 2 else: __context__['retcode'] = 0 @@ -5494,7 +5532,7 @@ def sls_build(name, base='opensuse/python', mods=None, saltenv='base', # Now execute the state into the container ret = sls(id_, mods, saltenv, **kwargs) # fail if the state was not successful - if not dryrun and not salt.utils.check_state_result(ret): + if not dryrun and not __utils__['state.check_result'](ret): raise CommandExecutionError(ret) if dryrun is False: ret = commit(id_, name) diff --git a/salt/modules/ebuild.py b/salt/modules/ebuild.py index ea41656eaf..5295c2652c 100644 --- a/salt/modules/ebuild.py +++ b/salt/modules/ebuild.py @@ -75,9 +75,20 @@ def _porttree(): def _p_to_cp(p): - ret = _porttree().dbapi.xmatch("match-all", p) - if ret: - return portage.cpv_getkey(ret[0]) + try: + ret = portage.dep_getkey(p) + if ret: + return ret + except portage.exception.InvalidAtom: + pass + + try: + ret = _porttree().dbapi.xmatch('bestmatch-visible', p) + if ret: + return portage.dep_getkey(ret) + except portage.exception.InvalidAtom: + pass + return None @@ -91,11 +102,14 @@ def _allnodes(): def _cpv_to_cp(cpv): - ret = portage.cpv_getkey(cpv) - if ret: - return ret - else: - return cpv + try: + ret = portage.dep_getkey(cpv) + if ret: + return ret + except portage.exception.InvalidAtom: + pass + + return cpv def _cpv_to_version(cpv): diff --git a/salt/modules/elasticsearch.py b/salt/modules/elasticsearch.py index 9be0ce20f5..9d81164383 100644 --- a/salt/modules/elasticsearch.py +++ b/salt/modules/elasticsearch.py @@ -88,6 +88,7 @@ def _get_instance(hosts=None, profile=None): ca_certs = None verify_certs = True http_auth = None + timeout = 10 if profile is None: profile = 'elasticsearch' @@ -106,6 +107,7 @@ def _get_instance(hosts=None, profile=None): verify_certs = _profile.get('verify_certs', True) username = _profile.get('username', None) password = _profile.get('password', None) + timeout = _profile.get('timeout', 10) if username and password: http_auth = (username, password) @@ -131,6 +133,7 @@ def _get_instance(hosts=None, profile=None): ca_certs=ca_certs, verify_certs=verify_certs, http_auth=http_auth, + timeout=timeout, ) else: es = elasticsearch.Elasticsearch( @@ -139,6 +142,7 @@ def _get_instance(hosts=None, profile=None): ca_certs=ca_certs, verify_certs=verify_certs, http_auth=http_auth, + timeout=timeout, ) # Try the connection diff --git a/salt/modules/esxcluster.py b/salt/modules/esxcluster.py new file mode 100644 index 0000000000..fca68d775f --- /dev/null +++ b/salt/modules/esxcluster.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +''' +Module used to access the esxcluster proxy connection methods +''' +from __future__ import absolute_import + +# Import python libs +import logging +import salt.utils.platform + + +log = logging.getLogger(__name__) + +__proxyenabled__ = ['esxcluster'] +# Define the module's virtual name +__virtualname__ = 'esxcluster' + + +def __virtual__(): + ''' + Only work on proxy + ''' + if salt.utils.platform.is_proxy(): + return __virtualname__ + return (False, 'Must be run on a proxy minion') + + +def get_details(): + return __proxy__['esxcluster.get_details']() diff --git a/salt/modules/file.py b/salt/modules/file.py index c43a371afe..7dfd5ced01 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -60,6 +60,7 @@ import salt.utils.stringutils import salt.utils.templates import salt.utils.url from salt.exceptions import CommandExecutionError, MinionError, SaltInvocationError, get_error_message as _get_error_message +from salt.utils.files import HASHES, HASHES_REVMAP log = logging.getLogger(__name__) @@ -67,16 +68,6 @@ __func_alias__ = { 'makedirs_': 'makedirs' } -HASHES = { - 'sha512': 128, - 'sha384': 96, - 'sha256': 64, - 'sha224': 56, - 'sha1': 40, - 'md5': 32, -} -HASHES_REVMAP = dict([(y, x) for x, y in six.iteritems(HASHES)]) - def __virtual__(): ''' @@ -126,8 +117,8 @@ def _binary_replace(old, new): This function should only be run AFTER it has been determined that the files differ. ''' - old_isbin = not salt.utils.istextfile(old) - new_isbin = not salt.utils.istextfile(new) + old_isbin = not __utils__['files.is_text_file'](old) + new_isbin = not __utils__['files.is_text_file'](new) if any((old_isbin, new_isbin)): if all((old_isbin, new_isbin)): return u'Replace binary file' @@ -1436,7 +1427,7 @@ def comment_line(path, raise SaltInvocationError('File not found: {0}'.format(path)) # Make sure it is a text file - if not salt.utils.istextfile(path): + if not __utils__['files.is_text_file'](path): raise SaltInvocationError( 'Cannot perform string replacements on a binary file: {0}'.format(path)) @@ -2180,7 +2171,7 @@ def replace(path, else: raise SaltInvocationError('File not found: {0}'.format(path)) - if not salt.utils.istextfile(path): + if not __utils__['files.is_text_file'](path): raise SaltInvocationError( 'Cannot perform string replacements on a binary file: {0}' .format(path) @@ -2497,7 +2488,7 @@ def blockreplace(path, 'Only one of append and prepend_if_not_found is permitted' ) - if not salt.utils.istextfile(path): + if not __utils__['files.is_text_file'](path): raise SaltInvocationError( 'Cannot perform string replacements on a binary file: {0}' .format(path) @@ -3767,14 +3758,8 @@ def source_list(source, source_hash, saltenv): ret = (single_src, single_hash) break elif proto.startswith('http') or proto == 'ftp': - try: - if __salt__['cp.cache_file'](single_src): - ret = (single_src, single_hash) - break - except MinionError as exc: - # Error downloading file. Log the caught exception and - # continue on to the next source. - log.exception(exc) + ret = (single_src, single_hash) + break elif proto == 'file' and os.path.exists(urlparsed_single_src.path): ret = (single_src, single_hash) break @@ -3794,9 +3779,8 @@ def source_list(source, source_hash, saltenv): ret = (single, source_hash) break elif proto.startswith('http') or proto == 'ftp': - if __salt__['cp.cache_file'](single): - ret = (single, source_hash) - break + ret = (single, source_hash) + break elif single.startswith('/') and os.path.exists(single): ret = (single, source_hash) break @@ -4007,11 +3991,14 @@ def get_managed( else: sfn = cached_dest - # If we didn't have the template or remote file, let's get it - # Similarly when the file has been updated and the cache has to be refreshed + # If we didn't have the template or remote file, or the file has been + # updated and the cache has to be refreshed, download the file. if not sfn or cache_refetch: try: - sfn = __salt__['cp.cache_file'](source, saltenv) + sfn = __salt__['cp.cache_file']( + source, + saltenv, + source_hash=source_sum.get('hsum')) except Exception as exc: # A 404 or other error code may raise an exception, catch it # and return a comment that will fail the calling state. @@ -4675,7 +4662,7 @@ def check_file_meta( ''' changes = {} if not source_sum: - source_sum = dict() + source_sum = {} lstats = stats(name, hash_type=source_sum.get('hash_type', None), follow_symlinks=False) if not lstats: changes['newfile'] = name @@ -4683,7 +4670,10 @@ def check_file_meta( if 'hsum' in source_sum: if source_sum['hsum'] != lstats['sum']: if not sfn and source: - sfn = __salt__['cp.cache_file'](source, saltenv) + sfn = __salt__['cp.cache_file']( + source, + saltenv, + source_hash=source_sum['hsum']) if sfn: try: changes['diff'] = get_diff( @@ -4750,7 +4740,9 @@ def get_diff(file1, saltenv='base', show_filenames=True, show_changes=True, - template=False): + template=False, + source_hash_file1=None, + source_hash_file2=None): ''' Return unified diff of two files @@ -4785,6 +4777,22 @@ def get_diff(file1, .. versionadded:: Oxygen + source_hash_file1 + If ``file1`` is an http(s)/ftp URL and the file exists in the minion's + file cache, this option can be passed to keep the minion from + re-downloading the archive if the cached copy matches the specified + hash. + + .. versionadded:: Oxygen + + source_hash_file2 + If ``file2`` is an http(s)/ftp URL and the file exists in the minion's + file cache, this option can be passed to keep the minion from + re-downloading the archive if the cached copy matches the specified + hash. + + .. versionadded:: Oxygen + CLI Examples: .. code-block:: bash @@ -4793,14 +4801,17 @@ def get_diff(file1, salt '*' file.get_diff /tmp/foo.txt /tmp/bar.txt ''' files = (file1, file2) + source_hashes = (source_hash_file1, source_hash_file2) paths = [] errors = [] - for filename in files: + for filename, source_hash in zip(files, source_hashes): try: # Local file paths will just return the same path back when passed # to cp.cache_file. - cached_path = __salt__['cp.cache_file'](filename, saltenv) + cached_path = __salt__['cp.cache_file'](filename, + saltenv, + source_hash=source_hash) if cached_path is False: errors.append( u'File {0} not found'.format( diff --git a/salt/modules/genesis.py b/salt/modules/genesis.py index a1a45025c9..ab0eed5138 100644 --- a/salt/modules/genesis.py +++ b/salt/modules/genesis.py @@ -17,13 +17,16 @@ except ImportError: from pipes import quote as _cmd_quote # Import salt libs -import salt.utils.path -import salt.utils.yast -import salt.utils.preseed -import salt.utils.kickstart import salt.syspaths +import salt.utils.kickstart +import salt.utils.path +import salt.utils.preseed +import salt.utils.validate.path +import salt.utils.yast from salt.exceptions import SaltInvocationError +# Import 3rd-party libs +from salt.ext import six log = logging.getLogger(__name__) @@ -325,6 +328,8 @@ def _bootstrap_yum( ''' if pkgs is None: pkgs = [] + elif isinstance(pkgs, six.string_types): + pkgs = pkgs.split(',') default_pkgs = ('yum', 'centos-release', 'iputils') for pkg in default_pkgs: @@ -333,6 +338,8 @@ def _bootstrap_yum( if exclude_pkgs is None: exclude_pkgs = [] + elif isinstance(exclude_pkgs, six.string_types): + exclude_pkgs = exclude_pkgs.split(',') for pkg in exclude_pkgs: pkgs.remove(pkg) @@ -393,15 +400,32 @@ def _bootstrap_deb( if repo_url is None: repo_url = 'http://ftp.debian.org/debian/' + if not salt.utils.path.which('debootstrap'): + log.error('Required tool debootstrap is not installed.') + return False + + if static_qemu and not salt.utils.validate.path.is_executable(static_qemu): + log.error('Required tool qemu not ' + 'present/readable at: {0}'.format(static_qemu)) + return False + + if isinstance(pkgs, (list, tuple)): + pkgs = ','.join(pkgs) + if isinstance(exclude_pkgs, (list, tuple)): + exclude_pkgs = ','.join(exclude_pkgs) + deb_args = [ 'debootstrap', '--foreign', '--arch', - _cmd_quote(arch), - '--include', - ] + pkgs + [ - '--exclude', - ] + exclude_pkgs + [ + _cmd_quote(arch)] + + if pkgs: + deb_args += ['--include', _cmd_quote(pkgs)] + if exclude_pkgs: + deb_args += ['--exclude', _cmd_quote(exclude_pkgs)] + + deb_args += [ _cmd_quote(flavor), _cmd_quote(root), _cmd_quote(repo_url), @@ -409,11 +433,13 @@ def _bootstrap_deb( __salt__['cmd.run'](deb_args, python_shell=False) - __salt__['cmd.run']( - 'cp {qemu} {root}/usr/bin/'.format( - qemu=_cmd_quote(static_qemu), root=_cmd_quote(root) + if static_qemu: + __salt__['cmd.run']( + 'cp {qemu} {root}/usr/bin/'.format( + qemu=_cmd_quote(static_qemu), root=_cmd_quote(root) + ) ) - ) + env = {'DEBIAN_FRONTEND': 'noninteractive', 'DEBCONF_NONINTERACTIVE_SEEN': 'true', 'LC_ALL': 'C', @@ -469,6 +495,8 @@ def _bootstrap_pacman( if pkgs is None: pkgs = [] + elif isinstance(pkgs, six.string_types): + pkgs = pkgs.split(',') default_pkgs = ('pacman', 'linux', 'systemd-sysvcompat', 'grub') for pkg in default_pkgs: @@ -477,6 +505,8 @@ def _bootstrap_pacman( if exclude_pkgs is None: exclude_pkgs = [] + elif isinstance(exclude_pkgs, six.string_types): + exclude_pkgs = exclude_pkgs.split(',') for pkg in exclude_pkgs: pkgs.remove(pkg) diff --git a/salt/modules/groupadd.py b/salt/modules/groupadd.py index 2f79c47dd9..f02a5811a7 100644 --- a/salt/modules/groupadd.py +++ b/salt/modules/groupadd.py @@ -31,7 +31,7 @@ def __virtual__(): if __grains__['kernel'] in ('Linux', 'OpenBSD', 'NetBSD'): return __virtualname__ return (False, 'The groupadd execution module cannot be loaded: ' - ' only available on Linux, OpenBSD and NetBSD') + ' only available on Linux, OpenBSD and NetBSD') def add(name, gid=None, system=False, root=None): @@ -44,12 +44,12 @@ def add(name, gid=None, system=False, root=None): salt '*' group.add foo 3456 ''' - cmd = 'groupadd ' + cmd = ['groupadd'] if gid: - cmd += '-g {0} '.format(gid) + cmd.append('-g {0}'.format(gid)) if system and __grains__['kernel'] != 'OpenBSD': - cmd += '-r ' - cmd += name + cmd.append('-r') + cmd.append(name) if root is not None: cmd.extend(('-R', root)) @@ -69,7 +69,7 @@ def delete(name, root=None): salt '*' group.delete foo ''' - cmd = ('groupdel', name) + cmd = ['groupdel', name] if root is not None: cmd.extend(('-R', root)) @@ -140,7 +140,7 @@ def chgid(name, gid, root=None): pre_gid = __salt__['file.group_to_gid'](name) if gid == pre_gid: return True - cmd = ('groupmod', '-g', gid, name) + cmd = ['groupmod', '-g', gid, name] if root is not None: cmd.extend(('-R', root)) @@ -170,15 +170,15 @@ def adduser(name, username, root=None): if __grains__['kernel'] == 'Linux': if on_redhat_5: - cmd = ('gpasswd', '-a', username, name) + cmd = ['gpasswd', '-a', username, name] elif on_suse_11: - cmd = ('usermod', '-A', name, username) + cmd = ['usermod', '-A', name, username] else: - cmd = ('gpasswd', '--add', username, name) + cmd = ['gpasswd', '--add', username, name] if root is not None: cmd.extend(('-Q', root)) else: - cmd = ('usermod', '-G', name, username) + cmd = ['usermod', '-G', name, username] if root is not None: cmd.extend(('-R', root)) @@ -208,20 +208,20 @@ def deluser(name, username, root=None): if username in grp_info['members']: if __grains__['kernel'] == 'Linux': if on_redhat_5: - cmd = ('gpasswd', '-d', username, name) + cmd = ['gpasswd', '-d', username, name] elif on_suse_11: - cmd = ('usermod', '-R', name, username) + cmd = ['usermod', '-R', name, username] else: - cmd = ('gpasswd', '--del', username, name) + cmd = ['gpasswd', '--del', username, name] if root is not None: cmd.extend(('-R', root)) retcode = __salt__['cmd.retcode'](cmd, python_shell=False) elif __grains__['kernel'] == 'OpenBSD': out = __salt__['cmd.run_stdout']('id -Gn {0}'.format(username), python_shell=False) - cmd = 'usermod -S ' - cmd += ','.join([g for g in out.split() if g != str(name)]) - cmd += ' {0}'.format(username) + cmd = ['usermod', '-S'] + cmd.append(','.join([g for g in out.split() if g != str(name)])) + cmd.append('{0}'.format(username)) retcode = __salt__['cmd.retcode'](cmd, python_shell=False) else: log.error('group.deluser is not yet supported on this platform') @@ -249,13 +249,13 @@ def members(name, members_list, root=None): if __grains__['kernel'] == 'Linux': if on_redhat_5: - cmd = ('gpasswd', '-M', members_list, name) + cmd = ['gpasswd', '-M', members_list, name] elif on_suse_11: for old_member in __salt__['group.info'](name).get('members'): __salt__['cmd.run']('groupmod -R {0} {1}'.format(old_member, name), python_shell=False) - cmd = ('groupmod', '-A', members_list, name) + cmd = ['groupmod', '-A', members_list, name] else: - cmd = ('gpasswd', '--members', members_list, name) + cmd = ['gpasswd', '--members', members_list, name] if root is not None: cmd.extend(('-R', root)) retcode = __salt__['cmd.retcode'](cmd, python_shell=False) @@ -270,7 +270,7 @@ def members(name, members_list, root=None): for user in members_list.split(","): if user: retcode = __salt__['cmd.retcode']( - 'usermod -G {0} {1}'.format(name, user), + ['usermod', '-G', name, user], python_shell=False) if not retcode == 0: break diff --git a/salt/modules/ini_manage.py b/salt/modules/ini_manage.py index 5b74e5a1b4..9af24eda19 100644 --- a/salt/modules/ini_manage.py +++ b/salt/modules/ini_manage.py @@ -318,17 +318,18 @@ class _Section(OrderedDict): yield '{0}[{1}]{0}'.format(os.linesep, self.name) sections_dict = OrderedDict() for name, value in six.iteritems(self): + # Handle Comment Lines if com_regx.match(name): yield '{0}{1}'.format(value, os.linesep) + # Handle Sections elif isinstance(value, _Section): sections_dict.update({name: value}) + # Key / Value pairs + # Adds spaces between the separator else: yield '{0}{1}{2}{3}'.format( name, - ( - ' {0} '.format(self.sep) if self.sep != ' ' - else self.sep - ), + ' {0} '.format(self.sep) if self.sep != ' ' else self.sep, value, os.linesep ) diff --git a/salt/modules/inspectlib/collector.py b/salt/modules/inspectlib/collector.py index e8c1123daf..d67b2519bb 100644 --- a/salt/modules/inspectlib/collector.py +++ b/salt/modules/inspectlib/collector.py @@ -31,6 +31,7 @@ from salt.modules.inspectlib.entities import (AllowedDir, IgnoredDir, Package, import salt.utils # Can be removed when reinit_crypto is moved import salt.utils.files import salt.utils.fsutils +import salt.utils.path import salt.utils.stringutils from salt.exceptions import CommandExecutionError @@ -312,7 +313,7 @@ class Inspector(EnvLoader): continue if not valid or not os.path.exists(obj) or not os.access(obj, os.R_OK): continue - if os.path.islink(obj): + if salt.utils.path.islink(obj): links.append(obj) elif os.path.isdir(obj): dirs.append(obj) diff --git a/salt/modules/inspectlib/kiwiproc.py b/salt/modules/inspectlib/kiwiproc.py index 8ce3a22cd2..c9b42344da 100644 --- a/salt/modules/inspectlib/kiwiproc.py +++ b/salt/modules/inspectlib/kiwiproc.py @@ -17,11 +17,14 @@ # Import python libs from __future__ import absolute_import import os -import grp -import pwd from xml.dom import minidom import platform import socket +try: + import grp + import pwd +except ImportError: + pass # Import salt libs import salt.utils.files diff --git a/salt/modules/iptables.py b/salt/modules/iptables.py index 18ccdd14f7..3123fb58fc 100644 --- a/salt/modules/iptables.py +++ b/salt/modules/iptables.py @@ -1463,6 +1463,8 @@ def _parser(): add_arg('--or-mark', dest='or-mark', action='append') add_arg('--xor-mark', dest='xor-mark', action='append') add_arg('--set-mark', dest='set-mark', action='append') + add_arg('--nfmask', dest='nfmask', action='append') + add_arg('--ctmask', dest='ctmask', action='append') ## CONNSECMARK add_arg('--save', dest='save', action='append') add_arg('--restore', dest='restore', action='append') diff --git a/salt/modules/junos.py b/salt/modules/junos.py index c6e3e64c6d..f6ae3e4dc4 100644 --- a/salt/modules/junos.py +++ b/salt/modules/junos.py @@ -27,6 +27,7 @@ except ImportError: # Import Salt libs import salt.utils.files +from salt.ext import six # Juniper interface libraries # https://github.com/Juniper/py-junos-eznc @@ -176,6 +177,10 @@ def rpc(cmd=None, dest=None, format='xml', **kwargs): if kwargs['__pub_arg']: if isinstance(kwargs['__pub_arg'][-1], dict): op.update(kwargs['__pub_arg'][-1]) + elif '__pub_schedule' in kwargs: + for key, value in six.iteritems(kwargs): + if not key.startswith('__pub_'): + op[key] = value else: op.update(kwargs) op['dev_timeout'] = str(op.pop('timeout', conn.timeout)) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index b44759d481..2257580270 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -40,11 +40,16 @@ import base64 import logging import yaml import tempfile +import signal +from time import sleep +from contextlib import contextmanager from salt.exceptions import CommandExecutionError from salt.ext.six import iteritems import salt.utils.files import salt.utils.templates +from salt.exceptions import TimeoutError +from salt.ext.six.moves import range # pylint: disable=import-error try: import kubernetes # pylint: disable=import-self @@ -78,6 +83,21 @@ def __virtual__(): return False, 'python kubernetes library not found' +if not salt.utils.is_windows(): + @contextmanager + def _time_limit(seconds): + def signal_handler(signum, frame): + raise TimeoutError + signal.signal(signal.SIGALRM, signal_handler) + signal.alarm(seconds) + try: + yield + finally: + signal.alarm(0) + + POLLING_TIME_LIMIT = 30 + + # pylint: disable=no-member def _setup_conn(**kwargs): ''' @@ -692,7 +712,30 @@ def delete_deployment(name, namespace='default', **kwargs): name=name, namespace=namespace, body=body) - return api_response.to_dict() + mutable_api_response = api_response.to_dict() + if not salt.utils.is_windows(): + try: + with _time_limit(POLLING_TIME_LIMIT): + while show_deployment(name, namespace) is not None: + sleep(1) + else: # pylint: disable=useless-else-on-loop + mutable_api_response['code'] = 200 + except TimeoutError: + pass + else: + # Windows has not signal.alarm implementation, so we are just falling + # back to loop-counting. + for i in range(60): + if show_deployment(name, namespace) is None: + mutable_api_response['code'] = 200 + break + else: + sleep(1) + if mutable_api_response['code'] != 200: + log.warning('Reached polling time limit. Deployment is not yet ' + 'deleted, but we are backing off. Sorry, but you\'ll ' + 'have to check manually.') + return mutable_api_response except (ApiException, HTTPError) as exc: if isinstance(exc, ApiException) and exc.status == 404: return None diff --git a/salt/modules/linux_acl.py b/salt/modules/linux_acl.py index d9959b7cc1..a8837e6207 100644 --- a/salt/modules/linux_acl.py +++ b/salt/modules/linux_acl.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- ''' Support for Linux File Access Control Lists + +The Linux ACL module requires the `getfacl` and `setfacl` binaries. + ''' from __future__ import absolute_import diff --git a/salt/modules/linux_lvm.py b/salt/modules/linux_lvm.py index ed6411d899..1001879b34 100644 --- a/salt/modules/linux_lvm.py +++ b/salt/modules/linux_lvm.py @@ -159,7 +159,7 @@ def vgdisplay(vgname=''): return ret -def lvdisplay(lvname=''): +def lvdisplay(lvname='', quiet=False): ''' Return information about the logical volume(s) @@ -174,7 +174,10 @@ def lvdisplay(lvname=''): cmd = ['lvdisplay', '-c'] if lvname: cmd.append(lvname) - cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False) + if quiet: + cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False, output_loglevel='quiet') + else: + cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False) if cmd_ret['retcode'] != 0: return {} diff --git a/salt/modules/mdadm.py b/salt/modules/mdadm.py index 0b453a2689..354ece93ba 100644 --- a/salt/modules/mdadm.py +++ b/salt/modules/mdadm.py @@ -356,3 +356,40 @@ def assemble(name, return cmd elif test_mode is False: return __salt__['cmd.run'](cmd, python_shell=False) + + +def examine(device): + ''' + Show detail for a specified RAID component device + + CLI Example: + + .. code-block:: bash + + salt '*' raid.examine '/dev/sda1' + ''' + res = __salt__['cmd.run_stdout']('mdadm -Y -E {0}'.format(device), output_loglevel='trace', python_shell=False) + ret = {} + + for line in res.splitlines(): + name, var = line.partition("=")[::2] + ret[name] = var + return ret + + +def add(name, device): + ''' + Add new device to RAID array. + + CLI Example: + + .. code-block:: bash + + salt '*' raid.add /dev/md0 /dev/sda1 + + ''' + + cmd = 'mdadm --manage {0} --add {1}'.format(name, device) + if __salt__['cmd.retcode'](cmd) == 0: + return True + return False diff --git a/salt/modules/mine.py b/salt/modules/mine.py index 063f577a44..b9ba6271cf 100644 --- a/salt/modules/mine.py +++ b/salt/modules/mine.py @@ -204,7 +204,7 @@ def send(func, *args, **kwargs): if mine_func not in __salt__: return False data = {} - arg_data = salt.utils.arg_lookup(__salt__[mine_func]) + arg_data = salt.utils.args.arg_lookup(__salt__[mine_func]) func_data = copy.deepcopy(kwargs) for ind, _ in enumerate(arg_data.get('args', [])): try: diff --git a/salt/modules/mount.py b/salt/modules/mount.py index ee263c4d77..9463283a4c 100644 --- a/salt/modules/mount.py +++ b/salt/modules/mount.py @@ -14,6 +14,7 @@ import salt.utils # Can be removed once test_mode is moved import salt.utils.files import salt.utils.path import salt.utils.platform +import salt.utils.mount from salt.exceptions import CommandNotFoundError, CommandExecutionError # Import 3rd-party libs @@ -1262,3 +1263,91 @@ def is_mounted(name): return True else: return False + + +def read_mount_cache(name): + ''' + .. versionadded:: Oxygen + + Provide information if the path is mounted + + CLI Example: + + .. code-block:: bash + + salt '*' mount.read_mount_cache /mnt/share + ''' + cache = salt.utils.mount.read_cache(__opts__) + if cache: + if 'mounts' in cache and cache['mounts']: + if name in cache['mounts']: + return cache['mounts'][name] + return {} + + +def write_mount_cache(real_name, + device, + mkmnt, + fstype, + mount_opts): + ''' + .. versionadded:: Oxygen + + Provide information if the path is mounted + + :param real_name: The real name of the mount point where the device is mounted. + :param device: The device that is being mounted. + :param mkmnt: Whether or not the mount point should be created. + :param fstype: The file system that is used. + :param mount_opts: Additional options used when mounting the device. + :return: Boolean if message was sent successfully. + + CLI Example: + + .. code-block:: bash + + salt '*' mount.write_mount_cache /mnt/share /dev/sda1 False ext4 defaults,nosuid + ''' + cache = salt.utils.mount.read_cache(__opts__) + + if not cache: + cache = {} + cache['mounts'] = {} + else: + if 'mounts' not in cache: + cache['mounts'] = {} + + cache['mounts'][real_name] = {'device': device, + 'fstype': fstype, + 'mkmnt': mkmnt, + 'opts': mount_opts} + + cache_write = salt.utils.mount.write_cache(cache, __opts__) + if cache_write: + return True + else: + raise CommandExecutionError('Unable to write mount cache.') + + +def delete_mount_cache(real_name): + ''' + .. versionadded:: Oxygen + + Provide information if the path is mounted + + CLI Example: + + .. code-block:: bash + + salt '*' mount.delete_mount_cache /mnt/share + ''' + cache = salt.utils.mount.read_cache(__opts__) + + if cache: + if 'mounts' in cache: + if real_name in cache['mounts']: + del cache['mounts'][real_name] + cache_write = salt.utils.mount.write_cache(cache, __opts__) + if not cache_write: + raise CommandExecutionError('Unable to write mount cache.') + return True diff --git a/salt/modules/mysql.py b/salt/modules/mysql.py index 26e4150ce4..5dcb7957dd 100644 --- a/salt/modules/mysql.py +++ b/salt/modules/mysql.py @@ -688,11 +688,20 @@ def file_query(database, file_name, **connection_args): .. versionadded:: 2017.7.0 + database + + database to run script inside + + file_name + + File name of the script. This can be on the minion, or a file that is reachable by the fileserver + CLI Example: .. code-block:: bash salt '*' mysql.file_query mydb file_name=/tmp/sqlfile.sql + salt '*' mysql.file_query mydb file_name=salt://sqlfile.sql Return data: @@ -701,6 +710,9 @@ def file_query(database, file_name, **connection_args): {'query time': {'human': '39.0ms', 'raw': '0.03899'}, 'rows affected': 1L} ''' + if any(file_name.startswith(proto) for proto in ('salt://', 'http://', 'https://', 'swift://', 's3://')): + file_name = __salt__['cp.cache_file'](file_name) + if os.path.exists(file_name): with salt.utils.files.fopen(file_name, 'r') as ifile: contents = ifile.read() @@ -709,7 +721,7 @@ def file_query(database, file_name, **connection_args): return False query_string = "" - ret = {'rows returned': 0, 'columns': 0, 'results': 0, 'rows affected': 0, 'query time': {'raw': 0}} + ret = {'rows returned': 0, 'columns': [], 'results': [], 'rows affected': 0, 'query time': {'raw': 0}} for line in contents.splitlines(): if re.match(r'--', line): # ignore sql comments continue @@ -729,16 +741,16 @@ def file_query(database, file_name, **connection_args): if 'rows returned' in query_result: ret['rows returned'] += query_result['rows returned'] if 'columns' in query_result: - ret['columns'] += query_result['columns'] + ret['columns'].append(query_result['columns']) if 'results' in query_result: - ret['results'] += query_result['results'] + ret['results'].append(query_result['results']) if 'rows affected' in query_result: ret['rows affected'] += query_result['rows affected'] ret['query time']['human'] = str(round(float(ret['query time']['raw']), 2)) + 's' ret['query time']['raw'] = round(float(ret['query time']['raw']), 5) # Remove empty keys in ret - ret = dict((k, v) for k, v in six.iteritems(ret) if v) + ret = {k: v for k, v in six.iteritems(ret) if v} return ret diff --git a/salt/modules/napalm_network.py b/salt/modules/napalm_network.py index 545332607c..4a31724663 100644 --- a/salt/modules/napalm_network.py +++ b/salt/modules/napalm_network.py @@ -1282,6 +1282,7 @@ def load_template(template_name, template_user='root', template_group='root', template_mode='755', + template_attrs='--------------e----', saltenv=None, template_engine='jinja', skip_verify=False, @@ -1368,11 +1369,16 @@ def load_template(template_name, .. versionadded:: 2016.11.2 - template_user: 755 + template_mode: 755 Permissions of file. .. versionadded:: 2016.11.2 + template_attrs: "--------------e----" + attributes of file. (see `man lsattr`) + + .. versionadded:: oxygen + saltenv: base Specifies the template environment. This will influence the relative imports inside the templates. @@ -1586,6 +1592,7 @@ def load_template(template_name, user=template_user, group=template_group, mode=template_mode, + attrs=template_attrs, template=template_engine, context=template_vars, defaults=defaults, diff --git a/salt/modules/nfs3.py b/salt/modules/nfs3.py index 423d9e9a4c..0d69202210 100644 --- a/salt/modules/nfs3.py +++ b/salt/modules/nfs3.py @@ -145,6 +145,8 @@ def reload_exports(): output = __salt__['cmd.run_all'](command) ret['stdout'] = output['stdout'] ret['stderr'] = output['stderr'] - ret['result'] = not output['retcode'] + # exportfs always returns 0, so retcode is useless + # We will consider it an error if stderr is nonempty + ret['result'] = output['stderr'] == '' return ret diff --git a/salt/modules/nilrt_ip.py b/salt/modules/nilrt_ip.py index 18b4470e73..a2450b067d 100644 --- a/salt/modules/nilrt_ip.py +++ b/salt/modules/nilrt_ip.py @@ -183,10 +183,10 @@ def _get_service_info(service): except Exception as exc: log.warning('Unable to get IPv6 {0} for service {1}\n'.format(info, service)) - domains = [] - for x in service_info.get_property('Domains'): - domains.append(str(x)) - data['ipv4']['dns'] = domains + nameservers = [] + for x in service_info.get_property('Nameservers'): + nameservers.append(str(x)) + data['ipv4']['dns'] = nameservers else: data['up'] = False @@ -351,13 +351,13 @@ def set_dhcp_linklocal_all(interface): ipv4['Gateway'] = dbus.String('', variant_level=1) try: service.set_property('IPv4.Configuration', ipv4) - service.set_property('Domains.Configuration', ['']) # reset domains list + service.set_property('Nameservers.Configuration', ['']) # reset nameservers list except Exception as exc: raise salt.exceptions.CommandExecutionError('Couldn\'t set dhcp linklocal for service: {0}\nError: {1}\n'.format(service, exc)) return True -def set_static_all(interface, address, netmask, gateway, domains): +def set_static_all(interface, address, netmask, gateway, nameservers): ''' Configure specified adapter to use ipv4 manual settings @@ -365,7 +365,7 @@ def set_static_all(interface, address, netmask, gateway, domains): :param str address: ipv4 address :param str netmask: ipv4 netmask :param str gateway: ipv4 gateway - :param str domains: list of domains servers separated by spaces + :param str nameservers: list of nameservers servers separated by spaces :return: True if the settings were applied, otherwise an exception will be thrown. :rtype: bool @@ -373,7 +373,7 @@ def set_static_all(interface, address, netmask, gateway, domains): .. code-block:: bash - salt '*' ip.set_static_all interface-label address netmask gateway domains + salt '*' ip.set_static_all interface-label address netmask gateway nameservers ''' service = _interface_to_service(interface) if not service: @@ -381,9 +381,15 @@ def set_static_all(interface, address, netmask, gateway, domains): validate, msg = _validate_ipv4([address, netmask, gateway]) if not validate: raise salt.exceptions.CommandExecutionError(msg) - validate, msg = _space_delimited_list(domains) - if not validate: - raise salt.exceptions.CommandExecutionError(msg) + if nameservers: + validate, msg = _space_delimited_list(nameservers) + if not validate: + raise salt.exceptions.CommandExecutionError(msg) + if not isinstance(nameservers, list): + nameservers = nameservers.split(' ') + service = _interface_to_service(interface) + if not service: + raise salt.exceptions.CommandExecutionError('Invalid interface name: {0}'.format(interface)) service = pyconnman.ConnService(_add_path(service)) ipv4 = service.get_property('IPv4.Configuration') ipv4['Method'] = dbus.String('manual', variant_level=1) @@ -392,10 +398,8 @@ def set_static_all(interface, address, netmask, gateway, domains): ipv4['Gateway'] = dbus.String('{0}'.format(gateway), variant_level=1) try: service.set_property('IPv4.Configuration', ipv4) - if not isinstance(domains, list): - dns = domains.split(' ') - domains = dns - service.set_property('Domains.Configuration', [dbus.String('{0}'.format(d)) for d in domains]) + if nameservers: + service.set_property('Nameservers.Configuration', [dbus.String('{0}'.format(d)) for d in nameservers]) except Exception as exc: raise salt.exceptions.CommandExecutionError('Couldn\'t set manual settings for service: {0}\nError: {1}\n'.format(service, exc)) return True diff --git a/salt/modules/panos.py b/salt/modules/panos.py index 6e8ffd556a..aecf93fffe 100644 --- a/salt/modules/panos.py +++ b/salt/modules/panos.py @@ -499,6 +499,76 @@ def get_ha_config(): return __proxy__['panos.call'](query) +def get_ha_link(): + ''' + Show high-availability link-monitoring state. + + CLI Example: + + .. code-block:: bash + + salt '*' panos.get_ha_link + + ''' + query = {'type': 'op', + 'cmd': ''} + + return __proxy__['panos.call'](query) + + +def get_ha_path(): + ''' + Show high-availability path-monitoring state. + + CLI Example: + + .. code-block:: bash + + salt '*' panos.get_ha_path + + ''' + query = {'type': 'op', + 'cmd': ''} + + return __proxy__['panos.call'](query) + + +def get_ha_state(): + ''' + Show high-availability state information. + + CLI Example: + + .. code-block:: bash + + salt '*' panos.get_ha_state + + ''' + + query = {'type': 'op', + 'cmd': ''} + + return __proxy__['panos.call'](query) + + +def get_ha_transitions(): + ''' + Show high-availability transition statistic information. + + CLI Example: + + .. code-block:: bash + + salt '*' panos.get_ha_transitions + + ''' + + query = {'type': 'op', + 'cmd': ''} + + return __proxy__['panos.call'](query) + + def get_hostname(): ''' Get the hostname of the device. @@ -856,6 +926,47 @@ def get_platform(): return __proxy__['panos.call'](query) +def get_security_rule(rulename=None, vsys='1'): + ''' + Get the candidate configuration for the specified rule. + + rulename(str): The name of the security rule. + + vsys(str): The string representation of the VSYS ID. + + CLI Example: + + .. code-block:: bash + + salt '*' panos.get_security_rule rule01 + salt '*' panos.get_security_rule rule01 3 + + ''' + query = {'type': 'config', + 'action': 'get', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/vsys/entry[@name=\'vsys{0}\']/' + 'rulebase/security/rules/entry[@name=\'{1}\']'.format(vsys, rulename)} + + return __proxy__['panos.call'](query) + + +def get_session_info(): + ''' + Show device session statistics. + + CLI Example: + + .. code-block:: bash + + salt '*' panos.get_session_info + + ''' + query = {'type': 'op', + 'cmd': ''} + + return __proxy__['panos.call'](query) + + def get_snmp_config(): ''' Get the SNMP configuration from the device. @@ -979,6 +1090,23 @@ def get_system_state(filter=None): return __proxy__['panos.call'](query) +def get_uncommitted_changes(): + ''' + Retrieve a list of all uncommitted changes on the device. + + CLI Example: + + .. code-block:: bash + + salt '*' panos.get_uncommitted_changes + + ''' + query = {'type': 'op', + 'cmd': ''} + + return __proxy__['panos.call'](query) + + def get_users_config(): ''' Get the local administrative user account configuration. @@ -1261,7 +1389,7 @@ def set_authentication_profile(profile=None, deploy=False): query = {'type': 'config', 'action': 'set', 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/' - 'authentication-profile/', + 'authentication-profile', 'element': '{0}'.format(profile)} ret.update(__proxy__['panos.call'](query)) @@ -1297,7 +1425,7 @@ def set_hostname(hostname=None, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system', 'element': '{0}'.format(hostname)} ret.update(__proxy__['panos.call'](query)) @@ -1337,7 +1465,7 @@ def set_management_icmp(enabled=True, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service/', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service', 'element': '{0}'.format(value)} ret.update(__proxy__['panos.call'](query)) @@ -1377,7 +1505,7 @@ def set_management_http(enabled=True, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service/', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service', 'element': '{0}'.format(value)} ret.update(__proxy__['panos.call'](query)) @@ -1417,7 +1545,7 @@ def set_management_https(enabled=True, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service/', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service', 'element': '{0}'.format(value)} ret.update(__proxy__['panos.call'](query)) @@ -1457,7 +1585,7 @@ def set_management_ocsp(enabled=True, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service/', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service', 'element': '{0}'.format(value)} ret.update(__proxy__['panos.call'](query)) @@ -1497,7 +1625,7 @@ def set_management_snmp(enabled=True, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service/', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service', 'element': '{0}'.format(value)} ret.update(__proxy__['panos.call'](query)) @@ -1537,7 +1665,7 @@ def set_management_ssh(enabled=True, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service/', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service', 'element': '{0}'.format(value)} ret.update(__proxy__['panos.call'](query)) @@ -1577,7 +1705,7 @@ def set_management_telnet(enabled=True, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service/', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/service', 'element': '{0}'.format(value)} ret.update(__proxy__['panos.call'](query)) @@ -1770,8 +1898,8 @@ def set_permitted_ip(address=None, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/permitted-ip/', - 'element': ''.format(address)} + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/permitted-ip', + 'element': ''.format(address)} ret.update(__proxy__['panos.call'](query)) @@ -1806,7 +1934,7 @@ def set_timezone(tz=None, deploy=False): query = {'type': 'config', 'action': 'set', - 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/timezone/', + 'xpath': '/config/devices/entry[@name=\'localhost.localdomain\']/deviceconfig/system/timezone', 'element': '{0}'.format(tz)} ret.update(__proxy__['panos.call'](query)) diff --git a/salt/modules/pkg_resource.py b/salt/modules/pkg_resource.py index 1e156f7e42..70a2737af9 100644 --- a/salt/modules/pkg_resource.py +++ b/salt/modules/pkg_resource.py @@ -106,12 +106,7 @@ def parse_targets(name=None, salt '*' pkg_resource.parse_targets ''' if '__env__' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'__env__\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('__env__') if __grains__['os'] == 'MacOS' and sources: @@ -316,7 +311,8 @@ def format_pkg_list(packages, versions_as_list, attr): ''' ret = copy.deepcopy(packages) if attr: - requested_attr = set(['version', 'arch', 'install_date', 'install_date_time_t']) + requested_attr = set(['epoch', 'version', 'release', 'arch', + 'install_date', 'install_date_time_t']) if attr != 'all': requested_attr &= set(attr + ['version']) @@ -326,13 +322,25 @@ def format_pkg_list(packages, versions_as_list, attr): for all_attr in ret[name]: filtered_attr = {} for key in requested_attr: - filtered_attr[key] = all_attr[key] + if all_attr[key]: + filtered_attr[key] = all_attr[key] versions.append(filtered_attr) ret[name] = versions return ret for name in ret: - ret[name] = [d['version'] for d in ret[name]] + ret[name] = [format_version(d['epoch'], d['version'], d['release']) + for d in ret[name]] if not versions_as_list: stringify(ret) return ret + + +def format_version(epoch, version, release): + ''' + Formats a version string for list_pkgs. + ''' + full_version = '{0}:{1}'.format(epoch, version) if epoch else version + if release: + full_version += '-{0}'.format(release) + return full_version diff --git a/salt/modules/portage_config.py b/salt/modules/portage_config.py index 2a05ebb451..8441956bc3 100644 --- a/salt/modules/portage_config.py +++ b/salt/modules/portage_config.py @@ -75,6 +75,8 @@ def _get_config_file(conf, atom): if parts.cp == '*/*': # parts.repo will be empty if there is no repo part relative_path = parts.repo or "gentoo" + elif str(parts.cp).endswith('/*'): + relative_path = str(parts.cp).split("/")[0] + "_" else: relative_path = os.path.join(*[x for x in os.path.split(parts.cp) if x != '*']) else: @@ -92,9 +94,20 @@ def _p_to_cp(p): Convert a package name or a DEPEND atom to category/package format. Raises an exception if program name is ambiguous. ''' - ret = _porttree().dbapi.xmatch("match-all", p) - if ret: - return portage.cpv_getkey(ret[0]) + try: + ret = portage.dep_getkey(p) + if ret: + return ret + except portage.exception.InvalidAtom: + pass + + try: + ret = _porttree().dbapi.xmatch('bestmatch-visible', p) + if ret: + return portage.dep_getkey(ret) + except portage.exception.InvalidAtom: + pass + return None @@ -188,12 +201,7 @@ def _package_conf_file_to_dir(file_name): else: os.rename(path, path + '.tmpbak') os.mkdir(path, 0o755) - with salt.utils.files.fopen(path + '.tmpbak') as fh_: - for line in fh_: - line = line.strip() - if line and not line.startswith('#'): - append_to_package_conf(file_name, string=line) - os.remove(path + '.tmpbak') + os.rename(path + '.tmpbak', os.path.join(path, 'tmp')) return True else: os.mkdir(path, 0o755) @@ -218,7 +226,7 @@ def _package_conf_ordering(conf, clean=True, keep_backup=False): shutil.copy(file_path, file_path + '.bak') backup_files.append(file_path + '.bak') - if cp[0] == '/' or cp.split('/') > 2: + if cp[0] == '/' or len(cp.split('/')) > 2: with salt.utils.files.fopen(file_path) as fp_: rearrange.extend(fp_.readlines()) os.remove(file_path) diff --git a/salt/modules/purefa.py b/salt/modules/purefa.py new file mode 100644 index 0000000000..aeb4104ee7 --- /dev/null +++ b/salt/modules/purefa.py @@ -0,0 +1,1256 @@ +# -*- coding: utf-8 -*- + +## +# Copyright 2017 Pure Storage Inc +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +''' + +Management of Pure Storage FlashArray + +Installation Prerequisites +-------------------------- +- You will need the ``purestorage`` python package in your python installation + path that is running salt. + + .. code-block:: bash + + pip install purestorage + +- Configure Pure Storage FlashArray authentication. Use one of the following + three methods. + + 1) From the minion config + .. code-block:: yaml + + pure_tags: + fa: + san_ip: management vip or hostname for the FlashArray + api_token: A valid api token for the FlashArray being managed + + 2) From environment (PUREFA_IP and PUREFA_API) + 3) From the pillar (PUREFA_IP and PUREFA_API) + +:maintainer: Simon Dodsley (simon@purestorage.com) +:maturity: new +:requires: purestorage +:platform: all + +.. versionadded:: Oxygen + +''' + +# Import Python libs +from __future__ import absolute_import +import os +import platform +from datetime import datetime + +# Import Salt libs +from salt.exceptions import CommandExecutionError + +# Import 3rd party modules +try: + import purestorage + HAS_PURESTORAGE = True +except ImportError: + HAS_PURESTORAGE = False + +__docformat__ = 'restructuredtext en' + +VERSION = '1.0.0' +USER_AGENT_BASE = 'Salt' + +__virtualname__ = 'purefa' + +# Default symbols to use for passwords. Avoids visually confusing characters. +# ~6 bits per symbol +DEFAULT_PASSWORD_SYMBOLS = ('23456789', # Removed: 0,1 + 'ABCDEFGHJKLMNPQRSTUVWXYZ', # Removed: I, O + 'abcdefghijkmnopqrstuvwxyz') # Removed: l + + +def __virtual__(): + ''' + Determine whether or not to load this module + ''' + if HAS_PURESTORAGE: + return __virtualname__ + return (False, 'purefa execution module not loaded: purestorage python library not available.') + + +def _get_system(): + ''' + Get Pure Storage FlashArray configuration + + 1) From the minion config + pure_tags: + fa: + san_ip: management vip or hostname for the FlashArray + api_token: A valid api token for the FlashArray being managed + 2) From environment (PUREFA_IP and PUREFA_API) + 3) From the pillar (PUREFA_IP and PUREFA_API) + + ''' + agent = {'base': USER_AGENT_BASE, + 'class': __name__, + 'version': VERSION, + 'platform': platform.platform() + } + + user_agent = '{base} {class}/{version} ({platform})'.format(**agent) + + try: + array = __opts__['pure_tags']['fa'].get('san_ip') + api = __opts__['pure_tags']['fa'].get('api_token') + if array and api: + system = purestorage.FlashArray(array, api_token=api, user_agent=user_agent) + except (KeyError, NameError, TypeError): + try: + san_ip = os.environ.get('PUREFA_IP') + api_token = os.environ.get('PUREFA_API') + system = purestorage.FlashArray(san_ip, + api_token=api_token, + user_agent=user_agent) + except (ValueError, KeyError, NameError): + try: + system = purestorage.FlashArray(__pillar__['PUREFA_IP'], + api_token=__pillar__['PUREFA_API'], + user_agent=user_agent) + except (KeyError, NameError): + raise CommandExecutionError('No Pure Storage FlashArray credentials found.') + + try: + system.get() + except Exception: + raise CommandExecutionError('Pure Storage FlashArray authentication failed.') + return system + + +def _get_volume(name, array): + '''Private function to check volume''' + try: + return array.get_volume(name) + except purestorage.PureError: + return None + + +def _get_snapshot(name, suffix, array): + '''Private function to check snapshot''' + snapshot = name + '.' + suffix + try: + for snap in array.get_volume(name, snap=True): + if snap['name'] == snapshot: + return snapshot + except purestorage.PureError: + return None + + +def _get_deleted_volume(name, array): + '''Private function to check deleted volume''' + try: + return array.get_volume(name, pending='true') + except purestorage.PureError: + return None + + +def _get_pgroup(name, array): + '''Private function to check protection group''' + pgroup = None + for temp in array.list_pgroups(): + if temp['name'] == name: + pgroup = temp + break + return pgroup + + +def _get_deleted_pgroup(name, array): + '''Private function to check deleted protection group''' + try: + return array.get_pgroup(name, pending='true') + except purestorage.PureError: + return None + + +def _get_hgroup(name, array): + '''Private function to check hostgroup''' + hostgroup = None + for temp in array.list_hgroups(): + if temp['name'] == name: + hostgroup = temp + break + return hostgroup + + +def _get_host(name, array): + '''Private function to check host''' + host = None + for temp in array.list_hosts(): + if temp['name'] == name: + host = temp + break + return host + + +def snap_create(name, suffix=None): + ''' + + Create a volume snapshot on a Pure Storage FlashArray. + + Will return False is volume selected to snap does not exist. + + .. versionadded:: Oxygen + + name : string + name of volume to snapshot + suffix : string + if specificed forces snapshot name suffix. If not specified defaults to timestamp. + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.snap_create foo + salt '*' purefa.snap_create foo suffix=bar + + ''' + array = _get_system() + if suffix is None: + suffix = 'snap-' + str((datetime.utcnow() - datetime(1970, 1, 1, 0, 0, 0, 0)).total_seconds()) + suffix = suffix.replace('.', '') + if _get_volume(name, array) is not None: + try: + array.create_snapshot(name, suffix=suffix) + return True + except purestorage.PureError: + return False + else: + return False + + +def snap_delete(name, suffix=None, eradicate=False): + ''' + + Delete a volume snapshot on a Pure Storage FlashArray. + + Will return False if selected snapshot does not exist. + + .. versionadded:: Oxygen + + name : string + name of volume + suffix : string + name of snapshot + eradicate : boolean + Eradicate snapshot after deletion if True. Default is False + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.snap_delete foo suffix=snap eradicate=True + + ''' + array = _get_system() + if _get_snapshot(name, suffix, array) is not None: + try: + snapname = name + '.' + suffix + array.destroy_volume(snapname) + except purestorage.PureError: + return False + if eradicate is True: + try: + array.eradicate_volume(snapname) + return True + except purestorage.PureError: + return False + else: + return True + else: + return False + + +def snap_eradicate(name, suffix=None): + ''' + + Eradicate a deleted volume snapshot on a Pure Storage FlashArray. + + Will retunr False is snapshot is not in a deleted state. + + .. versionadded:: Oxygen + + name : string + name of volume + suffix : string + name of snapshot + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.snap_delete foo suffix=snap eradicate=True + + ''' + array = _get_system() + if _get_snapshot(name, suffix, array) is not None: + snapname = name + '.' + suffix + try: + array.eradicate_volume(snapname) + return True + except purestorage.PureError: + return False + else: + return False + + +def volume_create(name, size=None): + ''' + + Create a volume on a Pure Storage FlashArray. + + Will return False if volume already exists. + + .. versionadded:: Oxygen + + name : string + name of volume (truncated to 63 characters) + size : string + if specificed capacity of volume. If not specified default to 1G. + Refer to Pure Storage documentation for formatting rules. + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.volume_create foo + salt '*' purefa.volume_create foo size=10T + + ''' + if len(name) > 63: + name = name[0:63] + array = _get_system() + if _get_volume(name, array) is None: + if size is None: + size = '1G' + try: + array.create_volume(name, size) + return True + except purestorage.PureError: + return False + else: + return False + + +def volume_delete(name, eradicate=False): + ''' + + Delete a volume on a Pure Storage FlashArray. + + Will return False if volume doesn't exist is already in a deleted state. + + .. versionadded:: Oxygen + + name : string + name of volume + eradicate : boolean + Eradicate volume after deletion if True. Default is False + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.volume_delete foo eradicate=True + + ''' + array = _get_system() + if _get_volume(name, array) is not None: + try: + array.destroy_volume(name) + except purestorage.PureError: + return False + if eradicate is True: + try: + array.eradicate_volume(name) + return True + except purestorage.PureError: + return False + else: + return True + else: + return False + + +def volume_eradicate(name): + ''' + + Eradicate a deleted volume on a Pure Storage FlashArray. + + Will return False is volume is not in a deleted state. + + .. versionadded:: Oxygen + + name : string + name of volume + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.volume_eradicate foo + + ''' + array = _get_system() + if _get_deleted_volume(name, array) is not None: + try: + array.eradicate_volume(name) + return True + except purestorage.PureError: + return False + else: + return False + + +def volume_extend(name, size): + ''' + + Extend an existing volume on a Pure Storage FlashArray. + + Will return False if new size is less than or equal to existing size. + + .. versionadded:: Oxygen + + name : string + name of volume + size : string + New capacity of volume. + Refer to Pure Storage documentation for formatting rules. + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.volume_extend foo 10T + + ''' + array = _get_system() + vol = _get_volume(name, array) + if vol is not None: + if __utils__['stringutils.human_to_bytes'](size) > vol['size']: + try: + array.extend_volume(name, size) + return True + except purestorage.PureError: + return False + else: + return False + else: + return False + + +def snap_volume_create(name, target, overwrite=False): + ''' + + Create R/W volume from snapshot on a Pure Storage FlashArray. + + Will return False if target volume already exists and + overwrite is not specified, or selected snapshot doesn't exist. + + .. versionadded:: Oxygen + + name : string + name of volume snapshot + target : string + name of clone volume + overwrite : boolean + overwrite clone if already exists (default: False) + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.snap_volume_create foo.bar clone overwrite=True + + ''' + array = _get_system() + source, suffix = name.split('.') + if _get_snapshot(source, suffix, array) is not None: + if _get_volume(target, array) is None: + try: + array.copy_volume(name, target) + return True + except purestorage.PureError: + return False + else: + if overwrite: + try: + array.copy_volume(name, target, overwrite=overwrite) + return True + except purestorage.PureError: + return False + else: + return False + else: + return False + + +def volume_clone(name, target, overwrite=False): + ''' + + Clone an existing volume on a Pure Storage FlashArray. + + Will return False if source volume doesn't exist, or + target volume already exists and overwrite not specified. + + .. versionadded:: Oxygen + + name : string + name of volume + target : string + name of clone volume + overwrite : boolean + overwrite clone if already exists (default: False) + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.volume_clone foo bar overwrite=True + + ''' + array = _get_system() + if _get_volume(name, array) is not None: + if _get_volume(target, array) is None: + try: + array.copy_volume(name, target) + return True + except purestorage.PureError: + return False + else: + if overwrite: + try: + array.copy_volume(name, target, overwrite=overwrite) + return True + except purestorage.PureError: + return False + else: + return False + else: + return False + + +def volume_attach(name, host): + ''' + + Attach a volume to a host on a Pure Storage FlashArray. + + Host and volume must exist or else will return False. + + .. versionadded:: Oxygen + + name : string + name of volume + host : string + name of host + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.volume_attach foo bar + + ''' + array = _get_system() + if _get_volume(name, array) is not None and _get_host(host, array) is not None: + try: + array.connect_host(host, name) + return True + except purestorage.PureError: + return False + else: + return False + + +def volume_detach(name, host): + ''' + + Detach a volume from a host on a Pure Storage FlashArray. + + Will return False if either host or volume do not exist, or + if selected volume isn't already connected to the host. + + .. versionadded:: Oxygen + + name : string + name of volume + host : string + name of host + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.volume_detach foo bar + + ''' + array = _get_system() + if _get_volume(name, array) is None or _get_host(host, array) is None: + return False + elif _get_volume(name, array) is not None and _get_host(host, array) is not None: + try: + array.disconnect_host(host, name) + return True + except purestorage.PureError: + return False + + +def host_create(name, iqn=None, wwn=None): + ''' + + Add a host on a Pure Storage FlashArray. + + Will return False if host already exists, or the iSCSI or + Fibre Channel parameters are not in a valid format. + See Pure Storage FlashArray documentation. + + .. versionadded:: Oxygen + + name : string + name of host (truncated to 63 characters) + iqn : string + iSCSI IQN of host + wwn : string + Fibre Channel WWN of host + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.host_create foo iqn='' wwn='' + + ''' + array = _get_system() + if len(name) > 63: + name = name[0:63] + if _get_host(name, array) is None: + try: + array.create_host(name) + except purestorage.PureError: + return False + if iqn is not None: + try: + array.set_host(name, addiqnlist=[iqn]) + except purestorage.PureError: + array.delete_host(name) + return False + if wwn is not None: + try: + array.set_host(name, addwwnlist=[wwn]) + except purestorage.PureError: + array.delete_host(name) + return False + else: + return False + + return True + + +def host_update(name, iqn=None, wwn=None): + ''' + + Update a hosts port definitions on a Pure Storage FlashArray. + + Will return False if new port definitions are already in use + by another host, or are not in a valid format. + See Pure Storage FlashArray documentation. + + .. versionadded:: Oxygen + + name : string + name of host + iqn : string + Additional iSCSI IQN of host + wwn : string + Additional Fibre Channel WWN of host + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.host_update foo iqn='' wwn='' + + ''' + array = _get_system() + if _get_host(name, array) is not None: + if iqn is not None: + try: + array.set_host(name, addiqnlist=[iqn]) + except purestorage.PureError: + return False + if wwn is not None: + try: + array.set_host(name, addwwnlist=[wwn]) + except purestorage.PureError: + return False + return True + else: + return False + + +def host_delete(name): + ''' + + Delete a host on a Pure Storage FlashArray (detaches all volumes). + + Will return False if the host doesn't exist. + + .. versionadded:: Oxygen + + name : string + name of host + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.host_delete foo + + ''' + array = _get_system() + if _get_host(name, array) is not None: + for vol in array.list_host_connections(name): + try: + array.disconnect_host(name, vol['vol']) + except purestorage.PureError: + return False + try: + array.delete_host(name) + return True + except purestorage.PureError: + return False + else: + return False + + +def hg_create(name, host=None, volume=None): + ''' + + Create a hostgroup on a Pure Storage FlashArray. + + Will return False if hostgroup already exists, or if + named host or volume do not exist. + + .. versionadded:: Oxygen + + name : string + name of hostgroup (truncated to 63 characters) + host : string + name of host to add to hostgroup + volume : string + name of volume to add to hostgroup + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.hg_create foo host=bar volume=vol + + ''' + array = _get_system() + if len(name) > 63: + name = name[0:63] + if _get_hgroup(name, array) is None: + try: + array.create_hgroup(name) + except purestorage.PureError: + return False + if host is not None: + if _get_host(host, array): + try: + array.set_hgroup(name, addhostlist=[host]) + except purestorage.PureError: + return False + else: + hg_delete(name) + return False + if volume is not None: + if _get_volume(volume, array): + try: + array.connect_hgroup(name, volume) + except purestorage.PureError: + hg_delete(name) + return False + else: + hg_delete(name) + return False + return True + else: + return False + + +def hg_update(name, host=None, volume=None): + ''' + + Adds entries to a hostgroup on a Pure Storage FlashArray. + + Will return False is hostgroup doesn't exist, or host + or volume do not exist. + + .. versionadded:: Oxygen + + name : string + name of hostgroup + host : string + name of host to add to hostgroup + volume : string + name of volume to add to hostgroup + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.hg_update foo host=bar volume=vol + + ''' + array = _get_system() + if _get_hgroup(name, array) is not None: + if host is not None: + if _get_host(host, array): + try: + array.set_hgroup(name, addhostlist=[host]) + except purestorage.PureError: + return False + else: + return False + if volume is not None: + if _get_volume(volume, array): + try: + array.connect_hgroup(name, volume) + except purestorage.PureError: + return False + else: + return False + return True + else: + return False + + +def hg_delete(name): + ''' + + Delete a hostgroup on a Pure Storage FlashArray (removes all volumes and hosts). + + Will return False is hostgroup is already in a deleted state. + + .. versionadded:: Oxygen + + name : string + name of hostgroup + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.hg_delete foo + + ''' + array = _get_system() + if _get_hgroup(name, array) is not None: + for vol in array.list_hgroup_connections(name): + try: + array.disconnect_hgroup(name, vol['vol']) + except purestorage.PureError: + return False + host = array.get_hgroup(name) + try: + array.set_hgroup(name, remhostlist=host['hosts']) + array.delete_hgroup(name) + return True + except purestorage.PureError: + return False + else: + return False + + +def hg_remove(name, volume=None, host=None): + ''' + + Remove a host and/or volume from a hostgroup on a Pure Storage FlashArray. + + Will return False is hostgroup does not exist, or named host or volume are + not in the hostgroup. + + .. versionadded:: Oxygen + + name : string + name of hostgroup + volume : string + name of volume to remove from hostgroup + host : string + name of host to remove from hostgroup + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.hg_remove foo volume=test host=bar + + ''' + array = _get_system() + if _get_hgroup(name, array) is not None: + if volume is not None: + if _get_volume(volume, array): + for temp in array.list_hgroup_connections(name): + if temp['vol'] == volume: + try: + array.disconnect_hgroup(name, volume) + return True + except purestorage.PureError: + return False + return False + else: + return False + if host is not None: + if _get_host(host, array): + temp = _get_host(host, array) + if temp['hgroup'] == name: + try: + array.set_hgroup(name, remhostlist=[host]) + return True + except purestorage.PureError: + return False + else: + return False + else: + return False + if host is None and volume is None: + return False + else: + return False + + +def pg_create(name, hostgroup=None, host=None, volume=None, enabled=True): + ''' + + Create a protection group on a Pure Storage FlashArray. + + Will return False is the following cases: + * Protection Grop already exists + * Protection Group in a deleted state + * More than one type is specified - protection groups are for only + hostgroups, hosts or volumes + * Named type for protection group does not exist + + .. versionadded:: Oxygen + + name : string + name of protection group + hostgroup : string + name of hostgroup to add to protection group + host : string + name of host to add to protection group + volume : string + name of volume to add to protection group + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.pg_create foo [hostgroup=foo | host=bar | volume=vol] enabled=[true | false] + + ''' + array = _get_system() + if hostgroup is None and host is None and volume is None: + if _get_pgroup(name, array) is None: + try: + array.create_pgroup(name) + except purestorage.PureError: + return False + try: + array.set_pgroup(name, snap_enabled=enabled) + return True + except purestorage.PureError: + pg_delete(name) + return False + else: + return False + elif __utils__['value.xor'](hostgroup, host, volume): + if _get_pgroup(name, array) is None: + try: + array.create_pgroup(name) + except purestorage.PureError: + return False + try: + array.set_pgroup(name, snap_enabled=enabled) + except purestorage.PureError: + pg_delete(name) + return False + if hostgroup is not None: + if _get_hgroup(hostgroup, array) is not None: + try: + array.set_pgroup(name, addhgrouplist=[hostgroup]) + return True + except purestorage.PureError: + pg_delete(name) + return False + else: + pg_delete(name) + return False + elif host is not None: + if _get_host(host, array) is not None: + try: + array.set_pgroup(name, addhostlist=[host]) + return True + except purestorage.PureError: + pg_delete(name) + return False + else: + pg_delete(name) + return False + elif volume is not None: + if _get_volume(volume, array) is not None: + try: + array.set_pgroup(name, addvollist=[volume]) + return True + except purestorage.PureError: + pg_delete(name) + return False + else: + pg_delete(name) + return False + else: + return False + else: + return False + + +def pg_update(name, hostgroup=None, host=None, volume=None): + ''' + + Update a protection group on a Pure Storage FlashArray. + + Will return False in the following cases: + * Protection group does not exist + * Incorrect type selected for current protection group type + * Specified type does not exist + + .. versionadded:: Oxygen + + name : string + name of protection group + hostgroup : string + name of hostgroup to add to protection group + host : string + name of host to add to protection group + volume : string + name of volume to add to protection group + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.pg_update foo [hostgroup=foo | host=bar | volume=vol] + + ''' + array = _get_system() + pgroup = _get_pgroup(name, array) + if pgroup is not None: + if hostgroup is not None and pgroup['hgroups'] is not None: + if _get_hgroup(hostgroup, array) is not None: + try: + array.add_hgroup(hostgroup, name) + return True + except purestorage.PureError: + return False + else: + return False + elif host is not None and pgroup['hosts'] is not None: + if _get_host(host, array) is not None: + try: + array.add_host(host, name) + return True + except purestorage.PureError: + return False + else: + return False + elif volume is not None and pgroup['volumes'] is not None: + if _get_volume(volume, array) is not None: + try: + array.add_volume(volume, name) + return True + except purestorage.PureError: + return False + else: + return False + else: + if pgroup['hgroups'] is None and pgroup['hosts'] is None and pgroup['volumes'] is None: + if hostgroup is not None: + if _get_hgroup(hostgroup, array) is not None: + try: + array.set_pgroup(name, addhgrouplist=[hostgroup]) + return True + except purestorage.PureError: + return False + else: + return False + elif host is not None: + if _get_host(host, array) is not None: + try: + array.set_pgroup(name, addhostlist=[host]) + return True + except purestorage.PureError: + return False + else: + return False + elif volume is not None: + if _get_volume(volume, array) is not None: + try: + array.set_pgroup(name, addvollist=[volume]) + return True + except purestorage.PureError: + return False + else: + return False + else: + return False + else: + return False + + +def pg_delete(name, eradicate=False): + ''' + + Delete a protecton group on a Pure Storage FlashArray. + + Will return False if protection group is already in a deleted state. + + .. versionadded:: Oxygen + + name : string + name of protection group + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.pg_delete foo + + ''' + array = _get_system() + if _get_pgroup(name, array) is not None: + try: + array.destroy_pgroup(name) + except purestorage.PureError: + return False + if eradicate is True: + try: + array.eradicate_pgroup(name) + return True + except purestorage.PureError: + return False + else: + return True + else: + return False + + +def pg_eradicate(name): + ''' + + Eradicate a deleted protecton group on a Pure Storage FlashArray. + + Will return False if protection group is not in a deleted state. + + .. versionadded:: Oxygen + + name : string + name of protection group + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.pg_eradicate foo + + ''' + array = _get_system() + if _get_deleted_pgroup(name, array) is not None: + try: + array.eradicate_pgroup(name) + return True + except purestorage.PureError: + return False + else: + return False + + +def pg_remove(name, hostgroup=None, host=None, volume=None): + ''' + + Remove a hostgroup, host or volume from a protection group on a Pure Storage FlashArray. + + Will return False in the following cases: + * Protection group does not exist + * Specified type is not currently associated with the protection group + + .. versionadded:: Oxygen + + name : string + name of hostgroup + hostgroup : string + name of hostgroup to remove from protection group + host : string + name of host to remove from hostgroup + volume : string + name of volume to remove from hostgroup + + CLI Example: + + .. code-block:: bash + + salt '*' purefa.pg_remove foo [hostgroup=bar | host=test | volume=bar] + + ''' + array = _get_system() + pgroup = _get_pgroup(name, array) + if pgroup is not None: + if hostgroup is not None and pgroup['hgroups'] is not None: + if _get_hgroup(hostgroup, array) is not None: + try: + array.remove_hgroup(hostgroup, name) + return True + except purestorage.PureError: + return False + else: + return False + elif host is not None and pgroup['hosts'] is not None: + if _get_host(host, array) is not None: + try: + array.remove_host(host, name) + return True + except purestorage.PureError: + return False + else: + return False + elif volume is not None and pgroup['volumes'] is not None: + if _get_volume(volume, array) is not None: + try: + array.remove_volume(volume, name) + return True + except purestorage.PureError: + return False + else: + return False + else: + return False + else: + return False diff --git a/salt/modules/redismod.py b/salt/modules/redismod.py index ee15a45a03..a95e1b9f3f 100644 --- a/salt/modules/redismod.py +++ b/salt/modules/redismod.py @@ -18,6 +18,8 @@ Module to provide redis functionality to Salt # Import Python libs from __future__ import absolute_import from salt.ext.six.moves import zip +from salt.ext import six +from datetime import datetime # Import third party libs try: @@ -513,8 +515,14 @@ def lastsave(host=None, port=None, db=None, password=None): salt '*' redis.lastsave ''' + # Use of %s to get the timestamp is not supported by Python. The reason it + # works is because it's passed to the system strftime which may not support + # it. See: https://stackoverflow.com/a/11743262 server = _connect(host, port, db, password) - return int(server.lastsave().strftime("%s")) + if six.PY2: + return int((server.lastsave() - datetime(1970, 1, 1)).total_seconds()) + else: + return int(server.lastsave().timestamp()) def llen(key, host=None, port=None, db=None, password=None): diff --git a/salt/modules/rh_ip.py b/salt/modules/rh_ip.py index 33d0955041..d29392f07a 100644 --- a/salt/modules/rh_ip.py +++ b/salt/modules/rh_ip.py @@ -1013,7 +1013,10 @@ def build_interface(iface, iface_type, enabled, **settings): salt '*' ip.build_interface eth0 eth ''' if __grains__['os'] == 'Fedora': - rh_major = '6' + if __grains__['osmajorrelease'] >= 18: + rh_major = '7' + else: + rh_major = '6' else: rh_major = __grains__['osrelease'][:1] diff --git a/salt/modules/saltcheck.py b/salt/modules/saltcheck.py new file mode 100644 index 0000000000..f59d2d7e91 --- /dev/null +++ b/salt/modules/saltcheck.py @@ -0,0 +1,602 @@ +# -*- coding: utf-8 -*- +''' +A module for testing the logic of states and highstates + +Saltcheck provides unittest like functionality requiring only the knowledge of salt module execution and yaml. + +In order to run state and highstate saltcheck tests a sub-folder of a state must be creaed and named "saltcheck-tests". + +Tests for a state should be created in files ending in *.tst and placed in the saltcheck-tests folder. + +Multiple tests can be created in a file. +Multiple *.tst files can be created in the saltcheck-tests folder. +The "id" of a test works in the same manner as in salt state files. +They should be unique and descriptive. + +Example file system layout: +/srv/salt/apache/ + init.sls + config.sls + saltcheck-tests/ + pkg_and_mods.tst + config.tst + + +Saltcheck Test Syntax: + +Unique-ID: + module_and_function: + args: + kwargs: + assertion: + expected-return: + + +Example test 1: + +echo-test-hello: + module_and_function: test.echo + args: + - "hello" + kwargs: + assertion: assertEqual + expected-return: 'hello' + + +:codeauthor: William Cannon +:maturity: new +''' +from __future__ import absolute_import +import logging +import os +import time +import yaml +try: + import salt.utils + import salt.client + import salt.exceptions +except ImportError: + pass + +log = logging.getLogger(__name__) + +__virtualname__ = 'saltcheck' + + +def __virtual__(): + ''' + Check dependencies - may be useful in future + ''' + return __virtualname__ + + +def update_master_cache(): + ''' + Updates the master cache onto the minion - transfers all salt-check-tests + Should be done one time before running tests, and if tests are updated + Can be automated by setting "auto_update_master_cache: True" in minion config + + CLI Example: + salt '*' saltcheck.update_master_cache + ''' + __salt__['cp.cache_master']() + return True + + +def run_test(**kwargs): + ''' + Execute one saltcheck test and return result + + :param keyword arg test: + CLI Example:: + salt '*' saltcheck.run_test + test='{"module_and_function": "test.echo", + "assertion": "assertEqual", + "expected-return": "This works!", + "args":["This works!"] }' + ''' + # salt converts the string to a dictionary auto-magically + scheck = SaltCheck() + test = kwargs.get('test', None) + if test and isinstance(test, dict): + return scheck.run_test(test) + else: + return "Test must be a dictionary" + + +def run_state_tests(state): + ''' + Execute all tests for a salt state and return results + Nested states will also be tested + + :param str state: the name of a user defined state + + CLI Example:: + salt '*' saltcheck.run_state_tests postfix + ''' + scheck = SaltCheck() + paths = scheck.get_state_search_path_list() + stl = StateTestLoader(search_paths=paths) + results = {} + sls_list = _get_state_sls(state) + for state_name in sls_list: + mypath = stl.convert_sls_to_path(state_name) + stl.add_test_files_for_sls(mypath) + stl.load_test_suite() + results_dict = {} + for key, value in stl.test_dict.items(): + result = scheck.run_test(value) + results_dict[key] = result + results[state_name] = results_dict + passed = 0 + failed = 0 + missing_tests = 0 + for state in results: + if len(results[state].items()) == 0: + missing_tests = missing_tests + 1 + else: + for dummy, val in results[state].items(): + log.info("dummy={}, val={}".format(dummy, val)) + if val.startswith('Pass'): + passed = passed + 1 + if val.startswith('Fail'): + failed = failed + 1 + out_list = [] + for key, value in results.items(): + out_list.append({key: value}) + out_list.sort() + out_list.append({"TEST RESULTS": {'Passed': passed, 'Failed': failed, 'Missing Tests': missing_tests}}) + return out_list + + +def run_highstate_tests(): + ''' + Execute all tests for a salt highstate and return results + + CLI Example:: + salt '*' saltcheck.run_highstate_tests + ''' + scheck = SaltCheck() + paths = scheck.get_state_search_path_list() + stl = StateTestLoader(search_paths=paths) + results = {} + sls_list = _get_top_states() + all_states = [] + for top_state in sls_list: + sls_list = _get_state_sls(top_state) + for state in sls_list: + if state not in all_states: + all_states.append(state) + + for state_name in all_states: + mypath = stl.convert_sls_to_path(state_name) + stl.add_test_files_for_sls(mypath) + stl.load_test_suite() + results_dict = {} + for key, value in stl.test_dict.items(): + result = scheck.run_test(value) + results_dict[key] = result + results[state_name] = results_dict + passed = 0 + failed = 0 + missing_tests = 0 + for state in results: + if len(results[state].items()) == 0: + missing_tests = missing_tests + 1 + else: + for dummy, val in results[state].items(): + log.info("dummy={}, val={}".format(dummy, val)) + if val.startswith('Pass'): + passed = passed + 1 + if val.startswith('Fail'): + failed = failed + 1 + out_list = [] + for key, value in results.items(): + out_list.append({key: value}) + out_list.sort() + out_list.append({"TEST RESULTS": {'Passed': passed, 'Failed': failed, 'Missing Tests': missing_tests}}) + return out_list + + +def _is_valid_module(module): + '''return a list of all modules available on minion''' + modules = __salt__['sys.list_modules']() + return bool(module in modules) + + +def _get_auto_update_cache_value(): + '''return the config value of auto_update_master_cache''' + __salt__['config.get']('auto_update_master_cache') + return True + + +def _is_valid_function(module_name, function): + '''Determine if a function is valid for a module''' + try: + functions = __salt__['sys.list_functions'](module_name) + except salt.exceptions.SaltException: + functions = ["unable to look up functions"] + return "{0}.{1}".format(module_name, function) in functions + + +def _get_top_states(): + ''' equivalent to a salt cli: salt web state.show_top''' + alt_states = [] + try: + returned = __salt__['state.show_top']() + for i in returned['base']: + alt_states.append(i) + except Exception: + raise + # log.info("top states: {}".format(alt_states)) + return alt_states + + +def _get_state_sls(state): + ''' equivalent to a salt cli: salt web state.show_low_sls STATE''' + sls_list_state = [] + try: + returned = __salt__['state.show_low_sls'](state) + for i in returned: + if i['__sls__'] not in sls_list_state: + sls_list_state.append(i['__sls__']) + except Exception: + raise + return sls_list_state + + +class SaltCheck(object): + ''' + This class implements the saltcheck + ''' + + def __init__(self): + # self.sls_list_top = [] + self.sls_list_state = [] + self.modules = [] + self.results_dict = {} + self.results_dict_summary = {} + self.assertions_list = '''assertEqual assertNotEqual + assertTrue assertFalse + assertIn assertNotIn + assertGreater + assertGreaterEqual + assertLess assertLessEqual'''.split() + self.auto_update_master_cache = _get_auto_update_cache_value + # self.salt_lc = salt.client.Caller(mopts=__opts__) + self.salt_lc = salt.client.Caller() + if self.auto_update_master_cache: + update_master_cache() + + def __is_valid_test(self, test_dict): + '''Determine if a test contains: + a test name, + a valid module and function, + a valid assertion, + an expected return value''' + tots = 0 # need total of >= 6 to be a valid test + m_and_f = test_dict.get('module_and_function', None) + assertion = test_dict.get('assertion', None) + expected_return = test_dict.get('expected-return', None) + log.info("__is_valid_test has test: {}".format(test_dict)) + if m_and_f: + tots += 1 + module, function = m_and_f.split('.') + if _is_valid_module(module): + tots += 1 + if _is_valid_function(module, function): + tots += 1 + log.info("__is_valid_test has valid m_and_f") + if assertion: + tots += 1 + if assertion in self.assertions_list: + tots += 1 + log.info("__is_valid_test has valid_assertion") + if expected_return: + tots += 1 + log.info("__is_valid_test has valid_expected_return") + log.info("__is_valid_test score: {}".format(tots)) + return tots >= 6 + + def call_salt_command(self, + fun, + args, + kwargs): + '''Generic call of salt Caller command''' + value = False + try: + if args and kwargs: + value = self.salt_lc.cmd(fun, *args, **kwargs) + elif args and not kwargs: + value = self.salt_lc.cmd(fun, *args) + elif not args and kwargs: + value = self.salt_lc.cmd(fun, **kwargs) + else: + value = self.salt_lc.cmd(fun) + except salt.exceptions.SaltException: + raise + except Exception: + raise + return value + + def run_test(self, test_dict): + '''Run a single saltcheck test''' + if self.__is_valid_test(test_dict): + mod_and_func = test_dict['module_and_function'] + args = test_dict.get('args', None) + kwargs = test_dict.get('kwargs', None) + assertion = test_dict['assertion'] + expected_return = test_dict['expected-return'] + actual_return = self.call_salt_command(mod_and_func, args, kwargs) + if assertion != "assertIn": + expected_return = self.cast_expected_to_returned_type(expected_return, actual_return) + if assertion == "assertEqual": + value = self.__assert_equal(expected_return, actual_return) + elif assertion == "assertNotEqual": + value = self.__assert_not_equal(expected_return, actual_return) + elif assertion == "assertTrue": + value = self.__assert_true(expected_return) + elif assertion == "assertFalse": + value = self.__assert_false(expected_return) + elif assertion == "assertIn": + value = self.__assert_in(expected_return, actual_return) + elif assertion == "assertNotIn": + value = self.__assert_not_in(expected_return, actual_return) + elif assertion == "assertGreater": + value = self.__assert_greater(expected_return, actual_return) + elif assertion == "assertGreaterEqual": + value = self.__assert_greater_equal(expected_return, actual_return) + elif assertion == "assertLess": + value = self.__assert_less(expected_return, actual_return) + elif assertion == "assertLessEqual": + value = self.__assert_less_equal(expected_return, actual_return) + else: + value = "Fail - bas assertion" + else: + return "Fail - invalid test" + return value + + @staticmethod + def cast_expected_to_returned_type(expected, returned): + ''' + Determine the type of variable returned + Cast the expected to the type of variable returned + ''' + ret_type = type(returned) + new_expected = expected + if expected == "False" and ret_type == bool: + expected = False + try: + new_expected = ret_type(expected) + except ValueError: + log.info("Unable to cast expected into type of returned") + log.info("returned = {}".format(returned)) + log.info("type of returned = {}".format(type(returned))) + log.info("expected = {}".format(expected)) + log.info("type of expected = {}".format(type(expected))) + return new_expected + + @staticmethod + def __assert_equal(expected, returned): + ''' + Test if two objects are equal + ''' + result = "Pass" + + try: + assert (expected == returned), "{0} is not equal to {1}".format(expected, returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def __assert_not_equal(expected, returned): + ''' + Test if two objects are not equal + ''' + result = "Pass" + try: + assert (expected != returned), "{0} is equal to {1}".format(expected, returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def __assert_true(returned): + ''' + Test if an boolean is True + ''' + result = "Pass" + try: + assert (returned is True), "{0} not True".format(returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def __assert_false(returned): + ''' + Test if an boolean is False + ''' + result = "Pass" + if isinstance(returned, str): + try: + returned = bool(returned) + except ValueError: + raise + try: + assert (returned is False), "{0} not False".format(returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def __assert_in(expected, returned): + ''' + Test if a value is in the list of returned values + ''' + result = "Pass" + try: + assert (expected in returned), "{0} not False".format(returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def __assert_not_in(expected, returned): + ''' + Test if a value is not in the list of returned values + ''' + result = "Pass" + try: + assert (expected not in returned), "{0} not False".format(returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def __assert_greater(expected, returned): + ''' + Test if a value is greater than the returned value + ''' + result = "Pass" + try: + assert (expected > returned), "{0} not False".format(returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def __assert_greater_equal(expected, returned): + ''' + Test if a value is greater than or equal to the returned value + ''' + result = "Pass" + try: + assert (expected >= returned), "{0} not False".format(returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def __assert_less(expected, returned): + ''' + Test if a value is less than the returned value + ''' + result = "Pass" + try: + assert (expected < returned), "{0} not False".format(returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def __assert_less_equal(expected, returned): + ''' + Test if a value is less than or equal to the returned value + ''' + result = "Pass" + try: + assert (expected <= returned), "{0} not False".format(returned) + except AssertionError as err: + result = "Fail: " + str(err) + return result + + @staticmethod + def get_state_search_path_list(): + '''For the state file system, return a + list of paths to search for states''' + # state cache should be updated before running this method + search_list = [] + cachedir = __opts__.get('cachedir', None) + environment = __opts__['environment'] + if environment: + path = cachedir + os.sep + "files" + os.sep + environment + search_list.append(path) + path = cachedir + os.sep + "files" + os.sep + "base" + search_list.append(path) + return search_list + + +class StateTestLoader(object): + ''' + Class loads in test files for a state + e.g. state_dir/saltcheck-tests/[1.tst, 2.tst, 3.tst] + ''' + + def __init__(self, search_paths): + self.search_paths = search_paths + self.path_type = None + self.test_files = [] # list of file paths + self.test_dict = {} + + def load_test_suite(self): + '''load tests either from one file, or a set of files''' + self.test_dict = {} + for myfile in self.test_files: + self.load_file(myfile) + self.test_files = [] + + def load_file(self, filepath): + ''' + loads in one test file + ''' + try: + with salt.utils.files.fopen(filepath, 'r') as myfile: + # with open(filepath, 'r') as myfile: + contents_yaml = yaml.load(myfile) + for key, value in contents_yaml.items(): + self.test_dict[key] = value + except: + raise + return + + def gather_files(self, filepath): + '''gather files for a test suite''' + self.test_files = [] + log.info("gather_files: {}".format(time.time())) + filepath = filepath + os.sep + 'saltcheck-tests' + rootdir = filepath + # for dirname, subdirlist, filelist in os.walk(rootdir): + for dirname, dummy, filelist in os.walk(rootdir): + for fname in filelist: + if fname.endswith('.tst'): + start_path = dirname + os.sep + fname + full_path = os.path.abspath(start_path) + self.test_files.append(full_path) + return + + @staticmethod + def convert_sls_to_paths(sls_list): + '''Converting sls to paths''' + new_sls_list = [] + for sls in sls_list: + sls = sls.replace(".", os.sep) + new_sls_list.append(sls) + return new_sls_list + + @staticmethod + def convert_sls_to_path(sls): + '''Converting sls to paths''' + sls = sls.replace(".", os.sep) + return sls + + def add_test_files_for_sls(self, sls_path): + '''Adding test files''' + for path in self.search_paths: + full_path = path + os.sep + sls_path + rootdir = full_path + if os.path.isdir(full_path): + log.info("searching path= {}".format(full_path)) + # for dirname, subdirlist, filelist in os.walk(rootdir, topdown=True): + for dirname, subdirlist, dummy in os.walk(rootdir, topdown=True): + if "saltcheck-tests" in subdirlist: + self.gather_files(dirname) + log.info("test_files list: {}".format(self.test_files)) + log.info("found subdir match in = {}".format(dirname)) + else: + log.info("did not find subdir match in = {}".format(dirname)) + del subdirlist[:] + else: + log.info("path is not a directory= {}".format(full_path)) + return diff --git a/salt/modules/selinux.py b/salt/modules/selinux.py index 437e428fb0..c0f91c4ee7 100644 --- a/salt/modules/selinux.py +++ b/salt/modules/selinux.py @@ -375,8 +375,10 @@ def list_semod(): def _validate_filetype(filetype): ''' - Checks if the given filetype is a valid SELinux filetype specification. - Throws an SaltInvocationError if it isn't. + .. versionadded:: 2017.7.0 + + Checks if the given filetype is a valid SELinux filetype + specification. Throws an SaltInvocationError if it isn't. ''' if filetype not in _SELINUX_FILETYPES.keys(): raise SaltInvocationError('Invalid filetype given: {0}'.format(filetype)) @@ -385,6 +387,8 @@ def _validate_filetype(filetype): def _context_dict_to_string(context): ''' + .. versionadded:: 2017.7.0 + Converts an SELinux file context from a dict to a string. ''' return '{sel_user}:{sel_role}:{sel_type}:{sel_level}'.format(**context) @@ -392,6 +396,8 @@ def _context_dict_to_string(context): def _context_string_to_dict(context): ''' + .. versionadded:: 2017.7.0 + Converts an SELinux file context from string to dict. ''' if not re.match('[^:]+:[^:]+:[^:]+:[^:]+$', context): @@ -406,8 +412,11 @@ def _context_string_to_dict(context): def filetype_id_to_string(filetype='a'): ''' - Translates SELinux filetype single-letter representation - to a more human-readable version (which is also used in `semanage fcontext -l`). + .. versionadded:: 2017.7.0 + + Translates SELinux filetype single-letter representation to a more + human-readable version (which is also used in `semanage fcontext + -l`). ''' _validate_filetype(filetype) return _SELINUX_FILETYPES.get(filetype, 'error') @@ -415,20 +424,27 @@ def filetype_id_to_string(filetype='a'): def fcontext_get_policy(name, filetype=None, sel_type=None, sel_user=None, sel_level=None): ''' - Returns the current entry in the SELinux policy list as a dictionary. - Returns None if no exact match was found + .. versionadded:: 2017.7.0 + + Returns the current entry in the SELinux policy list as a + dictionary. Returns None if no exact match was found. + Returned keys are: - - filespec (the name supplied and matched) - - filetype (the descriptive name of the filetype supplied) - - sel_user, sel_role, sel_type, sel_level (the selinux context) + + * filespec (the name supplied and matched) + * filetype (the descriptive name of the filetype supplied) + * sel_user, sel_role, sel_type, sel_level (the selinux context) + For a more in-depth explanation of the selinux context, go to https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security-Enhanced_Linux/chap-Security-Enhanced_Linux-SELinux_Contexts.html - name: filespec of the file or directory. Regex syntax is allowed. - filetype: The SELinux filetype specification. - Use one of [a, f, d, c, b, s, l, p]. - See also `man semanage-fcontext`. - Defaults to 'a' (all files) + name + filespec of the file or directory. Regex syntax is allowed. + + filetype + The SELinux filetype specification. Use one of [a, f, d, c, b, + s, l, p]. See also `man semanage-fcontext`. Defaults to 'a' + (all files). CLI Example: @@ -461,20 +477,34 @@ def fcontext_get_policy(name, filetype=None, sel_type=None, sel_user=None, sel_l def fcontext_add_or_delete_policy(action, name, filetype=None, sel_type=None, sel_user=None, sel_level=None): ''' - Sets or deletes the SELinux policy for a given filespec and other optional parameters. - Returns the result of the call to semanage. - Note that you don't have to remove an entry before setting a new one for a given - filespec and filetype, as adding one with semanage automatically overwrites a - previously configured SELinux context. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. - file_type: The SELinux filetype specification. - Use one of [a, f, d, c, b, s, l, p]. - See also ``man semanage-fcontext``. - Defaults to 'a' (all files) - sel_type: SELinux context type. There are many. - sel_user: SELinux user. Use ``semanage login -l`` to determine which ones are available to you - sel_level: The MLS range of the SELinux context. + Sets or deletes the SELinux policy for a given filespec and other + optional parameters. + + Returns the result of the call to semanage. + + Note that you don't have to remove an entry before setting a new + one for a given filespec and filetype, as adding one with semanage + automatically overwrites a previously configured SELinux context. + + name + filespec of the file or directory. Regex syntax is allowed. + + file_type + The SELinux filetype specification. Use one of [a, f, d, c, b, + s, l, p]. See also ``man semanage-fcontext``. Defaults to 'a' + (all files). + + sel_type + SELinux context type. There are many. + + sel_user + SELinux user. Use ``semanage login -l`` to determine which ones + are available to you. + + sel_level + The MLS range of the SELinux context. CLI Example: @@ -500,10 +530,14 @@ def fcontext_add_or_delete_policy(action, name, filetype=None, sel_type=None, se def fcontext_policy_is_applied(name, recursive=False): ''' - Returns an empty string if the SELinux policy for a given filespec is applied, - returns string with differences in policy and actual situation otherwise. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. + Returns an empty string if the SELinux policy for a given filespec + is applied, returns string with differences in policy and actual + situation otherwise. + + name + filespec of the file or directory. Regex syntax is allowed. CLI Example: @@ -520,11 +554,17 @@ def fcontext_policy_is_applied(name, recursive=False): def fcontext_apply_policy(name, recursive=False): ''' - Applies SElinux policies to filespec using `restorecon [-R] filespec`. - Returns dict with changes if succesful, the output of the restorecon command otherwise. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. - recursive: Recursively apply SELinux policies. + Applies SElinux policies to filespec using `restorecon [-R] + filespec`. Returns dict with changes if succesful, the output of + the restorecon command otherwise. + + name + filespec of the file or directory. Regex syntax is allowed. + + recursive + Recursively apply SELinux policies. CLI Example: diff --git a/salt/modules/ssh.py b/salt/modules/ssh.py index 7f1d042c01..cba2035534 100644 --- a/salt/modules/ssh.py +++ b/salt/modules/ssh.py @@ -769,10 +769,13 @@ def set_auth_key( with salt.utils.files.fopen(fconfig, 'ab+') as _fh: if new_file is False: # Let's make sure we have a new line at the end of the file - _fh.seek(1024, 2) - if not _fh.read(1024).rstrip(six.b(' ')).endswith(six.b('\n')): - _fh.seek(0, 2) - _fh.write(six.b('\n')) + _fh.seek(0, 2) + if _fh.tell() > 0: + # File isn't empty, check if last byte is a newline + # If not, add one + _fh.seek(-1, 2) + if _fh.read(1) != six.b('\n'): + _fh.write(six.b('\n')) if six.PY3: auth_line = auth_line.encode(__salt_system_encoding__) _fh.write(auth_line) diff --git a/salt/modules/state.py b/salt/modules/state.py index c585489419..bd2d90893f 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -98,8 +98,7 @@ def _set_retcode(ret, highstate=None): if isinstance(ret, list): __context__['retcode'] = 1 return - if not salt.utils.check_state_result(ret, highstate=highstate): - + if not __utils__['state.check_result'](ret, highstate=highstate): __context__['retcode'] = 2 @@ -121,7 +120,7 @@ def _wait(jid): Wait for all previously started state jobs to finish running ''' if jid is None: - jid = salt.utils.jid.gen_jid() + jid = salt.utils.jid.gen_jid(__opts__) states = _prior_running_states(jid) while states: time.sleep(1) @@ -316,7 +315,7 @@ def low(data, queue=False, **kwargs): ret = st_.call(data) if isinstance(ret, list): __context__['retcode'] = 1 - if salt.utils.check_state_result(ret): + if __utils__['state.check_result'](ret): __context__['retcode'] = 2 return ret @@ -397,12 +396,7 @@ def template(tem, queue=False, **kwargs): salt '*' state.template '' ''' if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') conflict = _check_queue(queue, kwargs) @@ -839,12 +833,7 @@ def highstate(test=None, queue=False, **kwargs): opts['test'] = _get_test_value(test, **kwargs) if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') if 'saltenv' in kwargs: @@ -1006,12 +995,7 @@ def sls(mods, test=None, exclude=None, queue=False, **kwargs): ''' concurrent = kwargs.get('concurrent', False) if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') # Modification to __opts__ lost after this if-else @@ -1489,12 +1473,7 @@ def show_low_sls(mods, test=None, queue=False, **kwargs): salt '*' state.show_low_sls foo saltenv=dev ''' if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') conflict = _check_queue(queue, kwargs) @@ -1580,12 +1559,7 @@ def show_sls(mods, test=None, queue=False, **kwargs): salt '*' state.show_sls core,edit.vim dev ''' if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') conflict = _check_queue(queue, kwargs) @@ -1656,12 +1630,7 @@ def show_top(queue=False, **kwargs): salt '*' state.show_top ''' if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') conflict = _check_queue(queue, kwargs) diff --git a/salt/modules/sysmod.py b/salt/modules/sysmod.py index 93a0c99321..31de9fa67b 100644 --- a/salt/modules/sysmod.py +++ b/salt/modules/sysmod.py @@ -12,8 +12,8 @@ import logging import salt.loader import salt.runner import salt.state -import salt.utils -import salt.utils.schema as S +import salt.utils.args +import salt.utils.schema from salt.utils.doc import strip_rst as _strip_rst from salt.ext.six.moves import zip @@ -450,7 +450,7 @@ def argspec(module=''): salt '*' sys.argspec 'pkg.*' ''' - return salt.utils.argspec_report(__salt__, module) + return salt.utils.args.argspec_report(__salt__, module) def state_argspec(module=''): @@ -476,7 +476,7 @@ def state_argspec(module=''): ''' st_ = salt.state.State(__opts__) - return salt.utils.argspec_report(st_.states, module) + return salt.utils.args.argspec_report(st_.states, module) def returner_argspec(module=''): @@ -502,7 +502,7 @@ def returner_argspec(module=''): ''' returners_ = salt.loader.returners(__opts__, []) - return salt.utils.argspec_report(returners_, module) + return salt.utils.args.argspec_report(returners_, module) def runner_argspec(module=''): @@ -527,7 +527,7 @@ def runner_argspec(module=''): salt '*' sys.runner_argspec 'winrepo.*' ''' run_ = salt.runner.Runner(__opts__) - return salt.utils.argspec_report(run_.functions, module) + return salt.utils.args.argspec_report(run_.functions, module) def list_state_functions(*args, **kwargs): # pylint: disable=unused-argument @@ -844,28 +844,28 @@ def _argspec_to_schema(mod, spec): } for i in args_req: - types[i] = S.OneOfItem(items=( - S.BooleanItem(title=i, description=i, required=True), - S.IntegerItem(title=i, description=i, required=True), - S.NumberItem(title=i, description=i, required=True), - S.StringItem(title=i, description=i, required=True), + types[i] = salt.utils.schema.OneOfItem(items=( + salt.utils.schema.BooleanItem(title=i, description=i, required=True), + salt.utils.schema.IntegerItem(title=i, description=i, required=True), + salt.utils.schema.NumberItem(title=i, description=i, required=True), + salt.utils.schema.StringItem(title=i, description=i, required=True), # S.ArrayItem(title=i, description=i, required=True), # S.DictItem(title=i, description=i, required=True), )) for i, j in args_defaults: - types[i] = S.OneOfItem(items=( - S.BooleanItem(title=i, description=i, default=j), - S.IntegerItem(title=i, description=i, default=j), - S.NumberItem(title=i, description=i, default=j), - S.StringItem(title=i, description=i, default=j), + types[i] = salt.utils.schema.OneOfItem(items=( + salt.utils.schema.BooleanItem(title=i, description=i, default=j), + salt.utils.schema.IntegerItem(title=i, description=i, default=j), + salt.utils.schema.NumberItem(title=i, description=i, default=j), + salt.utils.schema.StringItem(title=i, description=i, default=j), # S.ArrayItem(title=i, description=i, default=j), # S.DictItem(title=i, description=i, default=j), )) - return type(mod, (S.Schema,), types).serialize() + return type(mod, (salt.utils.schema.Schema,), types).serialize() def state_schema(module=''): diff --git a/salt/modules/system.py b/salt/modules/system.py index 54caa7c642..87673a372e 100644 --- a/salt/modules/system.py +++ b/salt/modules/system.py @@ -181,7 +181,10 @@ def has_settable_hwclock(): salt '*' system.has_settable_hwclock ''' if salt.utils.path.which_bin(['hwclock']) is not None: - res = __salt__['cmd.run_all'](['hwclock', '--test', '--systohc'], python_shell=False) + res = __salt__['cmd.run_all']( + ['hwclock', '--test', '--systohc'], python_shell=False, + output_loglevel='quiet', ignore_retcode=True + ) return res['retcode'] == 0 return False diff --git a/salt/modules/textfsm_mod.py b/salt/modules/textfsm_mod.py new file mode 100644 index 0000000000..760f27d3a0 --- /dev/null +++ b/salt/modules/textfsm_mod.py @@ -0,0 +1,459 @@ +# -*- coding: utf-8 -*- +''' +TextFSM +======= + +.. versionadded:: Oxygen + +Execution module that processes plain text and extracts data +using TextFSM templates. The output is presented in JSON serializable +data, and can be easily re-used in other modules, or directly +inside the renderer (Jinja, Mako, Genshi, etc.). + +:depends: - textfsm Python library + +.. note:: + + For Python 2/3 compatibility, it is more recommended to + install the ``jtextfsm`` library: ``pip install jtextfsm``. +''' +from __future__ import absolute_import + +# Import python libs +import os +import logging + +# Import third party modules +try: + import textfsm + HAS_TEXTFSM = True +except ImportError: + HAS_TEXTFSM = False + +try: + import clitable + HAS_CLITABLE = True +except ImportError: + HAS_CLITABLE = False + +try: + from salt.utils.files import fopen +except ImportError: + from salt.utils import fopen + +log = logging.getLogger(__name__) + +__virtualname__ = 'textfsm' +__proxyenabled__ = ['*'] + + +def __virtual__(): + ''' + Only load this execution module if TextFSM is installed. + ''' + if HAS_TEXTFSM: + return __virtualname__ + return (False, 'The textfsm execution module failed to load: requires the textfsm library.') + + +def _clitable_to_dict(objects, fsm_handler): + ''' + Converts TextFSM cli_table object to list of dictionaries. + ''' + objs = [] + log.debug('Cli Table:') + log.debug(objects) + log.debug('FSM handler:') + log.debug(fsm_handler) + for row in objects: + temp_dict = {} + for index, element in enumerate(row): + temp_dict[fsm_handler.header[index].lower()] = element + objs.append(temp_dict) + log.debug('Extraction result:') + log.debug(objs) + return objs + + +def extract(template_path, raw_text=None, raw_text_file=None, saltenv='base'): + r''' + Extracts the data entities from the unstructured + raw text sent as input and returns the data + mapping, processing using the TextFSM template. + + template_path + The path to the TextFSM template. + This can be specified using the absolute path + to the file, or using one of the following URL schemes: + + - ``salt://``, to fetch the template from the Salt fileserver. + - ``http://`` or ``https://`` + - ``ftp://`` + - ``s3://`` + - ``swift://`` + + raw_text: ``None`` + The unstructured text to be parsed. + + raw_text_file: ``None`` + Text file to read, having the raw text to be parsed using the TextFSM template. + Supports the same URL schemes as the ``template_path`` argument. + + saltenv: ``base`` + Salt fileserver envrionment from which to retrieve the file. + Ignored if ``template_path`` is not a ``salt://`` URL. + + CLI Example: + + .. code-block:: bash + + salt '*' textfsm.extract salt://textfsm/juniper_version_template raw_text_file=s3://junos_ver.txt + salt '*' textfsm.extract http://some-server/textfsm/juniper_version_template raw_text='Hostname: router.abc ... snip ...' + + Jinja template example: + + .. code-block:: jinja + + {%- set raw_text = 'Hostname: router.abc ... snip ...' -%} + {%- set textfsm_extract = salt.textfsm.extract('https://some-server/textfsm/juniper_version_template', raw_text) -%} + + Raw text example: + + .. code-block:: text + + Hostname: router.abc + Model: mx960 + JUNOS Base OS boot [9.1S3.5] + JUNOS Base OS Software Suite [9.1S3.5] + JUNOS Kernel Software Suite [9.1S3.5] + JUNOS Crypto Software Suite [9.1S3.5] + JUNOS Packet Forwarding Engine Support (M/T Common) [9.1S3.5] + JUNOS Packet Forwarding Engine Support (MX Common) [9.1S3.5] + JUNOS Online Documentation [9.1S3.5] + JUNOS Routing Software Suite [9.1S3.5] + + TextFSM Example: + + .. code-block:: text + + Value Chassis (\S+) + Value Required Model (\S+) + Value Boot (.*) + Value Base (.*) + Value Kernel (.*) + Value Crypto (.*) + Value Documentation (.*) + Value Routing (.*) + + Start + # Support multiple chassis systems. + ^\S+:$$ -> Continue.Record + ^${Chassis}:$$ + ^Model: ${Model} + ^JUNOS Base OS boot \[${Boot}\] + ^JUNOS Software Release \[${Base}\] + ^JUNOS Base OS Software Suite \[${Base}\] + ^JUNOS Kernel Software Suite \[${Kernel}\] + ^JUNOS Crypto Software Suite \[${Crypto}\] + ^JUNOS Online Documentation \[${Documentation}\] + ^JUNOS Routing Software Suite \[${Routing}\] + + Output example: + + .. code-block:: json + + { + "comment": "", + "result": true, + "out": [ + { + "kernel": "9.1S3.5", + "documentation": "9.1S3.5", + "boot": "9.1S3.5", + "crypto": "9.1S3.5", + "chassis": "", + "routing": "9.1S3.5", + "base": "9.1S3.5", + "model": "mx960" + } + ] + } + ''' + ret = { + 'result': False, + 'comment': '', + 'out': None + } + log.debug('Using the saltenv: {}'.format(saltenv)) + log.debug('Caching {} using the Salt fileserver'.format(template_path)) + tpl_cached_path = __salt__['cp.cache_file'](template_path, saltenv=saltenv) + if tpl_cached_path is False: + ret['comment'] = 'Unable to read the TextFSM template from {}'.format(template_path) + log.error(ret['comment']) + return ret + try: + log.debug('Reading TextFSM template from cache path: {}'.format(tpl_cached_path)) + # Disabling pylint W8470 to nto complain about fopen. + # Unfortunately textFSM needs the file handle rather than the content... + # pylint: disable=W8470 + tpl_file_handle = fopen(tpl_cached_path, 'r') + # pylint: disable=W8470 + log.debug(tpl_file_handle.read()) + tpl_file_handle.seek(0) # move the object position back at the top of the file + fsm_handler = textfsm.TextFSM(tpl_file_handle) + except textfsm.TextFSMTemplateError as tfte: + log.error('Unable to parse the TextFSM template', exc_info=True) + ret['comment'] = 'Unable to parse the TextFSM template from {}: {}. Please check the logs.'.format( + template_path, tfte) + return ret + if not raw_text and raw_text_file: + log.debug('Trying to read the raw input from {}'.format(raw_text_file)) + raw_text = __salt__['cp.get_file_str'](raw_text_file, saltenv=saltenv) + if raw_text is False: + ret['comment'] = 'Unable to read from {}. Please specify a valid input file or text.'.format(raw_text_file) + log.error(ret['comment']) + return ret + if not raw_text: + ret['comment'] = 'Please specify a valid input file or text.' + log.error(ret['comment']) + return ret + log.debug('Processing the raw text:') + log.debug(raw_text) + objects = fsm_handler.ParseText(raw_text) + ret['out'] = _clitable_to_dict(objects, fsm_handler) + ret['result'] = True + return ret + + +def index(command, + platform=None, + platform_grain_name=None, + platform_column_name=None, + output=None, + output_file=None, + textfsm_path=None, + index_file=None, + saltenv='base', + include_empty=False, + include_pat=None, + exclude_pat=None): + ''' + Dynamically identify the template required to extract the + information from the unstructured raw text. + + The output has the same structure as the ``extract`` execution + function, the difference being that ``index`` is capable + to identify what template to use, based on the platform + details and the ``command``. + + command + The command executed on the device, to get the output. + + platform + The platform name, as defined in the TextFSM index file. + + .. note:: + For ease of use, it is recommended to define the TextFSM + indexfile with values that can be matches using the grains. + + platform_grain_name + The name of the grain used to identify the platform name + in the TextFSM index file. + + .. note:: + This option can be also specified in the minion configuration + file or pillar as ``textfsm_platform_grain``. + + .. note:: + This option is ignored when ``platform`` is specified. + + platform_column_name: ``Platform`` + The column name used to identify the platform, + exactly as specified in the TextFSM index file. + Default: ``Platform``. + + .. note:: + This is field is case sensitive, make sure + to assign the correct value to this option, + exactly as defined in the index file. + + .. note:: + This option can be also specified in the minion configuration + file or pillar as ``textfsm_platform_column_name``. + + output + The raw output from the device, to be parsed + and extract the structured data. + + output_file + The path to a file that contains the raw output from the device, + used to extract the structured data. + This option supports the usual Salt-specific schemes: ``file://``, + ``salt://``, ``http://``, ``https://``, ``ftp://``, ``s3://``, ``swift://``. + + textfsm_path + The path where the TextFSM templates can be found. This can be either + absolute path on the server, either specified using the following URL + schemes: ``file://``, ``salt://``, ``http://``, ``https://``, ``ftp://``, + ``s3://``, ``swift://``. + + .. note:: + This needs to be a directory with a flat structure, having an + index file (whose name can be specified using the ``index_file`` option) + and a number of TextFSM templates. + + .. note:: + This option can be also specified in the minion configuration + file or pillar as ``textfsm_path``. + + index_file: ``index`` + The name of the TextFSM index file, under the ``textfsm_path``. Default: ``index``. + + .. note:: + This option can be also specified in the minion configuration + file or pillar as ``textfsm_index_file``. + + saltenv: ``base`` + Salt fileserver envrionment from which to retrieve the file. + Ignored if ``textfsm_path`` is not a ``salt://`` URL. + + include_empty: ``False`` + Include empty files under the ``textfsm_path``. + + include_pat + Glob or regex to narrow down the files cached from the given path. + If matching with a regex, the regex must be prefixed with ``E@``, + otherwise the expression will be interpreted as a glob. + + exclude_pat + Glob or regex to exclude certain files from being cached from the given path. + If matching with a regex, the regex must be prefixed with ``E@``, + otherwise the expression will be interpreted as a glob. + + .. note:: + If used with ``include_pat``, files matching this pattern will be + excluded from the subset of files defined by ``include_pat``. + + CLI Example: + + .. code-block:: bash + + salt '*' textfsm.index 'sh ver' platform=Juniper output_file=salt://textfsm/juniper_version_example textfsm_path=salt://textfsm/ + salt '*' textfsm.index 'sh ver' output_file=salt://textfsm/juniper_version_example textfsm_path=ftp://textfsm/ platform_column_name=Vendor + salt '*' textfsm.index 'sh ver' output_file=salt://textfsm/juniper_version_example textfsm_path=https://some-server/textfsm/ platform_column_name=Vendor platform_grain_name=vendor + + TextFSM index file example: + + ``salt://textfsm/index`` + + .. code-block:: text + + Template, Hostname, Vendor, Command + juniper_version_template, .*, Juniper, sh[[ow]] ve[[rsion]] + + The usage can be simplified, + by defining (some of) the following options: ``textfsm_platform_grain``, + ``textfsm_path``, ``textfsm_platform_column_name``, or ``textfsm_index_file``, + in the (proxy) minion configuration file or pillar. + + Configuration example: + + .. code-block:: yaml + + textfsm_platform_grain: vendor + textfsm_path: salt://textfsm/ + textfsm_platform_column_name: Vendor + + And the CLI usage becomes as simple as: + + .. code-block:: bash + + salt '*' textfsm.index 'sh ver' output_file=salt://textfsm/juniper_version_example + + Usgae inside a Jinja template: + + .. code-block:: jinja + + {%- set command = 'sh ver' -%} + {%- set output = salt.net.cli(command) -%} + {%- set textfsm_extract = salt.textfsm.index(command, output=output) -%} + ''' + ret = { + 'out': None, + 'result': False, + 'comment': '' + } + if not HAS_CLITABLE: + ret['comment'] = 'TextFSM doesnt seem that has clitable embedded.' + log.error(ret['comment']) + return ret + if not platform: + platform_grain_name = __opts__.get('textfsm_platform_grain') or\ + __pillar__.get('textfsm_platform_grain', platform_grain_name) + if platform_grain_name: + log.debug('Using the {} grain to identify the platform name'.format(platform_grain_name)) + platform = __grains__.get(platform_grain_name) + if not platform: + ret['comment'] = 'Unable to identify the platform name using the {} grain.'.format(platform_grain_name) + return ret + log.info('Using platform: {}'.format(platform)) + else: + ret['comment'] = 'No platform specified, no platform grain identifier configured.' + log.error(ret['comment']) + return ret + if not textfsm_path: + log.debug('No TextFSM templates path specified, trying to look into the opts and pillar') + textfsm_path = __opts__.get('textfsm_path') or __pillar__.get('textfsm_path') + if not textfsm_path: + ret['comment'] = 'No TextFSM templates path specified. Please configure in opts/pillar/function args.' + log.error(ret['comment']) + return ret + log.debug('Using the saltenv: {}'.format(saltenv)) + log.debug('Caching {} using the Salt fileserver'.format(textfsm_path)) + textfsm_cachedir_ret = __salt__['cp.cache_dir'](textfsm_path, + saltenv=saltenv, + include_empty=include_empty, + include_pat=include_pat, + exclude_pat=exclude_pat) + log.debug('Cache fun return:') + log.debug(textfsm_cachedir_ret) + if not textfsm_cachedir_ret: + ret['comment'] = 'Unable to fetch from {}. Is the TextFSM path correctly specified?'.format(textfsm_path) + log.error(ret['comment']) + return ret + textfsm_cachedir = os.path.dirname(textfsm_cachedir_ret[0]) # first item + index_file = __opts__.get('textfsm_index_file') or __pillar__.get('textfsm_index_file', 'index') + index_file_path = os.path.join(textfsm_cachedir, index_file) + log.debug('Using the cached index file: {}'.format(index_file_path)) + log.debug('TextFSM templates cached under: {}'.format(textfsm_cachedir)) + textfsm_obj = clitable.CliTable(index_file_path, textfsm_cachedir) + attrs = { + 'Command': command + } + platform_column_name = __opts__.get('textfsm_platform_column_name') or\ + __pillar__.get('textfsm_platform_column_name', 'Platform') + log.info('Using the TextFSM platform idenfiticator: {}'.format(platform_column_name)) + attrs[platform_column_name] = platform + log.debug('Processing the TextFSM index file using the attributes: {}'.format(attrs)) + if not output and output_file: + log.debug('Processing the output from {}'.format(output_file)) + output = __salt__['cp.get_file_str'](output_file, saltenv=saltenv) + if output is False: + ret['comment'] = 'Unable to read from {}. Please specify a valid file or text.'.format(output_file) + log.error(ret['comment']) + return ret + if not output: + ret['comment'] = 'Please specify a valid output text or file' + log.error(ret['comment']) + return ret + log.debug('Processing the raw text:') + log.debug(output) + try: + # Parse output through template + textfsm_obj.ParseCmd(output, attrs) + ret['out'] = _clitable_to_dict(textfsm_obj, textfsm_obj) + ret['result'] = True + except clitable.CliTableError as cterr: + log.error('Unable to proces the CliTable', exc_info=True) + ret['comment'] = 'Unable to process the output: {}'.format(cterr) + return ret diff --git a/salt/modules/virtualenv_mod.py b/salt/modules/virtualenv_mod.py index 8c2849e2ef..20145c57e9 100644 --- a/salt/modules/virtualenv_mod.py +++ b/salt/modules/virtualenv_mod.py @@ -200,12 +200,10 @@ def create(path, for entry in extra_search_dir: cmd.append('--extra-search-dir={0}'.format(entry)) if never_download is True: - if virtualenv_version_info >= (1, 10): + if virtualenv_version_info >= (1, 10) and virtualenv_version_info < (14, 0, 0): log.info( - 'The virtualenv \'--never-download\' option has been ' - 'deprecated in virtualenv(>=1.10), as such, the ' - '\'never_download\' option to `virtualenv.create()` has ' - 'also been deprecated and it\'s not necessary anymore.' + '--never-download was deprecated in 1.10.0, but reimplemented in 14.0.0. ' + 'If this feature is needed, please install a supported virtualenv version.' ) else: cmd.append('--never-download') diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 326294c7e6..d6aabb74e4 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -24,8 +24,8 @@ PyVmomi can be installed via pip: .. note:: Version 6.0 of pyVmomi has some problems with SSL error handling on certain - versions of Python. If using version 6.0 of pyVmomi, Python 2.6, - Python 2.7.9, or newer must be present. This is due to an upstream dependency + versions of Python. If using version 6.0 of pyVmomi, Python 2.7.9, + or newer must be present. This is due to an upstream dependency in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the version of Python is not in the supported range, you will need to install an earlier version of pyVmomi. See `Issue #29537`_ for more information. @@ -176,12 +176,24 @@ import salt.utils.dictupdate as dictupdate import salt.utils.http import salt.utils.path import salt.utils.vmware -from salt.exceptions import CommandExecutionError, VMwareSaltError +import salt.utils.vsan +from salt.exceptions import CommandExecutionError, VMwareSaltError, \ + ArgumentValueError, InvalidConfigError, VMwareObjectRetrievalError, \ + VMwareApiError, InvalidEntityError from salt.utils.decorators import depends, ignores_kwargs +from salt.config.schemas.esxcluster import ESXClusterConfigSchema, \ + ESXClusterEntitySchema +from salt.config.schemas.vcenter import VCenterEntitySchema # Import Third Party Libs try: - from pyVmomi import vim, vmodl + import jsonschema + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False + +try: + from pyVmomi import vim, vmodl, VmomiSupport HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False @@ -195,10 +207,21 @@ else: log = logging.getLogger(__name__) __virtualname__ = 'vsphere' -__proxyenabled__ = ['esxi', 'esxdatacenter'] +__proxyenabled__ = ['esxi', 'esxcluster', 'esxdatacenter'] def __virtual__(): + if not HAS_JSONSCHEMA: + return False, 'Execution module did not load: jsonschema not found' + if not HAS_PYVMOMI: + return False, 'Execution module did not load: pyVmomi not found' + + # We check the supported vim versions to infer the pyVmomi version + if 'vim25/6.0' in VmomiSupport.versionMap and \ + sys.version_info > (2, 7) and sys.version_info < (2, 7, 9): + + return False, ('Execution module did not load: Incompatible versions ' + 'of Python and pyVmomi present. See Issue #29537.') return __virtualname__ @@ -227,6 +250,8 @@ def _get_proxy_connection_details(): proxytype = get_proxy_type() if proxytype == 'esxi': details = __salt__['esxi.get_details']() + elif proxytype == 'esxcluster': + details = __salt__['esxcluster.get_details']() elif proxytype == 'esxdatacenter': details = __salt__['esxdatacenter.get_details']() else: @@ -267,7 +292,7 @@ def gets_service_instance_via_proxy(fn): proxy details and passes the connection (vim.ServiceInstance) to the decorated function. - Supported proxies: esxi, esxdatacenter. + Supported proxies: esxi, esxcluster, esxdatacenter. Notes: 1. The decorated function must have a ``service_instance`` parameter @@ -354,7 +379,7 @@ def gets_service_instance_via_proxy(fn): @depends(HAS_PYVMOMI) -@supports_proxies('esxi', 'esxdatacenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') def get_service_instance_via_proxy(service_instance=None): ''' Returns a service instance to the proxied endpoint (vCenter/ESXi host). @@ -374,7 +399,7 @@ def get_service_instance_via_proxy(service_instance=None): @depends(HAS_PYVMOMI) -@supports_proxies('esxi', 'esxdatacenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') def disconnect(service_instance): ''' Disconnects from a vCenter or ESXi host @@ -1909,7 +1934,7 @@ def get_vsan_eligible_disks(host, username, password, protocol=None, port=None, @depends(HAS_PYVMOMI) -@supports_proxies('esxi', 'esxdatacenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') @gets_service_instance_via_proxy def test_vcenter_connection(service_instance=None): ''' @@ -3598,7 +3623,7 @@ def vsan_enable(host, username, password, protocol=None, port=None, host_names=N @depends(HAS_PYVMOMI) -@supports_proxies('esxdatacenter') +@supports_proxies('esxdatacenter', 'esxcluster') @gets_service_instance_via_proxy def list_datacenters_via_proxy(datacenter_names=None, service_instance=None): ''' @@ -3659,6 +3684,810 @@ def create_datacenter(datacenter_name, service_instance=None): return {'create_datacenter': True} +def _get_cluster_dict(cluster_name, cluster_ref): + ''' + Returns a cluster dict representation from + a vim.ClusterComputeResource object. + + cluster_name + Name of the cluster + + cluster_ref + Reference to the cluster + ''' + + log.trace('Building a dictionary representation of cluster ' + '\'{0}\''.format(cluster_name)) + props = salt.utils.vmware.get_properties_of_managed_object( + cluster_ref, + properties=['configurationEx']) + res = {'ha': {'enabled': props['configurationEx'].dasConfig.enabled}, + 'drs': {'enabled': props['configurationEx'].drsConfig.enabled}} + # Convert HA properties of interest + ha_conf = props['configurationEx'].dasConfig + log.trace('ha_conf = {0}'.format(ha_conf)) + res['ha']['admission_control_enabled'] = ha_conf.admissionControlEnabled + if ha_conf.admissionControlPolicy and \ + isinstance(ha_conf.admissionControlPolicy, + vim.ClusterFailoverResourcesAdmissionControlPolicy): + pol = ha_conf.admissionControlPolicy + res['ha']['admission_control_policy'] = \ + {'cpu_failover_percent': pol.cpuFailoverResourcesPercent, + 'memory_failover_percent': pol.memoryFailoverResourcesPercent} + if ha_conf.defaultVmSettings: + def_vm_set = ha_conf.defaultVmSettings + res['ha']['default_vm_settings'] = \ + {'isolation_response': def_vm_set.isolationResponse, + 'restart_priority': def_vm_set.restartPriority} + res['ha']['hb_ds_candidate_policy'] = \ + ha_conf.hBDatastoreCandidatePolicy + if ha_conf.hostMonitoring: + res['ha']['host_monitoring'] = ha_conf.hostMonitoring + if ha_conf.option: + res['ha']['options'] = [{'key': o.key, 'value': o.value} + for o in ha_conf.option] + res['ha']['vm_monitoring'] = ha_conf.vmMonitoring + # Convert DRS properties + drs_conf = props['configurationEx'].drsConfig + log.trace('drs_conf = {0}'.format(drs_conf)) + res['drs']['vmotion_rate'] = 6 - drs_conf.vmotionRate + res['drs']['default_vm_behavior'] = drs_conf.defaultVmBehavior + # vm_swap_placement + res['vm_swap_placement'] = props['configurationEx'].vmSwapPlacement + # Convert VSAN properties + si = salt.utils.vmware.get_service_instance_from_managed_object( + cluster_ref) + + if salt.utils.vsan.vsan_supported(si): + # XXX The correct way of retrieving the VSAN data (on the if branch) + # is not supported before 60u2 vcenter + vcenter_info = salt.utils.vmware.get_service_info(si) + if int(vcenter_info.build) >= 3634794: # 60u2 + # VSAN API is fully supported by the VC starting with 60u2 + vsan_conf = salt.utils.vsan.get_cluster_vsan_info(cluster_ref) + log.trace('vsan_conf = {0}'.format(vsan_conf)) + res['vsan'] = {'enabled': vsan_conf.enabled, + 'auto_claim_storage': + vsan_conf.defaultConfig.autoClaimStorage} + if vsan_conf.dataEfficiencyConfig: + data_eff = vsan_conf.dataEfficiencyConfig + res['vsan'].update({ + # We force compression_enabled to be True/False + 'compression_enabled': + data_eff.compressionEnabled or False, + 'dedup_enabled': data_eff.dedupEnabled}) + else: # before 60u2 (no advanced vsan info) + if props['configurationEx'].vsanConfigInfo: + default_config = \ + props['configurationEx'].vsanConfigInfo.defaultConfig + res['vsan'] = { + 'enabled': props['configurationEx'].vsanConfigInfo.enabled, + 'auto_claim_storage': default_config.autoClaimStorage} + return res + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def list_cluster(datacenter=None, cluster=None, service_instance=None): + ''' + Returns a dict representation of an ESX cluster. + + datacenter + Name of datacenter containing the cluster. + Ignored if already contained by proxy details. + Default value is None. + + cluster + Name of cluster. + Ignored if already contained by proxy details. + Default value is None. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + + # vcenter proxy + salt '*' vsphere.list_cluster datacenter=dc1 cluster=cl1 + + # esxdatacenter proxy + salt '*' vsphere.list_cluster cluster=cl1 + + # esxcluster proxy + salt '*' vsphere.list_cluster + ''' + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + dc_ref = _get_proxy_target(service_instance) + if not cluster: + raise ArgumentValueError('\'cluster\' needs to be specified') + cluster_ref = salt.utils.vmware.get_cluster(dc_ref, cluster) + elif proxy_type == 'esxcluster': + cluster_ref = _get_proxy_target(service_instance) + cluster = __salt__['esxcluster.get_details']()['cluster'] + log.trace('Retrieving representation of cluster \'{0}\' in a ' + '{1} proxy'.format(cluster, proxy_type)) + return _get_cluster_dict(cluster, cluster_ref) + + +def _apply_cluster_dict(cluster_spec, cluster_dict, vsan_spec=None, + vsan_61=True): + ''' + Applies the values of cluster_dict dictionary to a cluster spec + (vim.ClusterConfigSpecEx). + + All vsan values (cluster_dict['vsan']) will be applied to + vsan_spec (vim.vsan.cluster.ConfigInfoEx). Can be not omitted + if not required. + + VSAN 6.1 config needs to be applied differently than the post VSAN 6.1 way. + The type of configuration desired is dictated by the flag vsan_61. + ''' + log.trace('Applying cluster dict {0}'.format(cluster_dict)) + if cluster_dict.get('ha'): + ha_dict = cluster_dict['ha'] + if not cluster_spec.dasConfig: + cluster_spec.dasConfig = vim.ClusterDasConfigInfo() + das_config = cluster_spec.dasConfig + if 'enabled' in ha_dict: + das_config.enabled = ha_dict['enabled'] + if ha_dict['enabled']: + # Default values when ha is enabled + das_config.failoverLevel = 1 + if 'admission_control_enabled' in ha_dict: + das_config.admissionControlEnabled = \ + ha_dict['admission_control_enabled'] + if 'admission_control_policy' in ha_dict: + adm_pol_dict = ha_dict['admission_control_policy'] + if not das_config.admissionControlPolicy or \ + not isinstance( + das_config.admissionControlPolicy, + vim.ClusterFailoverResourcesAdmissionControlPolicy): + + das_config.admissionControlPolicy = \ + vim.ClusterFailoverResourcesAdmissionControlPolicy( + cpuFailoverResourcesPercent= + adm_pol_dict['cpu_failover_percent'], + memoryFailoverResourcesPercent= + adm_pol_dict['memory_failover_percent']) + if 'default_vm_settings' in ha_dict: + vm_set_dict = ha_dict['default_vm_settings'] + if not das_config.defaultVmSettings: + das_config.defaultVmSettings = vim.ClusterDasVmSettings() + if 'isolation_response' in vm_set_dict: + das_config.defaultVmSettings.isolationResponse = \ + vm_set_dict['isolation_response'] + if 'restart_priority' in vm_set_dict: + das_config.defaultVmSettings.restartPriority = \ + vm_set_dict['restart_priority'] + if 'hb_ds_candidate_policy' in ha_dict: + das_config.hBDatastoreCandidatePolicy = \ + ha_dict['hb_ds_candidate_policy'] + if 'host_monitoring' in ha_dict: + das_config.hostMonitoring = ha_dict['host_monitoring'] + if 'options' in ha_dict: + das_config.option = [] + for opt_dict in ha_dict['options']: + das_config.option.append( + vim.OptionValue(key=opt_dict['key'])) + if 'value' in opt_dict: + das_config.option[-1].value = opt_dict['value'] + if 'vm_monitoring' in ha_dict: + das_config.vmMonitoring = ha_dict['vm_monitoring'] + cluster_spec.dasConfig = das_config + if cluster_dict.get('drs'): + drs_dict = cluster_dict['drs'] + drs_config = vim.ClusterDrsConfigInfo() + if 'enabled' in drs_dict: + drs_config.enabled = drs_dict['enabled'] + if 'vmotion_rate' in drs_dict: + drs_config.vmotionRate = 6 - drs_dict['vmotion_rate'] + if 'default_vm_behavior' in drs_dict: + drs_config.defaultVmBehavior = \ + vim.DrsBehavior(drs_dict['default_vm_behavior']) + cluster_spec.drsConfig = drs_config + if cluster_dict.get('vm_swap_placement'): + cluster_spec.vmSwapPlacement = cluster_dict['vm_swap_placement'] + if cluster_dict.get('vsan'): + vsan_dict = cluster_dict['vsan'] + if not vsan_61: # VSAN is 6.2 and above + if 'enabled' in vsan_dict: + if not vsan_spec.vsanClusterConfig: + vsan_spec.vsanClusterConfig = \ + vim.vsan.cluster.ConfigInfo() + vsan_spec.vsanClusterConfig.enabled = vsan_dict['enabled'] + if 'auto_claim_storage' in vsan_dict: + if not vsan_spec.vsanClusterConfig: + vsan_spec.vsanClusterConfig = \ + vim.vsan.cluster.ConfigInfo() + if not vsan_spec.vsanClusterConfig.defaultConfig: + vsan_spec.vsanClusterConfig.defaultConfig = \ + vim.VsanClusterConfigInfoHostDefaultInfo() + elif vsan_spec.vsanClusterConfig.defaultConfig.uuid: + # If this remains set it caused an error + vsan_spec.vsanClusterConfig.defaultConfig.uuid = None + vsan_spec.vsanClusterConfig.defaultConfig.autoClaimStorage = \ + vsan_dict['auto_claim_storage'] + if 'compression_enabled' in vsan_dict: + if not vsan_spec.dataEfficiencyConfig: + vsan_spec.dataEfficiencyConfig = \ + vim.vsan.DataEfficiencyConfig() + vsan_spec.dataEfficiencyConfig.compressionEnabled = \ + vsan_dict['compression_enabled'] + if 'dedup_enabled' in vsan_dict: + if not vsan_spec.dataEfficiencyConfig: + vsan_spec.dataEfficiencyConfig = \ + vim.vsan.DataEfficiencyConfig() + vsan_spec.dataEfficiencyConfig.dedupEnabled = \ + vsan_dict['dedup_enabled'] + # In all cases we need to configure the vsan on the cluster + # directly so not to have a missmatch between vsan_spec and + # cluster_spec + if not cluster_spec.vsanConfig: + cluster_spec.vsanConfig = \ + vim.VsanClusterConfigInfo() + vsan_config = cluster_spec.vsanConfig + if 'enabled' in vsan_dict: + vsan_config.enabled = vsan_dict['enabled'] + if 'auto_claim_storage' in vsan_dict: + if not vsan_config.defaultConfig: + vsan_config.defaultConfig = \ + vim.VsanClusterConfigInfoHostDefaultInfo() + elif vsan_config.defaultConfig.uuid: + # If this remains set it caused an error + vsan_config.defaultConfig.uuid = None + vsan_config.defaultConfig.autoClaimStorage = \ + vsan_dict['auto_claim_storage'] + log.trace('cluster_spec = {0}'.format(cluster_spec)) + + +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def create_cluster(cluster_dict, datacenter=None, cluster=None, + service_instance=None): + ''' + Creates a cluster. + + Note: cluster_dict['name'] will be overridden by the cluster param value + + config_dict + Dictionary with the config values of the new cluster. + + datacenter + Name of datacenter containing the cluster. + Ignored if already contained by proxy details. + Default value is None. + + cluster + Name of cluster. + Ignored if already contained by proxy details. + Default value is None. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + + # esxdatacenter proxy + salt '*' vsphere.create_cluster cluster_dict=$cluster_dict cluster=cl1 + + # esxcluster proxy + salt '*' vsphere.create_cluster cluster_dict=$cluster_dict + ''' + # Validate cluster dictionary + schema = ESXClusterConfigSchema.serialize() + try: + jsonschema.validate(cluster_dict, schema) + except jsonschema.exceptions.ValidationError as exc: + raise InvalidConfigError(exc) + # Get required details from the proxy + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + if not cluster: + raise ArgumentValueError('\'cluster\' needs to be specified') + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + cluster = __salt__['esxcluster.get_details']()['cluster'] + + if cluster_dict.get('vsan') and not \ + salt.utils.vsan.vsan_supported(service_instance): + + raise VMwareApiError('VSAN operations are not supported') + si = service_instance + cluster_spec = vim.ClusterConfigSpecEx() + vsan_spec = None + ha_config = None + vsan_61 = None + if cluster_dict.get('vsan'): + # XXX The correct way of retrieving the VSAN data (on the if branch) + # is not supported before 60u2 vcenter + vcenter_info = salt.utils.vmware.get_service_info(si) + if float(vcenter_info.apiVersion) >= 6.0 and \ + int(vcenter_info.build) >= 3634794: # 60u2 + vsan_spec = vim.vsan.ReconfigSpec(modify=True) + vsan_61 = False + # We need to keep HA disabled and enable it afterwards + if cluster_dict.get('ha', {}).get('enabled'): + enable_ha = True + ha_config = cluster_dict['ha'] + del cluster_dict['ha'] + else: + vsan_61 = True + # If VSAN is 6.1 the configuration of VSAN happens when configuring the + # cluster via the regular endpoint + _apply_cluster_dict(cluster_spec, cluster_dict, vsan_spec, vsan_61) + salt.utils.vmware.create_cluster(dc_ref, cluster, cluster_spec) + if not vsan_61: + # Only available after VSAN 61 + if vsan_spec: + cluster_ref = salt.utils.vmware.get_cluster(dc_ref, cluster) + salt.utils.vsan.reconfigure_cluster_vsan(cluster_ref, vsan_spec) + if enable_ha: + # Set HA after VSAN has been configured + _apply_cluster_dict(cluster_spec, {'ha': ha_config}) + salt.utils.vmware.update_cluster(cluster_ref, cluster_spec) + # Set HA back on the object + cluster_dict['ha'] = ha_config + return {'create_cluster': True} + + +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def update_cluster(cluster_dict, datacenter=None, cluster=None, + service_instance=None): + ''' + Updates a cluster. + + config_dict + Dictionary with the config values of the new cluster. + + datacenter + Name of datacenter containing the cluster. + Ignored if already contained by proxy details. + Default value is None. + + cluster + Name of cluster. + Ignored if already contained by proxy details. + Default value is None. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + + # esxdatacenter proxy + salt '*' vsphere.update_cluster cluster_dict=$cluster_dict cluster=cl1 + + # esxcluster proxy + salt '*' vsphere.update_cluster cluster_dict=$cluster_dict + + ''' + # Validate cluster dictionary + schema = ESXClusterConfigSchema.serialize() + try: + jsonschema.validate(cluster_dict, schema) + except jsonschema.exceptions.ValidationError as exc: + raise InvalidConfigError(exc) + # Get required details from the proxy + proxy_type = get_proxy_type() + if proxy_type == 'esxdatacenter': + datacenter = __salt__['esxdatacenter.get_details']()['datacenter'] + dc_ref = _get_proxy_target(service_instance) + if not cluster: + raise ArgumentValueError('\'cluster\' needs to be specified') + elif proxy_type == 'esxcluster': + datacenter = __salt__['esxcluster.get_details']()['datacenter'] + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + cluster = __salt__['esxcluster.get_details']()['cluster'] + + if cluster_dict.get('vsan') and not \ + salt.utils.vsan.vsan_supported(service_instance): + + raise VMwareApiError('VSAN operations are not supported') + + cluster_ref = salt.utils.vmware.get_cluster(dc_ref, cluster) + cluster_spec = vim.ClusterConfigSpecEx() + props = salt.utils.vmware.get_properties_of_managed_object( + cluster_ref, properties=['configurationEx']) + # Copy elements we want to update to spec + for p in ['dasConfig', 'drsConfig']: + setattr(cluster_spec, p, getattr(props['configurationEx'], p)) + if props['configurationEx'].vsanConfigInfo: + cluster_spec.vsanConfig = props['configurationEx'].vsanConfigInfo + vsan_spec = None + vsan_61 = None + if cluster_dict.get('vsan'): + # XXX The correct way of retrieving the VSAN data (on the if branch) + # is not supported before 60u2 vcenter + vcenter_info = salt.utils.vmware.get_service_info(service_instance) + if float(vcenter_info.apiVersion) >= 6.0 and \ + int(vcenter_info.build) >= 3634794: # 60u2 + vsan_61 = False + vsan_info = salt.utils.vsan.get_cluster_vsan_info(cluster_ref) + vsan_spec = vim.vsan.ReconfigSpec(modify=True) + # Only interested in the vsanClusterConfig and the + # dataEfficiencyConfig + # vsan_spec.vsanClusterConfig = vsan_info + vsan_spec.dataEfficiencyConfig = vsan_info.dataEfficiencyConfig + vsan_info.dataEfficiencyConfig = None + else: + vsan_61 = True + + _apply_cluster_dict(cluster_spec, cluster_dict, vsan_spec, vsan_61) + # We try to reconfigure vsan first as it fails if HA is enabled so the + # command will abort not having any side-effects + # also if HA was previously disabled it can be enabled automatically if + # desired + if vsan_spec: + log.trace('vsan_spec = {0}'.format(vsan_spec)) + salt.utils.vsan.reconfigure_cluster_vsan(cluster_ref, vsan_spec) + + # We need to retrieve again the properties and reapply them + # As the VSAN configuration has changed + cluster_spec = vim.ClusterConfigSpecEx() + props = salt.utils.vmware.get_properties_of_managed_object( + cluster_ref, properties=['configurationEx']) + # Copy elements we want to update to spec + for p in ['dasConfig', 'drsConfig']: + setattr(cluster_spec, p, getattr(props['configurationEx'], p)) + if props['configurationEx'].vsanConfigInfo: + cluster_spec.vsanConfig = props['configurationEx'].vsanConfigInfo + # We only need to configure the cluster_spec, as if it were a vsan_61 + # cluster + _apply_cluster_dict(cluster_spec, cluster_dict) + salt.utils.vmware.update_cluster(cluster_ref, cluster_spec) + return {'update_cluster': True} + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def list_datastores_via_proxy(datastore_names=None, backing_disk_ids=None, + backing_disk_scsi_addresses=None, + service_instance=None): + ''' + Returns a list of dict representations of the datastores visible to the + proxy object. The list of datastores can be filtered by datastore names, + backing disk ids (canonical names) or backing disk scsi addresses. + + Supported proxy types: esxi, esxcluster, esxdatacenter + + datastore_names + List of the names of datastores to filter on + + backing_disk_ids + List of canonical names of the backing disks of the datastores to filer. + Default is None. + + backing_disk_scsi_addresses + List of scsi addresses of the backing disks of the datastores to filter. + Default is None. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.list_datastores_via_proxy + + salt '*' vsphere.list_datastores_via_proxy datastore_names=[ds1, ds2] + ''' + target = _get_proxy_target(service_instance) + target_name = salt.utils.vmware.get_managed_object_name(target) + log.trace('target name = {}'.format(target_name)) + + # Default to getting all disks if no filtering is done + get_all_datastores = True if \ + not (datastore_names or backing_disk_ids or + backing_disk_scsi_addresses) else False + # Get the ids of the disks with the scsi addresses + if backing_disk_scsi_addresses: + log.debug('Retrieving disk ids for scsi addresses ' + '\'{0}\''.format(backing_disk_scsi_addresses)) + disk_ids = [d.canonicalName for d in + salt.utils.vmware.get_disks( + target, scsi_addresses=backing_disk_scsi_addresses)] + log.debug('Found disk ids \'{}\''.format(disk_ids)) + backing_disk_ids = backing_disk_ids.extend(disk_ids) if \ + backing_disk_ids else disk_ids + datastores = salt.utils.vmware.get_datastores(service_instance, + target, + datastore_names, + backing_disk_ids, + get_all_datastores) + + # Search for disk backed datastores if target is host + # to be able to add the backing_disk_ids + mount_infos = [] + if isinstance(target, vim.HostSystem): + storage_system = salt.utils.vmware.get_storage_system( + service_instance, target, target_name) + props = salt.utils.vmware.get_properties_of_managed_object( + storage_system, ['fileSystemVolumeInfo.mountInfo']) + mount_infos = props.get('fileSystemVolumeInfo.mountInfo', []) + ret_dict = [] + for ds in datastores: + ds_dict = {'name': ds.name, + 'type': ds.summary.type, + 'free_space': ds.summary.freeSpace, + 'capacity': ds.summary.capacity} + backing_disk_ids = [] + for vol in [i.volume for i in mount_infos if + i.volume.name == ds.name and + isinstance(i.volume, vim.HostVmfsVolume)]: + + backing_disk_ids.extend([e.diskName for e in vol.extent]) + if backing_disk_ids: + ds_dict['backing_disk_ids'] = backing_disk_ids + ret_dict.append(ds_dict) + return ret_dict + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def rename_datastore(datastore_name, new_datastore_name, + service_instance=None): + ''' + Renames a datastore. The datastore needs to be visible to the proxy. + + datastore_name + Current datastore name. + + new_datastore_name + New datastore name. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.rename_datastore old_name new_name + ''' + # Argument validation + log.trace('Renaming datastore {0} to {1}' + ''.format(datastore_name, new_datastore_name)) + target = _get_proxy_target(service_instance) + datastores = salt.utils.vmware.get_datastores( + service_instance, + target, + datastore_names=[datastore_name]) + if not datastores: + raise VMwareObjectRetrievalError('Datastore \'{0}\' was not found' + ''.format(datastore_name)) + ds = datastores[0] + salt.utils.vmware.rename_datastore(ds, new_datastore_name) + return True + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def list_licenses(service_instance=None): + ''' + Lists all licenses on a vCenter. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.list_licenses + ''' + log.trace('Retrieving all licenses') + licenses = salt.utils.vmware.get_licenses(service_instance) + ret_dict = [{'key': l.licenseKey, + 'name': l.name, + 'description': l.labels[0].value if l.labels else None, + # VMware handles unlimited capacity as 0 + 'capacity': l.total if l.total > 0 else sys.maxsize, + 'used': l.used if l.used else 0} + for l in licenses] + return ret_dict + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def add_license(key, description, safety_checks=True, + service_instance=None): + ''' + Adds a license to the vCenter or ESXi host + + key + License key. + + description + License description added in as a label. + + safety_checks + Specify whether to perform safety check or to skip the checks and try + performing the required task + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.add_license key= desc='License desc' + ''' + log.trace('Adding license \'{0}\''.format(key)) + salt.utils.vmware.add_license(service_instance, key, description) + return True + + +def _get_entity(service_instance, entity): + ''' + Returns the entity associated with the entity dict representation + + Supported entities: cluster, vcenter + + Expected entity format: + + .. code-block:: python + + cluster: + {'type': 'cluster', + 'datacenter': , + 'cluster': } + vcenter: + {'type': 'vcenter'} + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + + entity + Entity dict in the format above + ''' + + log.trace('Retrieving entity: {0}'.format(entity)) + if entity['type'] == 'cluster': + dc_ref = salt.utils.vmware.get_datacenter(service_instance, + entity['datacenter']) + return salt.utils.vmware.get_cluster(dc_ref, entity['cluster']) + elif entity['type'] == 'vcenter': + return None + raise ArgumentValueError('Unsupported entity type \'{0}\'' + ''.format(entity['type'])) + + +def _validate_entity(entity): + ''' + Validates the entity dict representation + + entity + Dictionary representation of an entity. + See ``_get_entity`` docstrings for format. + ''' + + #Validate entity: + if entity['type'] == 'cluster': + schema = ESXClusterEntitySchema.serialize() + elif entity['type'] == 'vcenter': + schema = VCenterEntitySchema.serialize() + else: + raise ArgumentValueError('Unsupported entity type \'{0}\'' + ''.format(entity['type'])) + try: + jsonschema.validate(entity, schema) + except jsonschema.exceptions.ValidationError as exc: + raise InvalidEntityError(exc) + + +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def list_assigned_licenses(entity, entity_display_name, license_keys=None, + service_instance=None): + ''' + Lists the licenses assigned to an entity + + entity + Dictionary representation of an entity. + See ``_get_entity`` docstrings for format. + + entity_display_name + Entity name used in logging + + license_keys: + List of license keys to be retrieved. Default is None. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.list_assigned_licenses + entity={type:cluster,datacenter:dc,cluster:cl} + entiy_display_name=cl + ''' + log.trace('Listing assigned licenses of entity {0}' + ''.format(entity)) + _validate_entity(entity) + + assigned_licenses = salt.utils.vmware.get_assigned_licenses( + service_instance, + entity_ref=_get_entity(service_instance, entity), + entity_name=entity_display_name) + + return [{'key': l.licenseKey, + 'name': l.name, + 'description': l.labels[0].value if l.labels else None, + # VMware handles unlimited capacity as 0 + 'capacity': l.total if l.total > 0 else sys.maxsize} + for l in assigned_licenses if (license_keys is None) or + (l.licenseKey in license_keys)] + + +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def assign_license(license_key, license_name, entity, entity_display_name, + safety_checks=True, service_instance=None): + ''' + Assigns a license to an entity + + license_key + Key of the license to assign + See ``_get_entity`` docstrings for format. + + license_name + Display name of license + + entity + Dictionary representation of an entity + + entity_display_name + Entity name used in logging + + safety_checks + Specify whether to perform safety check or to skip the checks and try + performing the required task. Default is False. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.assign_license license_key=00000:00000 + license name=test entity={type:cluster,datacenter:dc,cluster:cl} + ''' + log.trace('Assigning license {0} to entity {1}' + ''.format(license_key, entity)) + _validate_entity(entity) + if safety_checks: + licenses = salt.utils.vmware.get_licenses(service_instance) + if not [l for l in licenses if l.licenseKey == license_key]: + raise VMwareObjectRetrievalError('License \'{0}\' wasn\'t found' + ''.format(license_name)) + salt.utils.vmware.assign_license( + service_instance, + license_key, + license_name, + entity_ref=_get_entity(service_instance, entity), + entity_name=entity_display_name) + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or @@ -4286,6 +5115,39 @@ def add_host_to_dvs(host, username, password, vmknic_name, vmnic_name, return ret +@depends(HAS_PYVMOMI) +@supports_proxies('esxcluster', 'esxdatacenter') +def _get_proxy_target(service_instance): + ''' + Returns the target object of a proxy. + + If the object doesn't exist a VMwareObjectRetrievalError is raised + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + ''' + proxy_type = get_proxy_type() + if not salt.utils.vmware.is_connection_to_a_vcenter(service_instance): + raise CommandExecutionError('\'_get_proxy_target\' not supported ' + 'when connected via the ESXi host') + reference = None + if proxy_type == 'esxcluster': + host, username, password, protocol, port, mechanism, principal, \ + domain, datacenter, cluster = _get_esxcluster_proxy_details() + + dc_ref = salt.utils.vmware.get_datacenter(service_instance, datacenter) + reference = salt.utils.vmware.get_cluster(dc_ref, cluster) + elif proxy_type == 'esxdatacenter': + # esxdatacenter proxy + host, username, password, protocol, port, mechanism, principal, \ + domain, datacenter = _get_esxdatacenter_proxy_details() + + reference = salt.utils.vmware.get_datacenter(service_instance, + datacenter) + log.trace('reference = {0}'.format(reference)) + return reference + + def _get_esxdatacenter_proxy_details(): ''' Returns the running esxdatacenter's proxy details @@ -4294,3 +5156,14 @@ def _get_esxdatacenter_proxy_details(): return det.get('vcenter'), det.get('username'), det.get('password'), \ det.get('protocol'), det.get('port'), det.get('mechanism'), \ det.get('principal'), det.get('domain'), det.get('datacenter') + + +def _get_esxcluster_proxy_details(): + ''' + Returns the running esxcluster's proxy details + ''' + det = __salt__['esxcluster.get_details']() + return det.get('vcenter'), det.get('username'), det.get('password'), \ + det.get('protocol'), det.get('port'), det.get('mechanism'), \ + det.get('principal'), det.get('domain'), det.get('datacenter'), \ + det.get('cluster') diff --git a/salt/modules/win_iis.py b/salt/modules/win_iis.py index f206c91116..e171827316 100644 --- a/salt/modules/win_iis.py +++ b/salt/modules/win_iis.py @@ -837,6 +837,9 @@ def create_cert_binding(name, site, hostheader='', ipaddress='*', port=443, # IIS 7.5 and earlier have different syntax for associating a certificate with a site # Modify IP spec to IIS 7.5 format iis7path = binding_path.replace(r"\*!", "\\0.0.0.0!") + # win 2008 uses the following format: ip!port and not ip!port! + if iis7path.endswith("!"): + iis7path = iis7path[:-1] ps_cmd = ['New-Item', '-Path', "'{0}'".format(iis7path), @@ -1255,6 +1258,9 @@ def set_container_setting(name, container, settings): salt '*' win_iis.set_container_setting name='MyTestPool' container='AppPools' settings="{'managedPipeLineMode': 'Integrated'}" ''' + + identityType_map2string = {'0': 'LocalSystem', '1': 'LocalService', '2': 'NetworkService', '3': 'SpecificUser', '4': 'ApplicationPoolIdentity'} + identityType_map2numeric = {'LocalSystem': '0', 'LocalService': '1', 'NetworkService': '2', 'SpecificUser': '3', 'ApplicationPoolIdentity': '4'} ps_cmd = list() container_path = r"IIS:\{0}\{1}".format(container, name) @@ -1281,6 +1287,10 @@ def set_container_setting(name, container, settings): except ValueError: value = "'{0}'".format(settings[setting]) + # Map to numeric to support server 2008 + if setting == 'processModel.identityType' and settings[setting] in identityType_map2numeric.keys(): + value = identityType_map2numeric[settings[setting]] + ps_cmd.extend(['Set-ItemProperty', '-Path', "'{0}'".format(container_path), '-Name', "'{0}'".format(setting), @@ -1300,6 +1310,10 @@ def set_container_setting(name, container, settings): failed_settings = dict() for setting in settings: + # map identity type from numeric to string for comparing + if setting == 'processModel.identityType' and settings[setting] in identityType_map2string.keys(): + settings[setting] = identityType_map2string[settings[setting]] + if str(settings[setting]) != str(new_settings[setting]): failed_settings[setting] = settings[setting] diff --git a/salt/modules/win_path.py b/salt/modules/win_path.py index 27bba5f719..fa57f53d18 100644 --- a/salt/modules/win_path.py +++ b/salt/modules/win_path.py @@ -51,7 +51,7 @@ def rehash(): CLI Example: - ... code-block:: bash + .. code-block:: bash salt '*' win_path.rehash ''' diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index 156681f8b6..bf167934a2 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -977,7 +977,7 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): # Version is ignored salt '*' pkg.install pkgs="['foo', 'bar']" version=1.2.3 - If passed with a comma seperated list in the ``name`` parameter, the + If passed with a comma separated list in the ``name`` parameter, the version will apply to all packages in the list. CLI Example: @@ -987,18 +987,6 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): # Version 1.2.3 will apply to packages foo and bar salt '*' pkg.install foo,bar version=1.2.3 - cache_file (str): - A single file to copy down for use with the installer. Copied to the - same location as the installer. Use this over ``cache_dir`` if there - are many files in the directory and you only need a specific file - and don't want to cache additional files that may reside in the - installer directory. Only applies to files on ``salt://`` - - cache_dir (bool): - True will copy the contents of the installer directory. This is - useful for installations that are not a single file. Only applies to - directories on ``salt://`` - extra_install_flags (str): Additional install flags that will be appended to the ``install_flags`` defined in the software definition file. Only @@ -1286,16 +1274,16 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): use_msiexec, msiexec = _get_msiexec(pkginfo[version_num].get('msiexec', False)) # Build cmd and arguments - # cmd and arguments must be seperated for use with the task scheduler + # cmd and arguments must be separated for use with the task scheduler if use_msiexec: cmd = msiexec arguments = ['/i', cached_pkg] - if pkginfo['version_num'].get('allusers', True): + if pkginfo[version_num].get('allusers', True): arguments.append('ALLUSERS="1"') - arguments.extend(salt.utils.shlex_split(install_flags)) + arguments.extend(salt.utils.shlex_split(install_flags, posix=False)) else: cmd = cached_pkg - arguments = salt.utils.shlex_split(install_flags) + arguments = salt.utils.shlex_split(install_flags, posix=False) # Install the software # Check Use Scheduler Option @@ -1328,7 +1316,9 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): # Run Scheduled Task # Special handling for installing salt - if pkg_name in ['salt-minion', 'salt-minion-py3']: + if re.search(r'salt[\s_.-]*minion', + pkg_name, + flags=re.IGNORECASE + re.UNICODE) is not None: ret[pkg_name] = {'install status': 'task started'} if not __salt__['task.run'](name='update-salt-software'): log.error('Failed to install {0}'.format(pkg_name)) @@ -1360,12 +1350,12 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): else: # Combine cmd and arguments - cmd = [cmd].extend(arguments) + cmd = [cmd] + cmd.extend(arguments) # Launch the command result = __salt__['cmd.run_all'](cmd, cache_path, - output_loglevel='quiet', python_shell=False, redirect_stderr=True) if not result['retcode']: @@ -1624,19 +1614,19 @@ def remove(name=None, pkgs=None, version=None, **kwargs): #Compute msiexec string use_msiexec, msiexec = _get_msiexec(pkginfo[target].get('msiexec', False)) + # Build cmd and arguments + # cmd and arguments must be separated for use with the task scheduler + if use_msiexec: + cmd = msiexec + arguments = ['/x'] + arguments.extend(salt.utils.shlex_split(uninstall_flags, posix=False)) + else: + cmd = expanded_cached_pkg + arguments = salt.utils.shlex_split(uninstall_flags, posix=False) + # Uninstall the software # Check Use Scheduler Option if pkginfo[target].get('use_scheduler', False): - - # Build Scheduled Task Parameters - if use_msiexec: - cmd = msiexec - arguments = ['/x'] - arguments.extend(salt.utils.args.shlex_split(uninstall_flags)) - else: - cmd = expanded_cached_pkg - arguments = salt.utils.args.shlex_split(uninstall_flags) - # Create Scheduled Task __salt__['task.create_task'](name='update-salt-software', user_name='System', @@ -1657,16 +1647,12 @@ def remove(name=None, pkgs=None, version=None, **kwargs): ret[pkgname] = {'uninstall status': 'failed'} else: # Build the install command - cmd = [] - if use_msiexec: - cmd.extend([msiexec, '/x', expanded_cached_pkg]) - else: - cmd.append(expanded_cached_pkg) - cmd.extend(salt.utils.args.shlex_split(uninstall_flags)) + cmd = [cmd] + cmd.extend(arguments) + # Launch the command result = __salt__['cmd.run_all']( cmd, - output_loglevel='trace', python_shell=False, redirect_stderr=True) if not result['retcode']: diff --git a/salt/modules/win_pki.py b/salt/modules/win_pki.py index ef277f2baf..329da531f0 100644 --- a/salt/modules/win_pki.py +++ b/salt/modules/win_pki.py @@ -170,7 +170,11 @@ def get_certs(context=_DEFAULT_CONTEXT, store=_DEFAULT_STORE): if key not in blacklist_keys: cert_info[key.lower()] = item[key] - cert_info['dnsnames'] = [name.get('Unicode') for name in item.get('DnsNameList', {})] + names = item.get('DnsNameList', None) + if isinstance(names, list): + cert_info['dnsnames'] = [name.get('Unicode') for name in names] + else: + cert_info['dnsnames'] = [] ret[item['Thumbprint']] = cert_info return ret diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index ad0f4b6b53..849c5c4f29 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -61,7 +61,7 @@ from salt.ext import six log = logging.getLogger(__name__) -__HOLD_PATTERN = r'\w+(?:[.-][^-]+)*' +__HOLD_PATTERN = r'[\w+]+(?:[.-][^-]+)*' # Define the module's virtual name __virtualname__ = 'pkg' @@ -616,7 +616,8 @@ def list_pkgs(versions_as_list=False, **kwargs): {'': [{'version' : 'version', 'arch' : 'arch'}]} - Valid attributes are: ``version``, ``arch``, ``install_date``, ``install_date_time_t``. + Valid attributes are: ``epoch``, ``version``, ``release``, ``arch``, + ``install_date``, ``install_date_time_t``. If ``all`` is specified, all valid attributes will be returned. @@ -652,7 +653,16 @@ def list_pkgs(versions_as_list=False, **kwargs): osarch=__grains__['osarch'] ) if pkginfo is not None: - all_attr = {'version': pkginfo.version, 'arch': pkginfo.arch, 'install_date': pkginfo.install_date, + # see rpm version string rules available at https://goo.gl/UGKPNd + pkgver = pkginfo.version + epoch = '' + release = '' + if ':' in pkgver: + epoch, pkgver = pkgver.split(":", 1) + if '-' in pkgver: + pkgver, release = pkgver.split("-", 1) + all_attr = {'epoch': epoch, 'version': pkgver, 'release': release, + 'arch': pkginfo.arch, 'install_date': pkginfo.install_date, 'install_date_time_t': pkginfo.install_date_time_t} __salt__['pkg_resource.add_pkg'](ret, pkginfo.name, all_attr) @@ -879,8 +889,8 @@ def list_repo_pkgs(*args, **kwargs): _parse_output(out['stdout'], strict=True) else: for repo in repos: - cmd = [_yum(), '--quiet', 'repository-packages', repo, - 'list', '--showduplicates'] + cmd = [_yum(), '--quiet', '--showduplicates', + 'repository-packages', repo, 'list'] if cacheonly: cmd.append('-C') # Can't concatenate because args is a tuple, using list.extend() @@ -1284,7 +1294,8 @@ def install(name=None, 'version': '', 'arch': ''}}} - Valid attributes are: ``version``, ``arch``, ``install_date``, ``install_date_time_t``. + Valid attributes are: ``epoch``, ``version``, ``release``, ``arch``, + ``install_date``, ``install_date_time_t``. If ``all`` is specified, all valid attributes will be returned. @@ -2829,7 +2840,7 @@ def _parse_repo_file(filename): for section in parsed._sections: section_dict = dict(parsed._sections[section]) - section_dict.pop('__name__') + section_dict.pop('__name__', None) config[section] = section_dict # Try to extract leading comments diff --git a/salt/modules/zk_concurrency.py b/salt/modules/zk_concurrency.py index 4335a176d8..2dc0a8dbf5 100644 --- a/salt/modules/zk_concurrency.py +++ b/salt/modules/zk_concurrency.py @@ -185,7 +185,7 @@ def lock_holders(path, Example: - ... code-block: bash + .. code-block: bash salt minion zk_concurrency.lock_holders /lock/path host1:1234,host2:1234 ''' @@ -237,7 +237,7 @@ def lock(path, Example: - ... code-block: bash + .. code-block: bash salt minion zk_concurrency.lock /lock/path host1:1234,host2:1234 ''' @@ -298,7 +298,7 @@ def unlock(path, Example: - ... code-block: bash + .. code-block: bash salt minion zk_concurrency.unlock /lock/path host1:1234,host2:1234 ''' @@ -348,7 +348,7 @@ def party_members(path, Example: - ... code-block: bash + .. code-block: bash salt minion zk_concurrency.party_members /lock/path host1:1234,host2:1234 salt minion zk_concurrency.party_members /lock/path host1:1234,host2:1234 min_nodes=3 blocking=True diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py index 60b920d929..66658ba1f6 100644 --- a/salt/modules/zypper.py +++ b/salt/modules/zypper.py @@ -666,7 +666,8 @@ def list_pkgs(versions_as_list=False, **kwargs): {'': [{'version' : 'version', 'arch' : 'arch'}]} - Valid attributes are: ``version``, ``arch``, ``install_date``, ``install_date_time_t``. + Valid attributes are: ``epoch``, ``version``, ``release``, ``arch``, + ``install_date``, ``install_date_time_t``. If ``all`` is specified, all valid attributes will be returned. @@ -702,15 +703,11 @@ def list_pkgs(versions_as_list=False, **kwargs): ret = {} for line in __salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=False).splitlines(): name, pkgver, rel, arch, epoch, install_time = line.split('_|-') - if epoch: - pkgver = '{0}:{1}'.format(epoch, pkgver) - if rel: - pkgver += '-{0}'.format(rel) install_date = datetime.datetime.utcfromtimestamp(int(install_time)).isoformat() + "Z" install_date_time_t = int(install_time) - all_attr = {'version': pkgver, 'arch': arch, 'install_date': install_date, - 'install_date_time_t': install_date_time_t} + all_attr = {'epoch': epoch, 'version': pkgver, 'release': rel, 'arch': arch, + 'install_date': install_date, 'install_date_time_t': install_date_time_t} __salt__['pkg_resource.add_pkg'](ret, name, all_attr) for pkgname in ret: @@ -1097,7 +1094,8 @@ def install(name=None, 'version': '', 'arch': ''}}} - Valid attributes are: ``version``, ``arch``, ``install_date``, ``install_date_time_t``. + Valid attributes are: ``epoch``, ``version``, ``release``, ``arch``, + ``install_date``, ``install_date_time_t``. If ``all`` is specified, all valid attributes will be returned. diff --git a/salt/netapi/rest_cherrypy/__init__.py b/salt/netapi/rest_cherrypy/__init__.py index 6285de289c..d974cda6c1 100644 --- a/salt/netapi/rest_cherrypy/__init__.py +++ b/salt/netapi/rest_cherrypy/__init__.py @@ -20,7 +20,7 @@ try: except ImportError as exc: cpy_error = exc -__virtualname__ = os.path.abspath(__file__).rsplit('/')[-2] or 'rest_cherrypy' +__virtualname__ = os.path.abspath(__file__).rsplit(os.sep)[-2] or 'rest_cherrypy' logger = logging.getLogger(__virtualname__) cpy_min = '3.2.2' diff --git a/salt/netapi/rest_tornado/__init__.py b/salt/netapi/rest_tornado/__init__.py index fc547a02a3..2437714073 100644 --- a/salt/netapi/rest_tornado/__init__.py +++ b/salt/netapi/rest_tornado/__init__.py @@ -10,7 +10,7 @@ import os import salt.auth from salt.utils.versions import StrictVersion as _StrictVersion -__virtualname__ = os.path.abspath(__file__).rsplit('/')[-2] or 'rest_tornado' +__virtualname__ = os.path.abspath(__file__).rsplit(os.sep)[-2] or 'rest_tornado' logger = logging.getLogger(__virtualname__) diff --git a/salt/output/highstate.py b/salt/output/highstate.py index 2f30d6c8f9..c003cfc32c 100644 --- a/salt/output/highstate.py +++ b/salt/output/highstate.py @@ -108,7 +108,7 @@ import pprint import textwrap # Import salt libs -import salt.utils +import salt.utils.color import salt.utils.stringutils import salt.output from salt.utils.locales import sdecode @@ -158,7 +158,7 @@ def output(data, **kwargs): # pylint: disable=unused-argument def _format_host(host, data): host = sdecode(host) - colors = salt.utils.get_colors( + colors = salt.utils.color.get_colors( __opts__.get('color'), __opts__.get('color_theme')) tabular = __opts__.get('state_tabular', False) @@ -242,8 +242,15 @@ def _format_host(host, data): if ret['result'] is None: hcolor = colors['LIGHT_YELLOW'] tcolor = colors['LIGHT_YELLOW'] + + state_output = __opts__.get('state_output', 'full').lower() comps = [sdecode(comp) for comp in tname.split('_|-')] - if __opts__.get('state_output', 'full').lower() == 'filter': + + if state_output == 'mixed_id': + # Swap in the ID for the name. Refs #35137 + comps[2] = comps[1] + + if state_output.startswith('filter'): # By default, full data is shown for all types. However, return # data may be excluded by setting state_output_exclude to a # comma-separated list of True, False or None, or including the @@ -276,28 +283,17 @@ def _format_host(host, data): continue if str(ret['result']) in exclude: continue - elif __opts__.get('state_output', 'full').lower() == 'terse': - # Print this chunk in a terse way and continue in the - # loop + + elif any(( + state_output.startswith('terse'), + state_output.startswith('mixed') and ret['result'] is not False, # only non-error'd + state_output.startswith('changes') and ret['result'] and not schanged # non-error'd non-changed + )): + # Print this chunk in a terse way and continue in the loop msg = _format_terse(tcolor, comps, ret, colors, tabular) hstrs.append(msg) continue - elif __opts__.get('state_output', 'full').lower().startswith('mixed'): - if __opts__['state_output'] == 'mixed_id': - # Swap in the ID for the name. Refs #35137 - comps[2] = comps[1] - # Print terse unless it failed - if ret['result'] is not False: - msg = _format_terse(tcolor, comps, ret, colors, tabular) - hstrs.append(msg) - continue - elif __opts__.get('state_output', 'full').lower() == 'changes': - # Print terse if no error and no changes, otherwise, be - # verbose - if ret['result'] and not schanged: - msg = _format_terse(tcolor, comps, ret, colors, tabular) - hstrs.append(msg) - continue + state_lines = [ u'{tcolor}----------{colors[ENDC]}', u' {tcolor} ID: {comps[1]}{colors[ENDC]}', diff --git a/salt/output/key.py b/salt/output/key.py index dfa42b2e06..afb504918d 100644 --- a/salt/output/key.py +++ b/salt/output/key.py @@ -8,9 +8,9 @@ The ``salt-key`` command makes use of this outputter to format its output. from __future__ import absolute_import # Import salt libs -import salt.utils import salt.output from salt.utils.locales import sdecode +import salt.utils.color def output(data, **kwargs): # pylint: disable=unused-argument @@ -18,7 +18,7 @@ def output(data, **kwargs): # pylint: disable=unused-argument Read in the dict structure generated by the salt key API methods and print the structure. ''' - color = salt.utils.get_colors( + color = salt.utils.color.get_colors( __opts__.get('color'), __opts__.get('color_theme')) strip_colors = __opts__.get('strip_colors', True) diff --git a/salt/output/nested.py b/salt/output/nested.py index 8ef0e4c046..563bf5768f 100644 --- a/salt/output/nested.py +++ b/salt/output/nested.py @@ -29,9 +29,9 @@ from numbers import Number # Import salt libs import salt.output +import salt.utils.color import salt.utils.locales import salt.utils.odict -from salt.utils import get_colors from salt.ext.six import string_types @@ -41,7 +41,7 @@ class NestDisplay(object): ''' def __init__(self): self.__dict__.update( - get_colors( + salt.utils.color.get_colors( __opts__.get('color'), __opts__.get('color_theme') ) diff --git a/salt/output/no_return.py b/salt/output/no_return.py index 781db538de..00f601bade 100644 --- a/salt/output/no_return.py +++ b/salt/output/no_return.py @@ -15,7 +15,7 @@ Example output:: from __future__ import absolute_import # Import salt libs -import salt.utils +import salt.utils.color # Import 3rd-party libs from salt.ext import six @@ -26,7 +26,7 @@ class NestDisplay(object): Create generator for nested output ''' def __init__(self): - self.colors = salt.utils.get_colors( + self.colors = salt.utils.color.get_colors( __opts__.get(u'color'), __opts__.get(u'color_theme')) diff --git a/salt/output/overstatestage.py b/salt/output/overstatestage.py index d35dd5a311..ee101e7620 100644 --- a/salt/output/overstatestage.py +++ b/salt/output/overstatestage.py @@ -11,7 +11,7 @@ and should not be called directly. from __future__ import absolute_import # Import Salt libs -import salt.utils +import salt.utils.color # Import 3rd-party libs from salt.ext import six @@ -27,7 +27,7 @@ def output(data, **kwargs): # pylint: disable=unused-argument ''' Format the data for printing stage information from the overstate system ''' - colors = salt.utils.get_colors( + colors = salt.utils.color.get_colors( __opts__.get('color'), __opts__.get('color_theme')) ostr = '' diff --git a/salt/output/table_out.py b/salt/output/table_out.py index 59c4cd9486..531eb96c0f 100644 --- a/salt/output/table_out.py +++ b/salt/output/table_out.py @@ -42,12 +42,10 @@ from functools import reduce # pylint: disable=redefined-builtin # Import salt libs import salt.output -import salt.utils.locales from salt.ext.six import string_types -from salt.utils import get_colors -from salt.ext.six.moves import map # pylint: disable=redefined-builtin -from salt.ext.six.moves import zip # pylint: disable=redefined-builtin - +from salt.ext.six.moves import map, zip # pylint: disable=redefined-builtin +import salt.utils.color +import salt.utils.locales __virtualname__ = 'table' @@ -78,7 +76,7 @@ class TableDisplay(object): width=50, # column max width wrapfunc=None): # function wrapper self.__dict__.update( - get_colors( + salt.utils.color.get_colors( __opts__.get('color'), __opts__.get('color_theme') ) diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py index f93f4eea98..8a0756251c 100644 --- a/salt/pillar/__init__.py +++ b/salt/pillar/__init__.py @@ -13,6 +13,7 @@ import logging import tornado.gen import sys import traceback +import inspect # Import salt libs import salt.loader @@ -23,6 +24,8 @@ import salt.transport import salt.utils.url import salt.utils.cache import salt.utils.crypt +import salt.utils.dictupdate +import salt.utils.args from salt.exceptions import SaltClientError from salt.template import compile_template from salt.utils.dictupdate import merge @@ -36,7 +39,7 @@ log = logging.getLogger(__name__) def get_pillar(opts, grains, minion_id, saltenv=None, ext=None, funcs=None, - pillar_override=None, pillarenv=None): + pillar_override=None, pillarenv=None, extra_minion_data=None): ''' Return the correct pillar driver based on the file_client option ''' @@ -55,12 +58,14 @@ def get_pillar(opts, grains, minion_id, saltenv=None, ext=None, funcs=None, return PillarCache(opts, grains, minion_id, saltenv, ext=ext, functions=funcs, pillar_override=pillar_override, pillarenv=pillarenv) return ptype(opts, grains, minion_id, saltenv, ext, functions=funcs, - pillar_override=pillar_override, pillarenv=pillarenv) + pillar_override=pillar_override, pillarenv=pillarenv, + extra_minion_data=extra_minion_data) # TODO: migrate everyone to this one! def get_async_pillar(opts, grains, minion_id, saltenv=None, ext=None, funcs=None, - pillar_override=None, pillarenv=None): + pillar_override=None, pillarenv=None, + extra_minion_data=None): ''' Return the correct pillar driver based on the file_client option ''' @@ -72,15 +77,62 @@ def get_async_pillar(opts, grains, minion_id, saltenv=None, ext=None, funcs=None 'local': AsyncPillar, }.get(file_client, AsyncPillar) return ptype(opts, grains, minion_id, saltenv, ext, functions=funcs, - pillar_override=pillar_override, pillarenv=pillarenv) + pillar_override=pillar_override, pillarenv=pillarenv, + extra_minion_data=extra_minion_data) -class AsyncRemotePillar(object): +class RemotePillarMixin(object): + ''' + Common remote pillar functionality + ''' + def get_ext_pillar_extra_minion_data(self, opts): + ''' + Returns the extra data from the minion's opts dict (the config file). + + This data will be passed to external pillar functions. + ''' + def get_subconfig(opts_key): + ''' + Returns a dict containing the opts key subtree, while maintaining + the opts structure + ''' + ret_dict = aux_dict = {} + config_val = opts + subkeys = opts_key.split(':') + # Build an empty dict with the opts path + for subkey in subkeys[:-1]: + aux_dict[subkey] = {} + aux_dict = aux_dict[subkey] + if not config_val.get(subkey): + # The subkey is not in the config + return {} + config_val = config_val[subkey] + if subkeys[-1] not in config_val: + return {} + aux_dict[subkeys[-1]] = config_val[subkeys[-1]] + return ret_dict + + extra_data = {} + if 'pass_to_ext_pillars' in opts: + if not isinstance(opts['pass_to_ext_pillars'], list): + log.exception('\'pass_to_ext_pillars\' config is malformed.') + raise SaltClientError('\'pass_to_ext_pillars\' config is ' + 'malformed.') + for key in opts['pass_to_ext_pillars']: + salt.utils.dictupdate.update(extra_data, + get_subconfig(key), + recursive_update=True, + merge_lists=True) + log.trace('ext_pillar_extra_data = {0}'.format(extra_data)) + return extra_data + + +class AsyncRemotePillar(RemotePillarMixin): ''' Get the pillar from the master ''' def __init__(self, opts, grains, minion_id, saltenv, ext=None, functions=None, - pillar_override=None, pillarenv=None): + pillar_override=None, pillarenv=None, extra_minion_data=None): self.opts = opts self.opts['environment'] = saltenv self.ext = ext @@ -93,6 +145,14 @@ class AsyncRemotePillar(object): if not isinstance(self.pillar_override, dict): self.pillar_override = {} log.error('Pillar data must be a dictionary') + self.extra_minion_data = extra_minion_data or {} + if not isinstance(self.extra_minion_data, dict): + self.extra_minion_data = {} + log.error('Extra minion data must be a dictionary') + salt.utils.dictupdate.update(self.extra_minion_data, + self.get_ext_pillar_extra_minion_data(opts), + recursive_update=True, + merge_lists=True) @tornado.gen.coroutine def compile_pillar(self): @@ -104,6 +164,7 @@ class AsyncRemotePillar(object): 'saltenv': self.opts['environment'], 'pillarenv': self.opts['pillarenv'], 'pillar_override': self.pillar_override, + 'extra_minion_data': self.extra_minion_data, 'ver': '2', 'cmd': '_pillar'} if self.ext: @@ -126,12 +187,12 @@ class AsyncRemotePillar(object): raise tornado.gen.Return(ret_pillar) -class RemotePillar(object): +class RemotePillar(RemotePillarMixin): ''' Get the pillar from the master ''' def __init__(self, opts, grains, minion_id, saltenv, ext=None, functions=None, - pillar_override=None, pillarenv=None): + pillar_override=None, pillarenv=None, extra_minion_data=None): self.opts = opts self.opts['environment'] = saltenv self.ext = ext @@ -144,6 +205,14 @@ class RemotePillar(object): if not isinstance(self.pillar_override, dict): self.pillar_override = {} log.error('Pillar data must be a dictionary') + self.extra_minion_data = extra_minion_data or {} + if not isinstance(self.extra_minion_data, dict): + self.extra_minion_data = {} + log.error('Extra minion data must be a dictionary') + salt.utils.dictupdate.update(self.extra_minion_data, + self.get_ext_pillar_extra_minion_data(opts), + recursive_update=True, + merge_lists=True) def compile_pillar(self): ''' @@ -154,6 +223,7 @@ class RemotePillar(object): 'saltenv': self.opts['environment'], 'pillarenv': self.opts['pillarenv'], 'pillar_override': self.pillar_override, + 'extra_minion_data': self.extra_minion_data, 'ver': '2', 'cmd': '_pillar'} if self.ext: @@ -187,7 +257,7 @@ class PillarCache(object): ''' # TODO ABC? def __init__(self, opts, grains, minion_id, saltenv, ext=None, functions=None, - pillar_override=None, pillarenv=None): + pillar_override=None, pillarenv=None, extra_minion_data=None): # Yes, we need all of these because we need to route to the Pillar object # if we have no cache. This is another refactor target. @@ -265,7 +335,7 @@ class Pillar(object): Read over the pillar top files and render the pillar data ''' def __init__(self, opts, grains, minion_id, saltenv, ext=None, functions=None, - pillar_override=None, pillarenv=None): + pillar_override=None, pillarenv=None, extra_minion_data=None): self.minion_id = minion_id self.ext = ext if pillarenv is None: @@ -311,6 +381,10 @@ class Pillar(object): if not isinstance(self.pillar_override, dict): self.pillar_override = {} log.error('Pillar data must be a dictionary') + self.extra_minion_data = extra_minion_data or {} + if not isinstance(self.extra_minion_data, dict): + self.extra_minion_data = {} + log.error('Extra minion data must be a dictionary') def __valid_on_demand_ext_pillar(self, opts): ''' @@ -416,20 +490,19 @@ class Pillar(object): self.opts['pillarenv'], ', '.join(self.opts['file_roots']) ) else: - tops[self.opts['pillarenv']] = [ - compile_template( - self.client.cache_file( - self.opts['state_top'], - self.opts['pillarenv'] - ), - self.rend, - self.opts['renderer'], - self.opts['renderer_blacklist'], - self.opts['renderer_whitelist'], - self.opts['pillarenv'], - _pillar_rend=True, - ) - ] + top = self.client.cache_file(self.opts['state_top'], self.opts['pillarenv']) + if top: + tops[self.opts['pillarenv']] = [ + compile_template( + top, + self.rend, + self.opts['renderer'], + self.opts['renderer_blacklist'], + self.opts['renderer_whitelist'], + self.opts['pillarenv'], + _pillar_rend=True, + ) + ] else: for saltenv in self._get_envs(): if self.opts.get('pillar_source_merging_strategy', None) == "none": @@ -768,17 +841,35 @@ class Pillar(object): Builds actual pillar data structure and updates the ``pillar`` variable ''' ext = None + args = salt.utils.args.get_function_argspec(self.ext_pillars[key]).args if isinstance(val, dict): - ext = self.ext_pillars[key](self.minion_id, pillar, **val) + if ('extra_minion_data' in args) and self.extra_minion_data: + ext = self.ext_pillars[key]( + self.minion_id, pillar, + extra_minion_data=self.extra_minion_data, **val) + else: + ext = self.ext_pillars[key](self.minion_id, pillar, **val) elif isinstance(val, list): - ext = self.ext_pillars[key](self.minion_id, - pillar, - *val) + if ('extra_minion_data' in args) and self.extra_minion_data: + ext = self.ext_pillars[key]( + self.minion_id, pillar, *val, + extra_minion_data=self.extra_minion_data) + else: + ext = self.ext_pillars[key](self.minion_id, + pillar, + *val) else: - ext = self.ext_pillars[key](self.minion_id, - pillar, - val) + if ('extra_minion_data' in args) and self.extra_minion_data: + ext = self.ext_pillars[key]( + self.minion_id, + pillar, + val, + extra_minion_data=self.extra_minion_data) + else: + ext = self.ext_pillars[key](self.minion_id, + pillar, + val) return ext def ext_pillar(self, pillar, errors=None): diff --git a/salt/pillar/consul_pillar.py b/salt/pillar/consul_pillar.py index 0d10b80c36..d661649f4f 100644 --- a/salt/pillar/consul_pillar.py +++ b/salt/pillar/consul_pillar.py @@ -167,7 +167,8 @@ def ext_pillar(minion_id, opts['target'] = match.group(1) temp = temp.replace(match.group(0), '') checker = salt.utils.minions.CkMinions(__opts__) - minions = checker.check_minions(opts['target'], 'compound') + _res = checker.check_minions(opts['target'], 'compound') + minions = _res['minions'] if minion_id not in minions: return {} diff --git a/salt/pillar/file_tree.py b/salt/pillar/file_tree.py index 59b420e4a4..323958e2f9 100644 --- a/salt/pillar/file_tree.py +++ b/salt/pillar/file_tree.py @@ -336,9 +336,10 @@ def ext_pillar(minion_id, if (os.path.isdir(nodegroups_dir) and nodegroup in master_ngroups): ckminions = salt.utils.minions.CkMinions(__opts__) - match = ckminions.check_minions( + _res = ckminions.check_minions( master_ngroups[nodegroup], 'compound') + match = _res['minions'] if minion_id in match: ngroup_dir = os.path.join( nodegroups_dir, str(nodegroup)) diff --git a/salt/pillar/nacl.py b/salt/pillar/nacl.py new file mode 100644 index 0000000000..912becfc2b --- /dev/null +++ b/salt/pillar/nacl.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +''' +Decrypt pillar data through the builtin NACL renderer + +In most cases, you'll want to make this the last external pillar used. For +example, to pair with the builtin stack pillar you could do something like +this: + +.. code:: yaml + + nacl.config: + keyfile: /root/.nacl + + ext_pillar: + - stack: /path/to/stack.cfg + - nacl: {} + +Set ``nacl.config`` in your config. + +''' + +from __future__ import absolute_import +import salt + + +def ext_pillar(minion_id, pillar, *args, **kwargs): + render_function = salt.loader.render(__opts__, __salt__).get("nacl") + return render_function(pillar) diff --git a/salt/pillar/nodegroups.py b/salt/pillar/nodegroups.py index bf74c2f8b6..540213436c 100644 --- a/salt/pillar/nodegroups.py +++ b/salt/pillar/nodegroups.py @@ -64,9 +64,10 @@ def ext_pillar(minion_id, pillar, pillar_name=None): ckminions = None for nodegroup_name in six.iterkeys(all_nodegroups): ckminions = ckminions or CkMinions(__opts__) - match = ckminions.check_minions( + _res = ckminions.check_minions( all_nodegroups[nodegroup_name], 'compound') + match = _res['minions'] if minion_id in match: nodegroups_minion_is_in.append(nodegroup_name) diff --git a/salt/proxy/cimc.py b/salt/proxy/cimc.py new file mode 100644 index 0000000000..4692a8ef31 --- /dev/null +++ b/salt/proxy/cimc.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +''' + +Proxy Minion interface module for managing Cisco Integrated Management Controller devices. + +:codeauthor: :email:`Spencer Ervin ` +:maturity: new +:depends: none +:platform: unix + +This proxy minion enables Cisco Integrated Management Controller devices (hereafter referred to +as simply 'cimc' devices to be treated individually like a Salt Minion. + +The cimc proxy leverages the XML API functionality on the Cisco Integrated Management Controller. +The Salt proxy must have access to the cimc on HTTPS (tcp/443). + +More in-depth conceptual reading on Proxy Minions can be found in the +:ref:`Proxy Minion ` section of Salt's +documentation. + + +Configuration +============= +To use this integration proxy module, please configure the following: + +Pillar +------ + +Proxy minions get their configuration from Salt's Pillar. Every proxy must +have a stanza in Pillar and a reference in the Pillar top-file that matches +the ID. + +.. code-block:: yaml + + proxy: + proxytype: cimc + host: + username: + password: + +proxytype +^^^^^^^^^ +The ``proxytype`` key and value pair is critical, as it tells Salt which +interface to load from the ``proxy`` directory in Salt's install hierarchy, +or from ``/srv/salt/_proxy`` on the Salt Master (if you have created your +own proxy module, for example). To use this cimc Proxy Module, set this to +``cimc``. + +host +^^^^ +The location, or ip/dns, of the cimc host. Required. + +username +^^^^^^^^ +The username used to login to the cimc host. Required. + +password +^^^^^^^^ +The password used to login to the cimc host. Required. + +''' + +from __future__ import absolute_import + +# Import Python Libs +import logging +import re + +# Import Salt Libs +import salt.exceptions +from salt._compat import ElementTree as ET + +# This must be present or the Salt loader won't load this module. +__proxyenabled__ = ['cimc'] + +# Variables are scoped to this module so we can have persistent data. +GRAINS_CACHE = {'vendor': 'Cisco'} +DETAILS = {} + +# Set up logging +log = logging.getLogger(__file__) + +# Define the module's virtual name +__virtualname__ = 'cimc' + + +def __virtual__(): + ''' + Only return if all the modules are available. + ''' + return __virtualname__ + + +def init(opts): + ''' + This function gets called when the proxy starts up. + ''' + if 'host' not in opts['proxy']: + log.critical('No \'host\' key found in pillar for this proxy.') + return False + if 'username' not in opts['proxy']: + log.critical('No \'username\' key found in pillar for this proxy.') + return False + if 'password' not in opts['proxy']: + log.critical('No \'passwords\' key found in pillar for this proxy.') + return False + + DETAILS['url'] = 'https://{0}/nuova'.format(opts['proxy']['host']) + DETAILS['headers'] = {'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': 62, + 'USER-Agent': 'lwp-request/2.06'} + + # Set configuration details + DETAILS['host'] = opts['proxy']['host'] + DETAILS['username'] = opts['proxy'].get('username') + DETAILS['password'] = opts['proxy'].get('password') + + # Ensure connectivity to the device + log.debug("Attempting to connect to cimc proxy host.") + get_config_resolver_class("computeRackUnit") + log.debug("Successfully connected to cimc proxy host.") + + DETAILS['initialized'] = True + + +def set_config_modify(dn=None, inconfig=None, hierarchical=False): + ''' + The configConfMo method configures the specified managed object in a single subtree (for example, DN). + ''' + ret = {} + cookie = logon() + + # Declare if the search contains hierarchical results. + h = "false" + if hierarchical is True: + h = "true" + + payload = '' \ + '{3}'.format(cookie, h, dn, inconfig) + r = __utils__['http.query'](DETAILS['url'], + data=payload, + method='POST', + decode_type='plain', + decode=True, + verify_ssl=False, + raise_error=True, + headers=DETAILS['headers']) + answer = re.findall(r'(<[\s\S.]*>)', r['text'])[0] + items = ET.fromstring(answer) + logout(cookie) + for item in items: + ret[item.tag] = prepare_return(item) + return ret + + +def get_config_resolver_class(cid=None, hierarchical=False): + ''' + The configResolveClass method returns requested managed object in a given class. + ''' + ret = {} + cookie = logon() + + # Declare if the search contains hierarchical results. + h = "false" + if hierarchical is True: + h = "true" + + payload = ''.format(cookie, h, cid) + r = __utils__['http.query'](DETAILS['url'], + data=payload, + method='POST', + decode_type='plain', + decode=True, + verify_ssl=False, + raise_error=True, + headers=DETAILS['headers']) + + answer = re.findall(r'(<[\s\S.]*>)', r['text'])[0] + items = ET.fromstring(answer) + logout(cookie) + for item in items: + ret[item.tag] = prepare_return(item) + return ret + + +def logon(): + ''' + Logs into the cimc device and returns the session cookie. + ''' + content = {} + payload = "".format(DETAILS['username'], DETAILS['password']) + r = __utils__['http.query'](DETAILS['url'], + data=payload, + method='POST', + decode_type='plain', + decode=True, + verify_ssl=False, + raise_error=False, + headers=DETAILS['headers']) + answer = re.findall(r'(<[\s\S.]*>)', r['text'])[0] + items = ET.fromstring(answer) + for item in items.attrib: + content[item] = items.attrib[item] + + if 'outCookie' not in content: + raise salt.exceptions.CommandExecutionError("Unable to log into proxy device.") + + return content['outCookie'] + + +def logout(cookie=None): + ''' + Closes the session with the device. + ''' + payload = ''.format(cookie) + __utils__['http.query'](DETAILS['url'], + data=payload, + method='POST', + decode_type='plain', + decode=True, + verify_ssl=False, + raise_error=True, + headers=DETAILS['headers']) + return + + +def prepare_return(x): + ''' + Converts the etree to dict + ''' + ret = {} + for a in list(x): + if a.tag not in ret: + ret[a.tag] = [] + ret[a.tag].append(prepare_return(a)) + for a in x.attrib: + ret[a] = x.attrib[a] + return ret + + +def initialized(): + ''' + Since grains are loaded in many different places and some of those + places occur before the proxy can be initialized, return whether + our init() function has been called + ''' + return DETAILS.get('initialized', False) + + +def grains(): + ''' + Get the grains from the proxied device + ''' + if not DETAILS.get('grains_cache', {}): + DETAILS['grains_cache'] = GRAINS_CACHE + try: + compute_rack = get_config_resolver_class('computeRackUnit', False) + DETAILS['grains_cache'] = compute_rack['outConfigs']['computeRackUnit'] + except Exception as err: + log.error(err) + return DETAILS['grains_cache'] + + +def grains_refresh(): + ''' + Refresh the grains from the proxied device + ''' + DETAILS['grains_cache'] = None + return grains() + + +def ping(): + ''' + Returns true if the device is reachable, else false. + ''' + try: + cookie = logon() + logout(cookie) + except Exception as err: + log.debug(err) + return False + return True + + +def shutdown(): + ''' + Shutdown the connection to the proxy device. For this proxy, + shutdown is a no-op. + ''' + log.debug('CIMC proxy shutdown() called.') diff --git a/salt/proxy/esxcluster.py b/salt/proxy/esxcluster.py new file mode 100644 index 0000000000..af3740d8d5 --- /dev/null +++ b/salt/proxy/esxcluster.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- +''' +Proxy Minion interface module for managing VMWare ESXi clusters. + +Dependencies +============ + +- pyVmomi +- jsonschema + +Configuration +============= +To use this integration proxy module, please configure the following: + +Pillar +------ + +Proxy minions get their configuration from Salt's Pillar. This can now happen +from the proxy's configuration file. + +Example pillars: + +``userpass`` mechanism: + +.. code-block:: yaml + + proxy: + proxytype: esxcluster + cluster: + datacenter: + vcenter: + mechanism: userpass + username: + passwords: (required if userpass is used) + - first_password + - second_password + - third_password + +``sspi`` mechanism: + +.. code-block:: yaml + + proxy: + proxytype: esxcluster + cluster: + datacenter: + vcenter: + mechanism: sspi + domain: + principal: + +proxytype +^^^^^^^^^ +To use this Proxy Module, set this to ``esxdatacenter``. + +cluster +^^^^^^^ +Name of the managed cluster. Required. + +datacenter +^^^^^^^^^^ +Name of the datacenter the managed cluster is in. Required. + +vcenter +^^^^^^^ +The location of the VMware vCenter server (host of ip) where the datacenter +should be managed. Required. + +mechanism +^^^^^^^^ +The mechanism used to connect to the vCenter server. Supported values are +``userpass`` and ``sspi``. Required. + +Note: + Connections are attempted using all (``username``, ``password``) + combinations on proxy startup. + +username +^^^^^^^^ +The username used to login to the host, such as ``root``. Required if mechanism +is ``userpass``. + +passwords +^^^^^^^^^ +A list of passwords to be used to try and login to the vCenter server. At least +one password in this list is required if mechanism is ``userpass``. When the +proxy comes up, it will try the passwords listed in order. + +domain +^^^^^^ +User domain. Required if mechanism is ``sspi``. + +principal +^^^^^^^^ +Kerberos principal. Rquired if mechanism is ``sspi``. + +protocol +^^^^^^^^ +If the ESXi host is not using the default protocol, set this value to an +alternate protocol. Default is ``https``. + +port +^^^^ +If the ESXi host is not using the default port, set this value to an +alternate port. Default is ``443``. + +Salt Proxy +---------- + +After your pillar is in place, you can test the proxy. The proxy can run on +any machine that has network connectivity to your Salt Master and to the +vCenter server in the pillar. SaltStack recommends that the machine running the +salt-proxy process also run a regular minion, though it is not strictly +necessary. + +To start a proxy minion one needs to establish its identity : + +.. code-block:: bash + + salt-proxy --proxyid + +On the machine that will run the proxy, make sure there is a configuration file +present. By default this is ``/etc/salt/proxy``. If in a different location, the +```` has to be specified when running the proxy: +file with at least the following in it: + +.. code-block:: bash + + salt-proxy --proxyid -c + +Commands +-------- + +Once the proxy is running it will connect back to the specified master and +individual commands can be runs against it: + +.. code-block:: bash + + # Master - minion communication + salt test.ping + + # Test vcenter connection + salt vsphere.test_vcenter_connection + +States +------ + +Associated states are documented in +:mod:`salt.states.esxcluster `. +Look there to find an example structure for Pillar as well as an example +``.sls`` file for configuring an ESX cluster from scratch. +''' + + +# Import Python Libs +from __future__ import absolute_import +import logging +import os + +# Import Salt Libs +import salt.exceptions +from salt.config.schemas.esxcluster import EsxclusterProxySchema +from salt.utils.dictupdate import merge + +# This must be present or the Salt loader won't load this module. +__proxyenabled__ = ['esxcluster'] + +# External libraries +try: + import jsonschema + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False + +# Variables are scoped to this module so we can have persistent data +# across calls to fns in here. +GRAINS_CACHE = {} +DETAILS = {} + + +# Set up logging +log = logging.getLogger(__name__) +# Define the module's virtual name +__virtualname__ = 'esxcluster' + + +def __virtual__(): + ''' + Only load if the vsphere execution module is available. + ''' + if HAS_JSONSCHEMA: + return __virtualname__ + + return False, 'The esxcluster proxy module did not load.' + + +def init(opts): + ''' + This function gets called when the proxy starts up. For + login + the protocol and port are cached. + ''' + log.debug('Initting esxcluster proxy module in process ' + '{}'.format(os.getpid())) + log.debug('Validating esxcluster proxy input') + schema = EsxclusterProxySchema.serialize() + log.trace('schema = {}'.format(schema)) + proxy_conf = merge(opts.get('proxy', {}), __pillar__.get('proxy', {})) + log.trace('proxy_conf = {0}'.format(proxy_conf)) + try: + jsonschema.validate(proxy_conf, schema) + except jsonschema.exceptions.ValidationError as exc: + raise salt.exceptions.InvalidConfigError(exc) + + # Save mandatory fields in cache + for key in ('vcenter', 'datacenter', 'cluster', 'mechanism'): + DETAILS[key] = proxy_conf[key] + + # Additional validation + if DETAILS['mechanism'] == 'userpass': + if 'username' not in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'userpass\', but no ' + '\'username\' key found in proxy config.') + if 'passwords' not in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'userpass\', but no ' + '\'passwords\' key found in proxy config.') + for key in ('username', 'passwords'): + DETAILS[key] = proxy_conf[key] + else: + if 'domain' not in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'sspi\', but no ' + '\'domain\' key found in proxy config.') + if 'principal' not in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'sspi\', but no ' + '\'principal\' key found in proxy config.') + for key in ('domain', 'principal'): + DETAILS[key] = proxy_conf[key] + + # Save optional + DETAILS['protocol'] = proxy_conf.get('protocol') + DETAILS['port'] = proxy_conf.get('port') + + # Test connection + if DETAILS['mechanism'] == 'userpass': + # Get the correct login details + log.debug('Retrieving credentials and testing vCenter connection for ' + 'mehchanism \'userpass\'') + try: + username, password = find_credentials() + DETAILS['password'] = password + except salt.exceptions.SaltSystemExit as err: + log.critical('Error: {0}'.format(err)) + return False + return True + + +def ping(): + ''' + Returns True. + + CLI Example: + + .. code-block:: bash + + salt esx-cluster test.ping + ''' + return True + + +def shutdown(): + ''' + Shutdown the connection to the proxy device. For this proxy, + shutdown is a no-op. + ''' + log.debug('esxcluster proxy shutdown() called...') + + +def find_credentials(): + ''' + Cycle through all the possible credentials and return the first one that + works. + ''' + + # if the username and password were already found don't fo though the + # connection process again + if 'username' in DETAILS and 'password' in DETAILS: + return DETAILS['username'], DETAILS['password'] + + passwords = DETAILS['passwords'] + for password in passwords: + DETAILS['password'] = password + if not __salt__['vsphere.test_vcenter_connection'](): + # We are unable to authenticate + continue + # If we have data returned from above, we've successfully authenticated. + return DETAILS['username'], password + # We've reached the end of the list without successfully authenticating. + raise salt.exceptions.VMwareConnectionError('Cannot complete login due to ' + 'incorrect credentials.') + + +def get_details(): + ''' + Function that returns the cached details + ''' + return DETAILS diff --git a/salt/proxy/esxdatacenter.py b/salt/proxy/esxdatacenter.py index 186b880c2c..5460863d84 100644 --- a/salt/proxy/esxdatacenter.py +++ b/salt/proxy/esxdatacenter.py @@ -153,6 +153,7 @@ import os # Import Salt Libs import salt.exceptions from salt.config.schemas.esxdatacenter import EsxdatacenterProxySchema +from salt.utils.dictupdate import merge # This must be present or the Salt loader won't load this module. __proxyenabled__ = ['esxdatacenter'] @@ -195,42 +196,44 @@ def init(opts): log.trace('Validating esxdatacenter proxy input') schema = EsxdatacenterProxySchema.serialize() log.trace('schema = {}'.format(schema)) + proxy_conf = merge(opts.get('proxy', {}), __pillar__.get('proxy', {})) + log.trace('proxy_conf = {0}'.format(proxy_conf)) try: - jsonschema.validate(opts['proxy'], schema) + jsonschema.validate(proxy_conf, schema) except jsonschema.exceptions.ValidationError as exc: raise salt.exceptions.InvalidConfigError(exc) # Save mandatory fields in cache for key in ('vcenter', 'datacenter', 'mechanism'): - DETAILS[key] = opts['proxy'][key] + DETAILS[key] = proxy_conf[key] # Additional validation if DETAILS['mechanism'] == 'userpass': - if 'username' not in opts['proxy']: + if 'username' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'userpass\', but no ' '\'username\' key found in proxy config.') - if 'passwords' not in opts['proxy']: + if 'passwords' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'userpass\', but no ' '\'passwords\' key found in proxy config.') for key in ('username', 'passwords'): - DETAILS[key] = opts['proxy'][key] + DETAILS[key] = proxy_conf[key] else: - if 'domain' not in opts['proxy']: + if 'domain' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'sspi\', but no ' '\'domain\' key found in proxy config.') - if 'principal' not in opts['proxy']: + if 'principal' not in proxy_conf: raise salt.exceptions.InvalidConfigError( 'Mechanism is set to \'sspi\', but no ' '\'principal\' key found in proxy config.') for key in ('domain', 'principal'): - DETAILS[key] = opts['proxy'][key] + DETAILS[key] = proxy_conf[key] # Save optional - DETAILS['protocol'] = opts['proxy'].get('protocol') - DETAILS['port'] = opts['proxy'].get('port') + DETAILS['protocol'] = proxy_conf.get('protocol') + DETAILS['port'] = proxy_conf.get('port') # Test connection if DETAILS['mechanism'] == 'userpass': diff --git a/salt/proxy/junos.py b/salt/proxy/junos.py index c8278fdc71..e3227bb4ae 100644 --- a/salt/proxy/junos.py +++ b/salt/proxy/junos.py @@ -37,7 +37,6 @@ Run the salt proxy via the following command: ''' from __future__ import absolute_import -# Import python libs import logging # Import 3rd-party libs @@ -47,6 +46,11 @@ try: import jnpr.junos.utils import jnpr.junos.utils.config import jnpr.junos.utils.sw + from jnpr.junos.exception import RpcTimeoutError + from jnpr.junos.exception import ConnectClosedError + from jnpr.junos.exception import RpcError + from jnpr.junos.exception import ConnectError + from ncclient.operations.errors import TimeoutExpiredError except ImportError: HAS_JUNOS = False @@ -118,11 +122,31 @@ def conn(): def alive(opts): ''' - Return the connection status with the remote device. + Validate and return the connection status with the remote device. .. versionadded:: Oxygen ''' - return thisproxy['conn'].connected + + dev = conn() + + # Check that the underlying netconf connection still exists. + if dev._conn is None: + return False + + # call rpc only if ncclient queue is empty. If not empty that means other + # rpc call is going on. + if hasattr(dev._conn, '_session'): + if dev._conn._session._transport.is_active(): + # there is no on going rpc call. + if dev._conn._session._q.empty(): + thisproxy['conn'].connected = ping() + else: + # ssh connection is lost + dev.connected = False + else: + # other connection modes, like telnet + thisproxy['conn'].connected = ping() + return dev.connected def proxytype(): @@ -150,7 +174,16 @@ def ping(): ''' Ping? Pong! ''' - return thisproxy['conn'].connected + + dev = conn() + try: + dev.rpc.file_list(path='/dev/null', dev_timeout=2) + return True + except (RpcTimeoutError, ConnectClosedError): + try: + dev.close() + except (RpcError, ConnectError, TimeoutExpiredError): + return False def shutdown(opts): diff --git a/salt/proxy/panos.py b/salt/proxy/panos.py index 324a2e8ea3..f7fb8f574c 100644 --- a/salt/proxy/panos.py +++ b/salt/proxy/panos.py @@ -402,7 +402,7 @@ def ping(): ''' try: query = {'type': 'op', 'cmd': ''} - if 'result' in call(query)['system']: + if 'system' in call(query): return True else: return False diff --git a/salt/renderers/nacl.py b/salt/renderers/nacl.py index 98fa247b5b..91ba558e9d 100644 --- a/salt/renderers/nacl.py +++ b/salt/renderers/nacl.py @@ -22,8 +22,7 @@ To set things up, first generate a keypair. On the master, run the following: .. code-block:: bash - # salt-call --local nacl.keygen keyfile=/root/.nacl - # salt-call --local nacl.keygen_pub keyfile_pub=/root/.nacl.pub + # salt-call --local nacl.keygen sk_file=/root/.nacl Using encrypted pillar @@ -33,7 +32,7 @@ To encrypt secrets, copy the public key to your local machine and run: .. code-block:: bash - $ salt-call --local nacl.enc_pub datatoenc keyfile_pub=/root/.nacl.pub + $ salt-call --local nacl.enc datatoenc pk_file=/root/.nacl.pub To apply the renderer on a file-by-file basis add the following line to the @@ -80,7 +79,7 @@ def _decrypt_object(obj, **kwargs): return _decrypt_object(obj.getvalue(), **kwargs) if isinstance(obj, six.string_types): if re.search(NACL_REGEX, obj) is not None: - return __salt__['nacl.dec_pub'](re.search(NACL_REGEX, obj).group(1), **kwargs) + return __salt__['nacl.dec'](re.search(NACL_REGEX, obj).group(1), **kwargs) else: return obj elif isinstance(obj, dict): diff --git a/salt/returners/carbon_return.py b/salt/returners/carbon_return.py index 10c22b29f4..ba06ad29b6 100644 --- a/salt/returners/carbon_return.py +++ b/salt/returners/carbon_return.py @@ -303,4 +303,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/cassandra_cql_return.py b/salt/returners/cassandra_cql_return.py index 2a3b325595..5e7e7c0ff9 100644 --- a/salt/returners/cassandra_cql_return.py +++ b/salt/returners/cassandra_cql_return.py @@ -454,4 +454,4 @@ def prep_jid(nocache, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/cassandra_return.py b/salt/returners/cassandra_return.py index 2cae15bd9f..d2c0e2b06d 100644 --- a/salt/returners/cassandra_return.py +++ b/salt/returners/cassandra_return.py @@ -80,4 +80,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/couchbase_return.py b/salt/returners/couchbase_return.py index 24c3a9105a..972d7afb6b 100644 --- a/salt/returners/couchbase_return.py +++ b/salt/returners/couchbase_return.py @@ -160,7 +160,7 @@ def prep_jid(nocache=False, passed_jid=None): So do what you have to do to make sure that stays the case ''' if passed_jid is None: - jid = salt.utils.jid.gen_jid() + jid = salt.utils.jid.gen_jid(__opts__) else: jid = passed_jid @@ -213,8 +213,8 @@ def save_load(jid, clear_load, minion=None): try: jid_doc = cb_.get(str(jid)) except couchbase.exceptions.NotFoundError: - log.warning('Could not write job cache file for jid: {0}'.format(jid)) - return False + cb_.add(str(jid), {}, ttl=_get_ttl()) + jid_doc = cb_.get(str(jid)) jid_doc.value['load'] = clear_load cb_.replace(str(jid), jid_doc.value, cas=jid_doc.cas, ttl=_get_ttl()) @@ -223,10 +223,11 @@ def save_load(jid, clear_load, minion=None): if 'tgt' in clear_load and clear_load['tgt'] != '': ckminions = salt.utils.minions.CkMinions(__opts__) # Retrieve the minions list - minions = ckminions.check_minions( + _res = ckminions.check_minions( clear_load['tgt'], clear_load.get('tgt_type', 'glob') ) + minions = _res['minions'] save_minions(jid, minions) diff --git a/salt/returners/couchdb_return.py b/salt/returners/couchdb_return.py index d24020db4e..117b2802f4 100644 --- a/salt/returners/couchdb_return.py +++ b/salt/returners/couchdb_return.py @@ -364,7 +364,7 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) def save_minions(jid, minions, syndic_id=None): # pylint: disable=unused-argument diff --git a/salt/returners/django_return.py b/salt/returners/django_return.py index 5d756e6111..8a4517a5ce 100644 --- a/salt/returners/django_return.py +++ b/salt/returners/django_return.py @@ -82,4 +82,4 @@ def prep_jid(nocache=False, passed_jid=None): ''' Do any work necessary to prepare a JID, including sending a custom ID ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/elasticsearch_return.py b/salt/returners/elasticsearch_return.py index 5849c0e0e7..e4ffb20f1e 100644 --- a/salt/returners/elasticsearch_return.py +++ b/salt/returners/elasticsearch_return.py @@ -362,7 +362,7 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) def save_load(jid, load, minions=None): diff --git a/salt/returners/etcd_return.py b/salt/returners/etcd_return.py index 6582e957e2..5f3d8a2bae 100644 --- a/salt/returners/etcd_return.py +++ b/salt/returners/etcd_return.py @@ -223,4 +223,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/influxdb_return.py b/salt/returners/influxdb_return.py index d37958b0e8..e6cafb7cc5 100644 --- a/salt/returners/influxdb_return.py +++ b/salt/returners/influxdb_return.py @@ -328,4 +328,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/local_cache.py b/salt/returners/local_cache.py index 85a1c07264..c0b135c3ef 100644 --- a/salt/returners/local_cache.py +++ b/salt/returners/local_cache.py @@ -90,7 +90,7 @@ def prep_jid(nocache=False, passed_jid=None, recurse_count=0): log.error(err) raise salt.exceptions.SaltCacheError(err) if passed_jid is None: # this can be a None or an empty string. - jid = salt.utils.jid.gen_jid() + jid = salt.utils.jid.gen_jid(__opts__) else: jid = passed_jid @@ -225,10 +225,11 @@ def save_load(jid, clear_load, minions=None, recurse_count=0): if minions is None: ckminions = salt.utils.minions.CkMinions(__opts__) # Retrieve the minions list - minions = ckminions.check_minions( + _res = ckminions.check_minions( clear_load['tgt'], clear_load.get('tgt_type', 'glob') ) + minions = _res['minions'] # save the minions to a cache so we can see in the UI save_minions(jid, minions) @@ -397,7 +398,6 @@ def clean_old_jobs(): Clean out the old jobs from the job cache ''' if __opts__['keep_jobs'] != 0: - cur = time.time() jid_root = _job_dir() if not os.path.exists(jid_root): @@ -427,7 +427,7 @@ def clean_old_jobs(): shutil.rmtree(t_path) elif os.path.isfile(jid_file): jid_ctime = os.stat(jid_file).st_ctime - hours_difference = (cur - jid_ctime) / 3600.0 + hours_difference = (time.time()- jid_ctime) / 3600.0 if hours_difference > __opts__['keep_jobs'] and os.path.exists(t_path): # Remove the entire t_path from the original JID dir shutil.rmtree(t_path) @@ -441,7 +441,7 @@ def clean_old_jobs(): # Checking the time again prevents a possible race condition where # t_path JID dirs were created, but not yet populated by a jid file. t_path_ctime = os.stat(t_path).st_ctime - hours_difference = (cur - t_path_ctime) / 3600.0 + hours_difference = (time.time() - t_path_ctime) / 3600.0 if hours_difference > __opts__['keep_jobs']: shutil.rmtree(t_path) diff --git a/salt/returners/memcache_return.py b/salt/returners/memcache_return.py index dd3657da1f..c00dcbdf9b 100644 --- a/salt/returners/memcache_return.py +++ b/salt/returners/memcache_return.py @@ -134,7 +134,7 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) def returner(ret): diff --git a/salt/returners/mongo_future_return.py b/salt/returners/mongo_future_return.py index d7e6e1df53..0d9c7328b1 100644 --- a/salt/returners/mongo_future_return.py +++ b/salt/returners/mongo_future_return.py @@ -322,7 +322,7 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) def event_return(events): diff --git a/salt/returners/mongo_return.py b/salt/returners/mongo_return.py index a0a0cac8b4..f59c25f9f5 100644 --- a/salt/returners/mongo_return.py +++ b/salt/returners/mongo_return.py @@ -231,7 +231,7 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) def save_minions(jid, minions, syndic_id=None): # pylint: disable=unused-argument diff --git a/salt/returners/mysql.py b/salt/returners/mysql.py index 110c1caf6c..a7bfbed243 100644 --- a/salt/returners/mysql.py +++ b/salt/returners/mysql.py @@ -460,7 +460,7 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) def _purge_jobs(timestamp): diff --git a/salt/returners/odbc.py b/salt/returners/odbc.py index 7cc9ada0d9..03c114cb10 100644 --- a/salt/returners/odbc.py +++ b/salt/returners/odbc.py @@ -329,4 +329,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/pgjsonb.py b/salt/returners/pgjsonb.py index f6af142ac0..dd09d31d78 100644 --- a/salt/returners/pgjsonb.py +++ b/salt/returners/pgjsonb.py @@ -416,4 +416,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/postgres.py b/salt/returners/postgres.py index 7fb45fe837..02cbde3ce7 100644 --- a/salt/returners/postgres.py +++ b/salt/returners/postgres.py @@ -381,4 +381,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/postgres_local_cache.py b/salt/returners/postgres_local_cache.py index 42957f7dbe..0485fa4fe9 100644 --- a/salt/returners/postgres_local_cache.py +++ b/salt/returners/postgres_local_cache.py @@ -189,7 +189,7 @@ def _gen_jid(cur): ''' Generate an unique job id ''' - jid = salt.utils.jid.gen_jid() + jid = salt.utils.jid.gen_jid(__opts__) sql = '''SELECT jid FROM jids WHERE jid = %s''' cur.execute(sql, (jid,)) data = cur.fetchall() diff --git a/salt/returners/redis_return.py b/salt/returners/redis_return.py index 140af0d063..5eea5bf782 100644 --- a/salt/returners/redis_return.py +++ b/salt/returners/redis_return.py @@ -312,4 +312,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/sentry_return.py b/salt/returners/sentry_return.py index 2eba954764..3b3c5b11eb 100644 --- a/salt/returners/sentry_return.py +++ b/salt/returners/sentry_return.py @@ -170,4 +170,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/smtp_return.py b/salt/returners/smtp_return.py index 78dfdc9ef5..f630aa18f6 100644 --- a/salt/returners/smtp_return.py +++ b/salt/returners/smtp_return.py @@ -264,7 +264,7 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) def event_return(events): diff --git a/salt/returners/sqlite3_return.py b/salt/returners/sqlite3_return.py index 55c6c0b36b..1b2159980d 100644 --- a/salt/returners/sqlite3_return.py +++ b/salt/returners/sqlite3_return.py @@ -303,4 +303,4 @@ def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/syslog_return.py b/salt/returners/syslog_return.py index bcafaf614d..f963c751d8 100644 --- a/salt/returners/syslog_return.py +++ b/salt/returners/syslog_return.py @@ -213,4 +213,4 @@ def prep_jid(nocache=False, ''' Do any work necessary to prepare a JID, including sending a custom id ''' - return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid() + return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__) diff --git a/salt/returners/zabbix_return.py b/salt/returners/zabbix_return.py index 6470fe31ff..9969e3365c 100644 --- a/salt/returners/zabbix_return.py +++ b/salt/returners/zabbix_return.py @@ -26,6 +26,7 @@ import os # Import Salt libs from salt.ext import six +import salt.utils.files # Get logging started log = logging.getLogger(__name__) @@ -55,8 +56,20 @@ def zbx(): def zabbix_send(key, host, output): - cmd = zbx()['sender'] + " -c " + zbx()['config'] + " -s " + host + " -k " + key + " -o \"" + output +"\"" - __salt__['cmd.shell'](cmd) + with salt.utils.fopen(zbx()['zabbix_config'], 'r') as file_handle: + for line in file_handle: + if "ServerActive" in line: + flag = "true" + server = line.rsplit('=') + server = server[1].rsplit(',') + for s in server: + cmd = zbx()['sender'] + " -z " + s.replace('\n', '') + " -s " + host + " -k " + key + " -o \"" + output +"\"" + __salt__['cmd.shell'](cmd) + break + else: + flag = "false" + if flag == 'false': + cmd = zbx()['sender'] + " -c " + zbx()['config'] + " -s " + host + " -k " + key + " -o \"" + output +"\"" def returner(ret): diff --git a/salt/roster/cache.py b/salt/roster/cache.py index cea0377f8a..bdb1cc8171 100644 --- a/salt/roster/cache.py +++ b/salt/roster/cache.py @@ -117,7 +117,8 @@ def targets(tgt, tgt_type='glob', **kwargs): # pylint: disable=W0613 The resulting roster can be configured using ``roster_order`` and ``roster_default``. ''' minions = salt.utils.minions.CkMinions(__opts__) - minions = minions.check_minions(tgt, tgt_type) + _res = minions.check_minions(tgt, tgt_type) + minions = _res['minions'] ret = {} if not minions: diff --git a/salt/roster/sshconfig.py b/salt/roster/sshconfig.py new file mode 100644 index 0000000000..29c01ccaf3 --- /dev/null +++ b/salt/roster/sshconfig.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +''' +Parses roster entries out of Host directives from SSH config + +.. code-block:: bash + + salt-ssh --roster sshconfig '*' -r "echo hi" +''' +from __future__ import absolute_import + +# Import python libs +import os +import collections +import fnmatch +import re + +# Import Salt libs +import salt.utils +from salt.ext.six import string_types + +import logging +log = logging.getLogger(__name__) + +_SSHConfRegex = collections.namedtuple('_SSHConfRegex', ['target_field', 'pattern']) +_ROSTER_FIELDS = ( + _SSHConfRegex(target_field='user', pattern=r'\s+User (.*)'), + _SSHConfRegex(target_field='port', pattern=r'\s+Port (.*)'), + _SSHConfRegex(target_field='priv', pattern=r'\s+IdentityFile (.*)'), +) + + +def _get_ssh_config_file(opts): + ''' + :return: Path to the .ssh/config file - usually /.ssh/config + ''' + ssh_config_file = opts.get('ssh_config_file') + if not os.path.isfile(ssh_config_file): + raise IOError('Cannot find SSH config file') + if not os.access(ssh_config_file, os.R_OK): + raise IOError('Cannot access SSH config file: {}'.format(ssh_config_file)) + return ssh_config_file + + +def parse_ssh_config(lines): + ''' + Parses lines from the SSH config to create roster targets. + + :param lines: Individual lines from the ssh config file + :return: Dictionary of targets in similar style to the flat roster + ''' + # transform the list of individual lines into a list of sublists where each + # sublist represents a single Host definition + hosts = [] + for line in lines: + if not line or line.startswith('#'): + continue + elif line.startswith('Host '): + hosts.append([]) + hosts[-1].append(line) + + # construct a dictionary of Host names to mapped roster properties + targets = collections.OrderedDict() + for host_data in hosts: + target = collections.OrderedDict() + hostnames = host_data[0].split()[1:] + for line in host_data[1:]: + for field in _ROSTER_FIELDS: + match = re.match(field.pattern, line) + if match: + target[field.target_field] = match.group(1) + for hostname in hostnames: + targets[hostname] = target + + # apply matching for glob hosts + wildcard_targets = [] + non_wildcard_targets = [] + for target in targets.keys(): + if '*' in target or '?' in target: + wildcard_targets.append(target) + else: + non_wildcard_targets.append(target) + for pattern in wildcard_targets: + for candidate in non_wildcard_targets: + if fnmatch.fnmatch(candidate, pattern): + targets[candidate].update(targets[pattern]) + del targets[pattern] + + # finally, update the 'host' to refer to its declaration in the SSH config + # so that its connection parameters can be utilized + for target in targets: + targets[target]['host'] = target + return targets + + +def targets(tgt, tgt_type='glob', **kwargs): + ''' + Return the targets from the flat yaml file, checks opts for location but + defaults to /etc/salt/roster + ''' + ssh_config_file = _get_ssh_config_file(__opts__) + with salt.utils.fopen(ssh_config_file, 'r') as fp: + all_minions = parse_ssh_config([line.rstrip() for line in fp]) + rmatcher = RosterMatcher(all_minions, tgt, tgt_type) + matched = rmatcher.targets() + return matched + + +class RosterMatcher(object): + ''' + Matcher for the roster data structure + ''' + def __init__(self, raw, tgt, tgt_type): + self.tgt = tgt + self.tgt_type = tgt_type + self.raw = raw + + def targets(self): + ''' + Execute the correct tgt_type routine and return + ''' + try: + return getattr(self, 'ret_{0}_minions'.format(self.tgt_type))() + except AttributeError: + return {} + + def ret_glob_minions(self): + ''' + Return minions that match via glob + ''' + minions = {} + for minion in self.raw: + if fnmatch.fnmatch(minion, self.tgt): + data = self.get_data(minion) + if data: + minions[minion] = data + return minions + + def get_data(self, minion): + ''' + Return the configured ip + ''' + if isinstance(self.raw[minion], string_types): + return {'host': self.raw[minion]} + if isinstance(self.raw[minion], dict): + return self.raw[minion] + return False diff --git a/salt/runners/git_pillar.py b/salt/runners/git_pillar.py index ca302ae7f8..6826268076 100644 --- a/salt/runners/git_pillar.py +++ b/salt/runners/git_pillar.py @@ -28,6 +28,11 @@ def update(branch=None, repo=None): fetched, and ``False`` if there were errors or no new commits were fetched. + .. versionchanged:: Oxygen + The return for a given git_pillar remote will now be ``None`` when no + changes were fetched. ``False`` now is reserved only for instances in + which there were errors. + Fetch one or all configured git_pillar remotes. .. note:: diff --git a/salt/runners/mattermost.py b/salt/runners/mattermost.py index 4ca4e7125c..258e3c2a58 100644 --- a/salt/runners/mattermost.py +++ b/salt/runners/mattermost.py @@ -6,9 +6,10 @@ Module for sending messages to Mattermost :configuration: This module can be used by either passing an api_url and hook directly or by specifying both in a configuration profile in the salt - master/minion config. - For example: + master/minion config. For example: + .. code-block:: yaml + mattermost: hook: peWcBiMOS9HrZG15peWcBiMOS9HrZG15 api_url: https://example.com diff --git a/salt/runners/state.py b/salt/runners/state.py index e27298c2b6..25f12a814e 100644 --- a/salt/runners/state.py +++ b/salt/runners/state.py @@ -81,7 +81,7 @@ def orchestrate(mods, pillar_enc=pillar_enc, orchestration_jid=orchestration_jid) ret = {'data': {minion.opts['id']: running}, 'outputter': 'highstate'} - res = salt.utils.check_state_result(ret['data']) + res = __utils__['state.check_result'](ret['data']) if res: ret['retcode'] = 0 else: diff --git a/salt/spm/__init__.py b/salt/spm/__init__.py index ea19b6ee0e..10da68a9b4 100644 --- a/salt/spm/__init__.py +++ b/salt/spm/__init__.py @@ -13,23 +13,28 @@ import tarfile import shutil import hashlib import logging -import pwd -import grp import sys +try: + import pwd + import grp +except ImportError: + pass # Import Salt libs import salt.client import salt.config import salt.loader import salt.cache -import salt.utils.files -import salt.utils.http as http import salt.syspaths as syspaths from salt.ext import six from salt.ext.six import string_types from salt.ext.six.moves import input from salt.ext.six.moves import filter from salt.template import compile_template +import salt.utils.files +import salt.utils.http as http +import salt.utils.platform +import salt.utils.win_functions from salt.utils.yamldumper import SafeOrderedDumper # Get logging started @@ -490,10 +495,16 @@ class SPMClient(object): # No defaults for this in config.py; default to the current running # user and group - uid = self.opts.get('spm_uid', os.getuid()) - gid = self.opts.get('spm_gid', os.getgid()) - uname = pwd.getpwuid(uid)[0] - gname = grp.getgrgid(gid)[0] + if salt.utils.platform.is_windows(): + uname = gname = salt.utils.win_functions.get_current_user() + uname_sid = salt.utils.win_functions.get_sid_from_name(uname) + uid = self.opts.get('spm_uid', uname_sid) + gid = self.opts.get('spm_gid', uname_sid) + else: + uid = self.opts.get('spm_uid', os.getuid()) + gid = self.opts.get('spm_gid', os.getgid()) + uname = pwd.getpwuid(uid)[0] + gname = grp.getgrgid(gid)[0] # Second pass: install the files for member in pkg_files: @@ -709,7 +720,7 @@ class SPMClient(object): raise SPMInvocationError('A path to a directory must be specified') if args[1] == '.': - repo_path = os.environ['PWD'] + repo_path = os.getcwdu() else: repo_path = args[1] diff --git a/salt/state.py b/salt/state.py index 004ccdaedc..5527a0f87c 100644 --- a/salt/state.py +++ b/salt/state.py @@ -41,6 +41,7 @@ import salt.utils.immutabletypes as immutabletypes import salt.utils.url import salt.utils.platform import salt.utils.process +import salt.utils.files import salt.syspaths as syspaths from salt.template import compile_template, compile_template_str from salt.exceptions import ( @@ -149,6 +150,13 @@ def _gen_tag(low): return u'{0[state]}_|-{0[__id__]}_|-{0[name]}_|-{0[fun]}'.format(low) +def _clean_tag(tag): + ''' + Make tag name safe for filenames + ''' + return salt.utils.files.safe_filename_leaf(tag) + + def _l_tag(name, id_): low = {u'name': u'listen_{0}'.format(name), u'__id__': u'listen_{0}'.format(id_), @@ -969,22 +977,65 @@ class State(object): elif data[u'state'] in (u'pkg', u'ports'): self.module_refresh() - def verify_ret(self, ret): + @staticmethod + def verify_ret(ret): ''' - Verify the state return data + Perform basic verification of the raw state return data ''' if not isinstance(ret, dict): raise SaltException( - u'Malformed state return, return must be a dict' - ) + u'Malformed state return, return must be a dict' + ) bad = [] for val in [u'name', u'result', u'changes', u'comment']: if val not in ret: bad.append(val) if bad: - raise SaltException( - u'The following keys were not present in the state ' - u'return: {0}'.format(u','.join(bad))) + m = u'The following keys were not present in the state return: {0}' + raise SaltException(m.format(u','.join(bad))) + + @staticmethod + def munge_ret_for_export(ret): + ''' + Process raw state return data to make it suitable for export, + to ensure consistency of the data format seen by external systems + ''' + # We support lists of strings for ret['comment'] internal + # to the state system for improved ergonomics. + # However, to maintain backwards compatability with external tools, + # the list representation is not allowed to leave the state system, + # and should be converted like this at external boundaries. + if isinstance(ret[u'comment'], list): + ret[u'comment'] = u'\n'.join(ret[u'comment']) + + @staticmethod + def verify_ret_for_export(ret): + ''' + Verify the state return data for export outside the state system + ''' + State.verify_ret(ret) + + for key in [u'name', u'comment']: + if not isinstance(ret[key], six.string_types): + msg = ( + u'The value for the {0} key in the state return ' + u'must be a string, found {1}' + ) + raise SaltException(msg.format(key, repr(ret[key]))) + + if ret[u'result'] not in [True, False, None]: + msg = ( + u'The value for the result key in the state return ' + u'must be True, False, or None, found {0}' + ) + raise SaltException(msg.format(repr(ret[u'result']))) + + if not isinstance(ret[u'changes'], dict): + msg = ( + u'The value for the changes key in the state return ' + u'must be a dict, found {0}' + ) + raise SaltException(msg.format(repr(ret[u'changes']))) def verify_data(self, data): ''' @@ -1698,7 +1749,7 @@ class State(object): trb) } troot = os.path.join(self.opts[u'cachedir'], self.jid) - tfile = os.path.join(troot, tag) + tfile = os.path.join(troot, _clean_tag(tag)) if not os.path.isdir(troot): try: os.makedirs(troot) @@ -1847,6 +1898,7 @@ class State(object): if u'check_cmd' in low and u'{0[state]}.mod_run_check_cmd'.format(low) not in self.states: ret.update(self._run_check_cmd(low)) self.verify_ret(ret) + self.munge_ret_for_export(ret) except Exception: trb = traceback.format_exc() # There are a number of possibilities to not have the cdata @@ -1874,6 +1926,7 @@ class State(object): self.state_con.pop('runas') self.state_con.pop('runas_password') + self.verify_ret_for_export(ret) # If format_call got any warnings, let's show them to the user if u'warnings' in cdata: @@ -2052,7 +2105,7 @@ class State(object): proc = running[tag].get(u'proc') if proc: if not proc.is_alive(): - ret_cache = os.path.join(self.opts[u'cachedir'], self.jid, tag) + ret_cache = os.path.join(self.opts[u'cachedir'], self.jid, _clean_tag(tag)) if not os.path.isfile(ret_cache): ret = {u'result': False, u'comment': u'Parallel process failed to return', @@ -3119,7 +3172,7 @@ class BaseHighState(object): Returns: {'saltenv': ['state1', 'state2', ...]} ''' - matches = {} + matches = DefaultOrderedDict(OrderedDict) # pylint: disable=cell-var-from-loop for saltenv, body in six.iteritems(top): if self.opts[u'environment']: diff --git a/salt/states/acme.py b/salt/states/acme.py index 1ab6b57dfb..43649a6426 100644 --- a/salt/states/acme.py +++ b/salt/states/acme.py @@ -116,9 +116,14 @@ def cert(name, if res['result'] is None: ret['changes'] = {} else: + if not __salt__['acme.has'](name): + new = None + else: + new = __salt__['acme.info'](name) + ret['changes'] = { 'old': old, - 'new': __salt__['acme.info'](name) + 'new': new } return ret diff --git a/salt/states/archive.py b/salt/states/archive.py index c2308cbbd0..2a1454f99d 100644 --- a/salt/states/archive.py +++ b/salt/states/archive.py @@ -64,16 +64,30 @@ def _gen_checksum(path): 'hash_type': __opts__['hash_type']} -def _update_checksum(cached_source): - cached_source_sum = '.'.join((cached_source, 'hash')) - source_sum = _gen_checksum(cached_source) +def _checksum_file_path(path): + relpath = '.'.join((os.path.relpath(path, __opts__['cachedir']), 'hash')) + if re.match(r'..[/\\]', relpath): + # path is a local file + relpath = salt.utils.path.join( + 'local', + os.path.splitdrive(path)[-1].lstrip('/\\'), + ) + return salt.utils.path.join(__opts__['cachedir'], 'archive_hash', relpath) + + +def _update_checksum(path): + checksum_file = _checksum_file_path(path) + checksum_dir = os.path.dirname(checksum_file) + if not os.path.isdir(checksum_dir): + os.makedirs(checksum_dir) + source_sum = _gen_checksum(path) hash_type = source_sum.get('hash_type') hsum = source_sum.get('hsum') if hash_type and hsum: lines = [] try: try: - with salt.utils.files.fopen(cached_source_sum, 'r') as fp_: + with salt.utils.files.fopen(checksum_file, 'r') as fp_: for line in fp_: try: lines.append(line.rstrip('\n').split(':', 1)) @@ -83,7 +97,7 @@ def _update_checksum(cached_source): if exc.errno != errno.ENOENT: raise - with salt.utils.files.fopen(cached_source_sum, 'w') as fp_: + with salt.utils.files.fopen(checksum_file, 'w') as fp_: for line in lines: if line[0] == hash_type: line[1] = hsum @@ -93,16 +107,16 @@ def _update_checksum(cached_source): except (IOError, OSError) as exc: log.warning( 'Failed to update checksum for %s: %s', - cached_source, exc.__str__() + path, exc.__str__(), exc_info=True ) -def _read_cached_checksum(cached_source, form=None): +def _read_cached_checksum(path, form=None): if form is None: form = __opts__['hash_type'] - path = '.'.join((cached_source, 'hash')) + checksum_file = _checksum_file_path(path) try: - with salt.utils.files.fopen(path, 'r') as fp_: + with salt.utils.files.fopen(checksum_file, 'r') as fp_: for line in fp_: # Should only be one line in this file but just in case it # isn't, read only a single line to avoid overuse of memory. @@ -117,9 +131,9 @@ def _read_cached_checksum(cached_source, form=None): return {'hash_type': hash_type, 'hsum': hsum} -def _compare_checksum(cached_source, source_sum): +def _compare_checksum(cached, source_sum): cached_sum = _read_cached_checksum( - cached_source, + cached, form=source_sum.get('hash_type', __opts__['hash_type']) ) return source_sum == cached_sum @@ -155,7 +169,6 @@ def extracted(name, user=None, group=None, if_missing=None, - keep=False, trim_output=False, use_cmd_unzip=None, extract_perms=True, @@ -391,6 +404,22 @@ def extracted(name, .. versionadded:: 2016.3.4 + keep_source : True + For ``source`` archives not local to the minion (i.e. from the Salt + fileserver or a remote source such as ``http(s)`` or ``ftp``), Salt + will need to download the archive to the minion cache before they can + be extracted. To remove the downloaded archive after extraction, set + this argument to ``False``. + + .. versionadded:: 2017.7.3 + + keep : True + Same as ``keep_source``. + + .. note:: + If both ``keep_source`` and ``keep`` are used, ``keep`` will be + ignored. + password **For ZIP archives only.** Password used for extraction. @@ -518,13 +547,6 @@ def extracted(name, simply checked for existence and extraction will be skipped if if is present. - keep : False - For ``source`` archives not local to the minion (i.e. from the Salt - fileserver or a remote source such as ``http(s)`` or ``ftp``), Salt - will need to download the archive to the minion cache before they can - be extracted. After extraction, these source archives will be removed - unless this argument is set to ``True``. - trim_output : False Useful for archives with many files in them. This can either be set to ``True`` (in which case only the first 100 files extracted will be @@ -626,6 +648,21 @@ def extracted(name, # Remove pub kwargs as they're irrelevant here. kwargs = salt.utils.args.clean_kwargs(**kwargs) + if 'keep_source' in kwargs and 'keep' in kwargs: + ret.setdefault('warnings', []).append( + 'Both \'keep_source\' and \'keep\' were used. Since these both ' + 'do the same thing, \'keep\' was ignored.' + ) + keep_source = bool(kwargs.pop('keep_source')) + kwargs.pop('keep') + elif 'keep_source' in kwargs: + keep_source = bool(kwargs.pop('keep_source')) + elif 'keep' in kwargs: + keep_source = bool(kwargs.pop('keep')) + else: + # Neither was passed, default is True + keep_source = True + if not _path_is_abs(name): ret['comment'] = '{0} is not an absolute path'.format(name) return ret @@ -721,10 +758,10 @@ def extracted(name, urlparsed_source = _urlparse(source_match) source_hash_basename = urlparsed_source.path or urlparsed_source.netloc - source_is_local = urlparsed_source.scheme in ('', 'file') + source_is_local = urlparsed_source.scheme in salt.utils.files.LOCAL_PROTOS if source_is_local: # Get rid of "file://" from start of source_match - source_match = urlparsed_source.path + source_match = os.path.realpath(os.path.expanduser(urlparsed_source.path)) if not os.path.isfile(source_match): ret['comment'] = 'Source file \'{0}\' does not exist'.format(source_match) return ret @@ -858,95 +895,59 @@ def extracted(name, source_sum = {} if source_is_local: - cached_source = source_match + cached = source_match else: - cached_source = os.path.join( - __opts__['cachedir'], - 'files', - __env__, - re.sub(r'[:/\\]', '_', source_hash_basename), - ) - - if os.path.isdir(cached_source): - # Prevent a traceback from attempting to read from a directory path - salt.utils.files.rm_rf(cached_source) - - existing_cached_source_sum = _read_cached_checksum(cached_source) - - if source_is_local: - # No need to download archive, it's local to the minion - update_source = False - else: - if not os.path.isfile(cached_source): - # Archive not cached, we need to download it - update_source = True - else: - # Archive is cached, keep=True likely used in prior run. If we need - # to verify the hash, then we *have* to update the source archive - # to know whether or not the hash changed. Hence the below - # statement. bool(source_hash) will be True if source_hash was - # passed, and otherwise False. - update_source = bool(source_hash) - - if update_source: if __opts__['test']: ret['result'] = None ret['comment'] = ( - 'Archive {0} would be downloaded to cache and checked to ' - 'discover if extraction is necessary'.format( + 'Archive {0} would be cached (if necessary) and checked to ' + 'discover if extraction is needed'.format( salt.utils.url.redact_http_basic_auth(source_match) ) ) return ret - # NOTE: This will result in more than one copy of the source archive on - # the minion. The reason this is necessary is because if we are - # tracking the checksum using source_hash_update, we need a location - # where we can place the checksum file alongside the cached source - # file, where it won't be overwritten by caching a file with the same - # name in the same parent dir as the source file. Long term, we should - # come up with a better solution for this. - file_result = __states__['file.managed'](cached_source, - source=source_match, - source_hash=source_hash, - source_hash_name=source_hash_name, - makedirs=True, - skip_verify=skip_verify) - log.debug('file.managed: {0}'.format(file_result)) - - # Prevent a traceback if errors prevented the above state from getting - # off the ground. - if isinstance(file_result, list): - try: - ret['comment'] = '\n'.join(file_result) - except TypeError: - ret['comment'] = '\n'.join([str(x) for x in file_result]) + if 'file.cached' not in __states__: + # Shouldn't happen unless there is a traceback keeping + # salt/states/file.py from being processed through the loader. If + # that is the case, we have much more important problems as _all_ + # file states would be unavailable. + ret['comment'] = ( + 'Unable to cache {0}, file.cached state not available'.format( + source_match + ) + ) return ret try: - if not file_result['result']: - log.debug( - 'failed to download %s', - salt.utils.url.redact_http_basic_auth(source_match) - ) - return file_result - except TypeError: - if not file_result: - log.debug( - 'failed to download %s', - salt.utils.url.redact_http_basic_auth(source_match) - ) - return file_result + result = __states__['file.cached'](source_match, + source_hash=source_hash, + source_hash_name=source_hash_name, + skip_verify=skip_verify, + saltenv=__env__) + except Exception as exc: + msg = 'Failed to cache {0}: {1}'.format(source_match, exc.__str__()) + log.exception(msg) + ret['comment'] = msg + return ret + else: + log.debug('file.cached: {0}'.format(result)) - else: - log.debug( - 'Archive %s is already in cache', - salt.utils.url.redact_http_basic_auth(source_match) - ) + if result['result']: + # Get the path of the file in the minion cache + cached = __salt__['cp.is_cached'](source_match) + else: + log.debug( + 'failed to download %s', + salt.utils.url.redact_http_basic_auth(source_match) + ) + return result + + existing_cached_source_sum = _read_cached_checksum(cached) if source_hash and source_hash_update and not skip_verify: # Create local hash sum file if we're going to track sum update - _update_checksum(cached_source) + _update_checksum(cached) if archive_format == 'zip' and not password: log.debug('Checking %s to see if it is password-protected', @@ -955,7 +956,7 @@ def extracted(name, # implicitly enabled by setting the "options" argument. try: encrypted_zip = __salt__['archive.is_encrypted']( - cached_source, + cached, clean=False, saltenv=__env__) except CommandExecutionError: @@ -973,7 +974,7 @@ def extracted(name, return ret try: - contents = __salt__['archive.list'](cached_source, + contents = __salt__['archive.list'](cached, archive_format=archive_format, options=list_options, strip_components=strip_components, @@ -1142,7 +1143,7 @@ def extracted(name, if not extraction_needed \ and source_hash_update \ and existing_cached_source_sum is not None \ - and not _compare_checksum(cached_source, existing_cached_source_sum): + and not _compare_checksum(cached, existing_cached_source_sum): extraction_needed = True source_hash_trigger = True else: @@ -1200,13 +1201,13 @@ def extracted(name, __states__['file.directory'](name, user=user, makedirs=True) created_destdir = True - log.debug('Extracting {0} to {1}'.format(cached_source, name)) + log.debug('Extracting {0} to {1}'.format(cached, name)) try: if archive_format == 'zip': if use_cmd_unzip: try: files = __salt__['archive.cmd_unzip']( - cached_source, + cached, name, options=options, trim_output=trim_output, @@ -1216,7 +1217,7 @@ def extracted(name, ret['comment'] = exc.strerror return ret else: - files = __salt__['archive.unzip'](cached_source, + files = __salt__['archive.unzip'](cached, name, options=options, trim_output=trim_output, @@ -1225,7 +1226,7 @@ def extracted(name, **kwargs) elif archive_format == 'rar': try: - files = __salt__['archive.unrar'](cached_source, + files = __salt__['archive.unrar'](cached, name, trim_output=trim_output, **kwargs) @@ -1235,7 +1236,7 @@ def extracted(name, else: if options is None: try: - with closing(tarfile.open(cached_source, 'r')) as tar: + with closing(tarfile.open(cached, 'r')) as tar: tar.extractall(name) files = tar.getnames() if trim_output: @@ -1243,7 +1244,7 @@ def extracted(name, except tarfile.ReadError: if salt.utils.path.which('xz'): if __salt__['cmd.retcode']( - ['xz', '-t', cached_source], + ['xz', '-t', cached], python_shell=False, ignore_retcode=True) == 0: # XZ-compressed data @@ -1259,7 +1260,7 @@ def extracted(name, # pipe it to tar for extraction. cmd = 'xz --decompress --stdout {0} | tar xvf -' results = __salt__['cmd.run_all']( - cmd.format(_cmd_quote(cached_source)), + cmd.format(_cmd_quote(cached)), cwd=name, python_shell=True) if results['retcode'] != 0: @@ -1329,7 +1330,7 @@ def extracted(name, tar_cmd.append(tar_shortopts) tar_cmd.extend(tar_longopts) - tar_cmd.extend(['-f', cached_source]) + tar_cmd.extend(['-f', cached]) results = __salt__['cmd.run_all'](tar_cmd, cwd=name, @@ -1500,18 +1501,15 @@ def extracted(name, for item in enforce_failed: ret['comment'] += '\n- {0}'.format(item) - if not source_is_local and not keep: - for path in (cached_source, __salt__['cp.is_cached'](source_match)): - if not path: - continue - log.debug('Cleaning cached source file %s', path) - try: - os.remove(path) - except OSError as exc: - if exc.errno != errno.ENOENT: - log.error( - 'Failed to clean cached source file %s: %s', - cached_source, exc.__str__() - ) + if not source_is_local: + if keep_source: + log.debug('Keeping cached source file %s', cached) + else: + log.debug('Cleaning cached source file %s', cached) + result = __states__['file.not_cached'](source_match, saltenv=__env__) + if not result['result']: + # Don't let failure to delete cached file cause the state + # itself to fail, just drop it in the warnings. + ret.setdefault('warnings', []).append(result['comment']) return ret diff --git a/salt/states/beacon.py b/salt/states/beacon.py index 7398bf757a..64d1905dc2 100644 --- a/salt/states/beacon.py +++ b/salt/states/beacon.py @@ -9,6 +9,7 @@ Management of the Salt beacons ps: beacon.present: + - save: True - enable: False - services: salt-master: running @@ -37,12 +38,15 @@ log = logging.getLogger(__name__) def present(name, + save=False, **kwargs): ''' Ensure beacon is configured with the included beacon data. name The name of the beacon ensure is configured. + save + True/False, if True the beacons.conf file be updated too. Default is False. ''' @@ -76,10 +80,11 @@ def present(name, ret['changes'] = result['changes'] else: ret['comment'].append(result['comment']) + else: if 'test' in __opts__ and __opts__['test']: kwargs['test'] = True - result = __salt__['beacons.add'](name, beacon_data) + result = __salt__['beacons.add'](name, beacon_data, **kwargs) ret['comment'].append(result['comment']) else: result = __salt__['beacons.add'](name, beacon_data) @@ -90,16 +95,24 @@ def present(name, else: ret['comment'].append('Adding {0} to beacons'.format(name)) + if save: + result = __salt__['beacons.save']() + ret['comment'].append('Beacon {0} saved'.format(name)) + ret['comment'] = '\n'.join(ret['comment']) return ret -def absent(name, **kwargs): +def absent(name, + save=False, + **kwargs): ''' Ensure beacon is absent. name The name of the beacon ensured absent. + save + True/False, if True the beacons.conf file be updated too. Default is False. ''' ### NOTE: The keyword arguments in **kwargs are ignored in this state, but @@ -128,6 +141,10 @@ def absent(name, **kwargs): else: ret['comment'].append('{0} not configured in beacons'.format(name)) + if save: + result = __salt__['beacons.save']() + ret['comment'].append('Beacon {0} saved'.format(name)) + ret['comment'] = '\n'.join(ret['comment']) return ret diff --git a/salt/states/boto_cloudfront.py b/salt/states/boto_cloudfront.py new file mode 100644 index 0000000000..eb4d2ab940 --- /dev/null +++ b/salt/states/boto_cloudfront.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +''' +Manage CloudFront distributions + +.. versionadded:: Oxygen + +Create, update and destroy CloudFront distributions. + +This module accepts explicit AWS credentials but can also utilize +IAM roles assigned to the instance through Instance Profiles. +Dynamic credentials are then automatically obtained from AWS API +and no further configuration is necessary. +More information available `here +`_. + +If IAM roles are not used you need to specify them, +either in a pillar file or in the minion's config file: + +.. code-block:: yaml + + cloudfront.keyid: GKTADJGHEIQSXMKKRBJ08H + cloudfront.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs + +It's also possible to specify ``key``, ``keyid``, and ``region`` via a profile, +either passed in as a dict, or a string to pull from pillars or minion config: + +.. code-block:: yaml + + myprofile: + keyid: GKTADJGHEIQSXMKKRBJ08H + key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs + region: us-east-1 + +.. code-block:: yaml + + aws: + region: + us-east-1: + profile: + keyid: GKTADJGHEIQSXMKKRBJ08H + key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs + region: us-east-1 + +:depends: boto3 +''' + +# Import Python Libs +from __future__ import absolute_import +import difflib +import logging + +import yaml + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only load if boto is available. + ''' + if 'boto_cloudfront.get_distribution' not in __salt__: + msg = 'The boto_cloudfront state module could not be loaded: {}.' + return (False, msg.format('boto_cloudfront exec module unavailable.')) + return 'boto_cloudfront' + + +def present( + name, + config, + tags, + region=None, + key=None, + keyid=None, + profile=None, +): + ''' + Ensure the CloudFront distribution is present. + + name (string) + Name of the CloudFront distribution + + config (dict) + Configuration for the distribution + + tags (dict) + Tags to associate with the distribution + + region (string) + Region to connect to + + key (string) + Secret key to use + + keyid (string) + Access key to use + + profile (dict or string) + A dict with region, key, and keyid, + or a pillar key (string) that contains such a dict. + + Example: + + .. code-block:: yaml + + Manage my_distribution CloudFront distribution: + boto_cloudfront.present: + - name: my_distribution + - config: + Comment: 'partial config shown, most parameters elided' + Enabled: True + - tags: + testing_key: testing_value + ''' + ret = { + 'name': name, + 'comment': '', + 'changes': {}, + } + + res = __salt__['boto_cloudfront.get_distribution']( + name, + region=region, + key=key, + keyid=keyid, + profile=profile, + ) + if 'error' in res: + ret['result'] = False + ret['comment'] = 'Error checking distribution {0}: {1}'.format( + name, + res['error'], + ) + return ret + + old = res['result'] + if old is None: + if __opts__['test']: + ret['result'] = None + ret['comment'] = 'Distribution {0} set for creation.'.format(name) + ret['pchanges'] = {'old': None, 'new': name} + return ret + + res = __salt__['boto_cloudfront.create_distribution']( + name, + config, + tags, + region=region, + key=key, + keyid=keyid, + profile=profile, + ) + if 'error' in res: + ret['result'] = False + ret['comment'] = 'Error creating distribution {0}: {1}'.format( + name, + res['error'], + ) + return ret + + ret['result'] = True + ret['comment'] = 'Created distribution {0}.'.format(name) + ret['changes'] = {'old': None, 'new': name} + return ret + else: + full_config_old = { + 'config': old['distribution']['DistributionConfig'], + 'tags': old['tags'], + } + full_config_new = { + 'config': config, + 'tags': tags, + } + diffed_config = __utils__['dictdiffer.deep_diff']( + full_config_old, + full_config_new, + ) + + def _yaml_safe_dump(attrs): + '''Safely dump YAML using a readable flow style''' + dumper_name = 'IndentedSafeOrderedDumper' + dumper = __utils__['yamldumper.get_dumper'](dumper_name) + return yaml.dump( + attrs, + default_flow_style=False, + Dumper=dumper, + ) + changes_diff = ''.join(difflib.unified_diff( + _yaml_safe_dump(full_config_old).splitlines(True), + _yaml_safe_dump(full_config_new).splitlines(True), + )) + + any_changes = bool('old' in diffed_config or 'new' in diffed_config) + if not any_changes: + ret['result'] = True + ret['comment'] = 'Distribution {0} has correct config.'.format( + name, + ) + return ret + + if __opts__['test']: + ret['result'] = None + ret['comment'] = '\n'.join([ + 'Distribution {0} set for new config:'.format(name), + changes_diff, + ]) + ret['pchanges'] = {'diff': changes_diff} + return ret + + res = __salt__['boto_cloudfront.update_distribution']( + name, + config, + tags, + region=region, + key=key, + keyid=keyid, + profile=profile, + ) + if 'error' in res: + ret['result'] = False + ret['comment'] = 'Error updating distribution {0}: {1}'.format( + name, + res['error'], + ) + return ret + + ret['result'] = True + ret['comment'] = 'Updated distribution {0}.'.format(name) + ret['changes'] = {'diff': changes_diff} + return ret diff --git a/salt/states/boto_elb.py b/salt/states/boto_elb.py index 6163c09dfd..5beb01fd37 100644 --- a/salt/states/boto_elb.py +++ b/salt/states/boto_elb.py @@ -517,7 +517,8 @@ def register_instances(name, instances, region=None, key=None, keyid=None, health = __salt__['boto_elb.get_instance_health']( name, region, key, keyid, profile) - nodes = [value['instance_id'] for value in health] + nodes = [value['instance_id'] for value in health + if value['description'] != 'Instance deregistration currently in progress.'] new = [value for value in instances if value not in nodes] if not len(new): msg = 'Instance/s {0} already exist.'.format(str(instances).strip('[]')) diff --git a/salt/states/boto_sqs.py b/salt/states/boto_sqs.py index 8a33e078ee..e9b142f864 100644 --- a/salt/states/boto_sqs.py +++ b/salt/states/boto_sqs.py @@ -108,8 +108,12 @@ def present( A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. ''' - comments = [] - ret = {'name': name, 'result': True, 'changes': {}} + ret = { + 'name': name, + 'result': True, + 'comment': [], + 'changes': {}, + } r = __salt__['boto_sqs.exists']( name, @@ -120,17 +124,18 @@ def present( ) if 'error' in r: ret['result'] = False - ret['comment'] = '\n'.join(comments + [str(r['error'])]) + ret['comment'].append(r['error']) return ret if r['result']: - comments.append('SQS queue {0} present.'.format(name)) + ret['comment'].append('SQS queue {0} present.'.format(name)) else: if __opts__['test']: ret['result'] = None - comments.append('SQS queue {0} is set to be created.'.format(name)) + ret['comment'].append( + 'SQS queue {0} is set to be created.'.format(name), + ) ret['pchanges'] = {'old': None, 'new': name} - ret['comment'] = '\n'.join(comments) return ret r = __salt__['boto_sqs.create']( @@ -143,22 +148,18 @@ def present( ) if 'error' in r: ret['result'] = False - comments.append('Failed to create SQS queue {0}: {1}'.format( - name, - str(r['error']), - )) - ret['comment'] = '\n'.join(comments) + ret['comment'].append( + 'Failed to create SQS queue {0}: {1}'.format(name, r['error']), + ) return ret - comments.append('SQS queue {0} created.'.format(name)) + ret['comment'].append('SQS queue {0} created.'.format(name)) ret['changes']['old'] = None ret['changes']['new'] = name # Return immediately, as the create call also set all attributes - ret['comment'] = '\n'.join(comments) return ret if not attributes: - ret['comment'] = '\n'.join(comments) return ret r = __salt__['boto_sqs.get_attributes']( @@ -170,10 +171,9 @@ def present( ) if 'error' in r: ret['result'] = False - comments.append('Failed to get queue attributes: {0}'.format( - str(r['error']), - )) - ret['comment'] = '\n'.join(comments) + ret['comment'].append( + 'Failed to get queue attributes: {0}'.format(r['error']), + ) return ret current_attributes = r['result'] @@ -195,8 +195,7 @@ def present( attr_names = ', '.join(attrs_to_set) if not attrs_to_set: - comments.append('Queue attributes already set correctly.') - ret['comment'] = '\n'.join(comments) + ret['comment'].append('Queue attributes already set correctly.') return ret final_attributes = current_attributes.copy() @@ -218,12 +217,13 @@ def present( if __opts__['test']: ret['result'] = None - comments.append('Attribute(s) {0} set to be updated:'.format( - attr_names, - )) - comments.append(attributes_diff) + ret['comment'].append( + 'Attribute(s) {0} set to be updated:\n{1}'.format( + attr_names, + attributes_diff, + ) + ) ret['pchanges'] = {'attributes': {'diff': attributes_diff}} - ret['comment'] = '\n'.join(comments) return ret r = __salt__['boto_sqs.set_attributes']( @@ -236,15 +236,15 @@ def present( ) if 'error' in r: ret['result'] = False - comments.append('Failed to set queue attributes: {0}'.format( - str(r['error']), - )) - ret['comment'] = '\n'.join(comments) + ret['comment'].append( + 'Failed to set queue attributes: {0}'.format(r['error']), + ) return ret - comments.append('Updated SQS queue attribute(s) {0}.'.format(attr_names)) + ret['comment'].append( + 'Updated SQS queue attribute(s) {0}.'.format(attr_names), + ) ret['changes']['attributes'] = {'diff': attributes_diff} - ret['comment'] = '\n'.join(comments) return ret @@ -291,7 +291,7 @@ def absent( if not r['result']: ret['comment'] = 'SQS queue {0} does not exist in {1}.'.format( name, - region + region, ) return ret diff --git a/salt/states/chocolatey.py b/salt/states/chocolatey.py index d83f9bddd3..141d5e7d59 100644 --- a/salt/states/chocolatey.py +++ b/salt/states/chocolatey.py @@ -97,29 +97,61 @@ def installed(name, version=None, source=None, force=False, pre_versions=False, ret['changes'] = {name: 'Version {0} will be installed' ''.format(version)} else: - ret['changes'] = {name: 'Will be installed'} + ret['changes'] = {name: 'Latest version will be installed'} + # Package installed else: version_info = __salt__['chocolatey.version'](name, check_remote=True) full_name = name - lower_name = name.lower() for pkg in version_info: - if lower_name == pkg.lower(): + if name.lower() == pkg.lower(): full_name = pkg - available_version = version_info[full_name]['available'][0] - version = version if version else available_version + installed_version = version_info[full_name]['installed'][0] - if force: - ret['changes'] = {name: 'Version {0} will be forcibly installed' - ''.format(version)} - elif allow_multiple: - ret['changes'] = {name: 'Version {0} will be installed side by side' - ''.format(version)} + if version: + if salt.utils.compare_versions( + ver1=installed_version, oper="==", ver2=version): + if force: + ret['changes'] = { + name: 'Version {0} will be reinstalled'.format(version)} + ret['comment'] = 'Reinstall {0} {1}' \ + ''.format(full_name, version) + else: + ret['comment'] = '{0} {1} is already installed' \ + ''.format(name, version) + if __opts__['test']: + ret['result'] = None + return ret + else: + if allow_multiple: + ret['changes'] = { + name: 'Version {0} will be installed side by side with ' + 'Version {1} if supported' + ''.format(version, installed_version)} + ret['comment'] = 'Install {0} {1} side-by-side with {0} {2}' \ + ''.format(full_name, version, installed_version) + else: + ret['changes'] = { + name: 'Version {0} will be installed over Version {1} ' + ''.format(version, installed_version)} + ret['comment'] = 'Install {0} {1} over {0} {2}' \ + ''.format(full_name, version, installed_version) + force = True else: - ret['comment'] = 'The Package {0} is already installed'.format(name) - return ret + version = installed_version + if force: + ret['changes'] = { + name: 'Version {0} will be reinstalled'.format(version)} + ret['comment'] = 'Reinstall {0} {1}' \ + ''.format(full_name, version) + else: + ret['comment'] = '{0} {1} is already installed' \ + ''.format(name, version) + if __opts__['test']: + ret['result'] = None + return ret if __opts__['test']: ret['result'] = None diff --git a/salt/states/cimc.py b/salt/states/cimc.py new file mode 100644 index 0000000000..11b84839e7 --- /dev/null +++ b/salt/states/cimc.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +''' +A state module to manage Cisco UCS chassis devices. + +:codeauthor: :email:`Spencer Ervin ` +:maturity: new +:depends: none +:platform: unix + + +About +===== +This state module was designed to handle connections to a Cisco Unified Computing System (UCS) chassis. This module +relies on the CIMC proxy module to interface with the device. + +.. seealso:: + :prox:`CIMC Proxy Module ` + +''' + +# Import Python Libs +from __future__ import absolute_import +import logging + +log = logging.getLogger(__name__) + + +def __virtual__(): + return 'cimc.get_system_info' in __salt__ + + +def _default_ret(name): + ''' + Set the default response values. + + ''' + ret = { + 'name': name, + 'changes': {}, + 'result': False, + 'comment': '' + } + return ret + + +def ntp(name, servers): + ''' + Ensures that the NTP servers are configured. Servers are provided as an individual string or list format. Only four + NTP servers will be reviewed. Any entries past four will be ignored. + + name: The name of the module function to execute. + + servers(str, list): The IP address or FQDN of the NTP servers. + + SLS Example: + + .. code-block:: yaml + + ntp_configuration_list: + cimc.ntp: + - servers: + - foo.bar.com + - 10.10.10.10 + + ntp_configuration_str: + cimc.ntp: + - servers: foo.bar.com + + ''' + ret = _default_ret(name) + + ntp_servers = ['', '', '', ''] + + # Parse our server arguments + if isinstance(servers, list): + i = 0 + for x in servers: + ntp_servers[i] = x + i += 1 + else: + ntp_servers[0] = servers + + conf = __salt__['cimc.get_ntp']() + + # Check if our NTP configuration is already set + req_change = False + try: + if conf['outConfigs']['commNtpProvider'][0]['ntpEnable'] != 'yes' \ + or ntp_servers[0] != conf['outConfigs']['commNtpProvider'][0]['ntpServer1'] \ + or ntp_servers[1] != conf['outConfigs']['commNtpProvider'][0]['ntpServer2'] \ + or ntp_servers[2] != conf['outConfigs']['commNtpProvider'][0]['ntpServer3'] \ + or ntp_servers[3] != conf['outConfigs']['commNtpProvider'][0]['ntpServer4']: + req_change = True + except KeyError as err: + ret['result'] = False + ret['comment'] = "Unable to confirm current NTP settings." + log.error(err) + return ret + + if req_change: + + try: + update = __salt__['cimc.set_ntp_server'](ntp_servers[0], + ntp_servers[1], + ntp_servers[2], + ntp_servers[3]) + if update['outConfig']['commNtpProvider'][0]['status'] != 'modified': + ret['result'] = False + ret['comment'] = "Error setting NTP configuration." + return ret + except Exception as err: + ret['result'] = False + ret['comment'] = "Error setting NTP configuration." + log.error(err) + return ret + + ret['changes']['before'] = conf + ret['changes']['after'] = __salt__['cimc.get_ntp']() + ret['comment'] = "NTP settings modified." + else: + ret['comment'] = "NTP already configured. No changes required." + + ret['result'] = True + + return ret + + +def syslog(name, primary=None, secondary=None): + ''' + Ensures that the syslog servers are set to the specified values. A value of None will be ignored. + + name: The name of the module function to execute. + + primary(str): The IP address or FQDN of the primary syslog server. + + secondary(str): The IP address or FQDN of the secondary syslog server. + + SLS Example: + + .. code-block:: yaml + + syslog_configuration: + cimc.syslog: + - primary: 10.10.10.10 + - secondary: foo.bar.com + + ''' + ret = _default_ret(name) + + conf = __salt__['cimc.get_syslog']() + + req_change = False + + if primary: + prim_change = True + if 'outConfigs' in conf and 'commSyslogClient' in conf['outConfigs']: + for entry in conf['outConfigs']['commSyslogClient']: + if entry['name'] != 'primary': + continue + if entry['adminState'] == 'enabled' and entry['hostname'] == primary: + prim_change = False + + if prim_change: + try: + update = __salt__['cimc.set_syslog_server'](primary, "primary") + if update['outConfig']['commSyslogClient'][0]['status'] == 'modified': + req_change = True + else: + ret['result'] = False + ret['comment'] = "Error setting primary SYSLOG server." + return ret + except Exception as err: + ret['result'] = False + ret['comment'] = "Error setting primary SYSLOG server." + log.error(err) + return ret + + if secondary: + sec_change = True + if 'outConfig' in conf and 'commSyslogClient' in conf['outConfig']: + for entry in conf['outConfig']['commSyslogClient']: + if entry['name'] != 'secondary': + continue + if entry['adminState'] == 'enabled' and entry['hostname'] == secondary: + sec_change = False + + if sec_change: + try: + update = __salt__['cimc.set_syslog_server'](secondary, "secondary") + if update['outConfig']['commSyslogClient'][0]['status'] == 'modified': + req_change = True + else: + ret['result'] = False + ret['comment'] = "Error setting secondary SYSLOG server." + return ret + except Exception as err: + ret['result'] = False + ret['comment'] = "Error setting secondary SYSLOG server." + log.error(err) + return ret + + if req_change: + ret['changes']['before'] = conf + ret['changes']['after'] = __salt__['cimc.get_syslog']() + ret['comment'] = "SYSLOG settings modified." + else: + ret['comment'] = "SYSLOG already configured. No changes required." + + ret['result'] = True + + return ret diff --git a/salt/states/cron.py b/salt/states/cron.py index d936762514..c00d79ca87 100644 --- a/salt/states/cron.py +++ b/salt/states/cron.py @@ -116,7 +116,7 @@ entry on the minion already contains a numeric value, then using the ``random`` keyword will not modify it. Added the opportunity to set a job with a special keyword like '@reboot' or -'@hourly'. +'@hourly'. Quotes must be used, otherwise PyYAML will strip the '@' sign. .. code-block:: yaml @@ -302,7 +302,8 @@ def present(name, edits. This defaults to the state id special - A special keyword to specify periodicity (eg. @reboot, @hourly...) + A special keyword to specify periodicity (eg. @reboot, @hourly...). + Quotes must be used, otherwise PyYAML will strip the '@' sign. .. versionadded:: 2016.3.0 ''' @@ -388,7 +389,8 @@ def absent(name, edits. This defaults to the state id special - The special keyword used in the job (eg. @reboot, @hourly...) + The special keyword used in the job (eg. @reboot, @hourly...). + Quotes must be used, otherwise PyYAML will strip the '@' sign. ''' ### NOTE: The keyword arguments in **kwargs are ignored in this state, but ### cannot be removed from the function definition, otherwise the use diff --git a/salt/states/docker_image.py b/salt/states/docker_image.py index e3c1a37779..670fc89111 100644 --- a/salt/states/docker_image.py +++ b/salt/states/docker_image.py @@ -40,6 +40,8 @@ import logging # Import salt libs import salt.utils.docker +import salt.utils.args +from salt.ext.six.moves import zip # Enable proper logging log = logging.getLogger(__name__) # pylint: disable=invalid-name @@ -135,13 +137,14 @@ def present(name, .. versionadded:: 2016.11.0 sls - Allow for building images with ``dockerng.sls_build`` by specify the - SLS files to build with. This can be a list or comma-seperated string. + Allow for building of image with :py:func:`docker.sls_build + ` by specifying the SLS files with + which to build. This can be a list or comma-seperated string. .. code-block:: yaml myuser/myimage:mytag: - dockerng.image_present: + docker_image.present: - sls: - webapp1 - webapp2 @@ -151,12 +154,14 @@ def present(name, .. versionadded: 2017.7.0 base - Base image with which to start ``dockerng.sls_build`` + Base image with which to start :py:func:`docker.sls_build + ` .. versionadded: 2017.7.0 saltenv - environment from which to pull sls files for ``dockerng.sls_build``. + Environment from which to pull SLS files for :py:func:`docker.sls_build + ` .. versionadded: 2017.7.0 ''' @@ -169,11 +174,14 @@ def present(name, ret['comment'] = 'Only one of \'build\' or \'load\' is permitted.' return ret - # Ensure that we have repo:tag notation image = ':'.join(salt.utils.docker.get_repo_tag(name)) - all_tags = __salt__['docker.list_tags']() + resolved_tag = __salt__['docker.resolve_tag'](image) - if image in all_tags: + if resolved_tag is False: + # Specified image is not present + image_info = None + else: + # Specified image is present if not force: ret['result'] = True ret['comment'] = 'Image \'{0}\' already present'.format(name) @@ -185,8 +193,6 @@ def present(name, ret['comment'] = \ 'Unable to get info for image \'{0}\': {1}'.format(name, exc) return ret - else: - image_info = None if build or sls: action = 'built' @@ -197,15 +203,24 @@ def present(name, if __opts__['test']: ret['result'] = None - if (image in all_tags and force) or image not in all_tags: + if (resolved_tag is not False and force) or resolved_tag is False: ret['comment'] = 'Image \'{0}\' will be {1}'.format(name, action) return ret if build: + # get the functions default value and args + argspec = salt.utils.args.get_function_argspec(__salt__['docker.build']) + # Map any if existing args from kwargs into the build_args dictionary + build_args = dict(list(zip(argspec.args, argspec.defaults))) + for k, v in build_args.items(): + if k in kwargs.get('kwargs', {}): + build_args[k] = kwargs.get('kwargs', {}).get(k) try: - image_update = __salt__['docker.build'](path=build, - image=image, - dockerfile=dockerfile) + # map values passed from the state to the build args + build_args['path'] = build + build_args['image'] = image + build_args['dockerfile'] = dockerfile + image_update = __salt__['docker.build'](**build_args) except Exception as exc: ret['comment'] = ( 'Encountered error building {0} as {1}: {2}' @@ -219,10 +234,10 @@ def present(name, if isinstance(sls, list): sls = ','.join(sls) try: - image_update = __salt__['dockerng.sls_build'](name=image, - base=base, - mods=sls, - saltenv=saltenv) + image_update = __salt__['docker.sls_build'](name=image, + base=base, + mods=sls, + saltenv=saltenv) except Exception as exc: ret['comment'] = ( 'Encountered error using sls {0} for building {1}: {2}' @@ -252,10 +267,8 @@ def present(name, client_timeout=client_timeout ) except Exception as exc: - ret['comment'] = ( - 'Encountered error pulling {0}: {1}' - .format(image, exc) - ) + ret['comment'] = \ + 'Encountered error pulling {0}: {1}'.format(image, exc) return ret if (image_info is not None and image_info['Id'][:12] == image_update .get('Layers', {}) @@ -267,7 +280,7 @@ def present(name, # Only add to the changes dict if layers were pulled ret['changes'] = image_update - ret['result'] = image in __salt__['docker.list_tags']() + ret['result'] = bool(__salt__['docker.resolve_tag'](image)) if not ret['result']: # This shouldn't happen, failure to pull should be caught above @@ -345,23 +358,16 @@ def absent(name=None, images=None, force=False): ret['comment'] = 'One of \'name\' and \'images\' must be provided' return ret elif images is not None: - targets = [] - for target in images: - try: - targets.append(':'.join(salt.utils.docker.get_repo_tag(target))) - except TypeError: - # Don't stomp on images with unicode characters in Python 2, - # only force image to be a str if it wasn't already (which is - # very unlikely). - targets.append(':'.join(salt.utils.docker.get_repo_tag(str(target)))) + targets = images elif name: - try: - targets = [':'.join(salt.utils.docker.get_repo_tag(name))] - except TypeError: - targets = [':'.join(salt.utils.docker.get_repo_tag(str(name)))] + targets = [name] pre_tags = __salt__['docker.list_tags']() - to_delete = [x for x in targets if x in pre_tags] + to_delete = [] + for target in targets: + resolved_tag = __salt__['docker.resolve_tag'](target, tags=pre_tags) + if resolved_tag is not False: + to_delete.append(resolved_tag) log.debug('targets = {0}'.format(targets)) log.debug('to_delete = {0}'.format(to_delete)) diff --git a/salt/states/elasticsearch.py b/salt/states/elasticsearch.py index 2c37a304ce..c5f297db59 100644 --- a/salt/states/elasticsearch.py +++ b/salt/states/elasticsearch.py @@ -230,7 +230,7 @@ def index_template_absent(name): return ret -def index_template_present(name, definition): +def index_template_present(name, definition, check_definition=False): ''' Ensure that the named index templat eis present. @@ -238,6 +238,8 @@ def index_template_present(name, definition): Name of the index to add definition Required dict for creation parameters as per https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html + check_definition + If the template already exists and the definition is up to date **Example:** @@ -270,7 +272,27 @@ def index_template_present(name, definition): ret['result'] = False ret['comment'] = 'Cannot create index template {0}, {1}'.format(name, output) else: - ret['comment'] = 'Index template {0} is already present'.format(name) + if check_definition: + definition_parsed = json.loads(definition) + current_template = __salt__['elasticsearch.index_template_get'](name=name)[name] + diff = __utils__['dictdiffer.deep_diff'](current_template, definition_parsed) + if len(diff) != 0: + if __opts__['test']: + ret['comment'] = 'Index template {0} exist but need to be updated'.format(name) + ret['changes'] = diff + ret['result'] = None + else: + output = __salt__['elasticsearch.index_template_create'](name=name, body=definition) + if output: + ret['comment'] = 'Successfully updated index template {0}'.format(name) + ret['changes'] = diff + else: + ret['result'] = False + ret['comment'] = 'Cannot update index template {0}, {1}'.format(name, output) + else: + ret['comment'] = 'Index template {0} is already present and up to date'.format(name) + else: + ret['comment'] = 'Index template {0} is already present'.format(name) except Exception as e: ret['result'] = False ret['comment'] = str(e) diff --git a/salt/states/esxcluster.py b/salt/states/esxcluster.py new file mode 100644 index 0000000000..77b6eb0ec6 --- /dev/null +++ b/salt/states/esxcluster.py @@ -0,0 +1,538 @@ +# -*- coding: utf-8 -*- +''' +Manage VMware ESXi Clusters. + +Dependencies +============ + +- pyVmomi Python Module + + +pyVmomi +------- + +PyVmomi can be installed via pip: + +.. code-block:: bash + + pip install pyVmomi + +.. note:: + + Version 6.0 of pyVmomi has some problems with SSL error handling on certain + versions of Python. If using version 6.0 of pyVmomi, Python 2.7.9, + or newer must be present. This is due to an upstream dependency + in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the + version of Python is not in the supported range, you will need to install an + earlier version of pyVmomi. See `Issue #29537`_ for more information. + +.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 + +Based on the note above, to install an earlier version of pyVmomi than the +version currently listed in PyPi, run the following: + +.. code-block:: bash + + pip install pyVmomi==5.5.0.2014.1.1 + +The 5.5.0.2014.1.1 is a known stable version that this original ESXi State +Module was developed against. +''' + +# Import Python Libs +from __future__ import absolute_import +import logging +import traceback +import sys + +# Import Salt Libs +import salt.exceptions +from salt.utils.dictdiffer import recursive_diff +from salt.utils.listdiffer import list_diff +from salt.config.schemas.esxcluster import ESXClusterConfigSchema, \ + LicenseSchema +from salt.utils import dictupdate + +# External libraries +try: + import jsonschema + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False + +try: + from pyVmomi import VmomiSupport + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + +# Get Logging Started +log = logging.getLogger(__name__) + + +def __virtual__(): + if not HAS_JSONSCHEMA: + return False, 'State module did not load: jsonschema not found' + if not HAS_PYVMOMI: + return False, 'State module did not load: pyVmomi not found' + + # We check the supported vim versions to infer the pyVmomi version + if 'vim25/6.0' in VmomiSupport.versionMap and \ + sys.version_info > (2, 7) and sys.version_info < (2, 7, 9): + + return False, ('State module did not load: Incompatible versions ' + 'of Python and pyVmomi present. See Issue #29537.') + return True + + +def mod_init(low): + ''' + Retrieves and adapt the login credentials from the proxy connection module + ''' + return True + + +def _get_vsan_datastore(si, cluster_name): + '''Retrieves the vsan_datastore''' + + log.trace('Retrieving vsan datastore') + vsan_datastores = [ds for ds in + __salt__['vsphere.list_datastores_via_proxy']( + service_instance=si) + if ds['type'] == 'vsan'] + + if not vsan_datastores: + raise salt.exceptions.VMwareObjectRetrievalError( + 'No vSAN datastores where retrieved for cluster ' + '\'{0}\''.format(cluster_name)) + return vsan_datastores[0] + + +def cluster_configured(name, cluster_config): + ''' + Configures a cluster. Creates a new cluster, if it doesn't exist on the + vCenter or reconfigures it if configured differently + + Supported proxies: esxdatacenter, esxcluster + + name + Name of the state. If the state is run in by an ``esxdatacenter`` + proxy, it will be the name of the cluster. + + cluster_config + Configuration applied to the cluster. + Complex datastructure following the ESXClusterConfigSchema. + Valid example is: + + .. code-block::yaml + + drs: + default_vm_behavior: fullyAutomated + enabled: true + vmotion_rate: 3 + ha: + admission_control + _enabled: false + default_vm_settings: + isolation_response: powerOff + restart_priority: medium + enabled: true + hb_ds_candidate_policy: userSelectedDs + host_monitoring: enabled + options: + - key: das.ignoreinsufficienthbdatastore + value: 'true' + vm_monitoring: vmMonitoringDisabled + vm_swap_placement: vmDirectory + vsan: + auto_claim_storage: false + compression_enabled: true + dedup_enabled: true + enabled: true + + ''' + proxy_type = __salt__['vsphere.get_proxy_type']() + if proxy_type == 'esxdatacenter': + cluster_name, datacenter_name = \ + name, __salt__['esxdatacenter.get_details']()['datacenter'] + elif proxy_type == 'esxcluster': + cluster_name, datacenter_name = \ + __salt__['esxcluster.get_details']()['cluster'], \ + __salt__['esxcluster.get_details']()['datacenter'] + else: + raise salt.exceptions.CommandExecutionError('Unsupported proxy {0}' + ''.format(proxy_type)) + log.info('Running {0} for cluster \'{1}\' in datacenter ' + '\'{2}\''.format(name, cluster_name, datacenter_name)) + cluster_dict = cluster_config + log.trace('cluster_dict = {0}'.format(cluster_dict)) + changes_required = False + ret = {'name': name, + 'changes': {}, 'result': None, 'comment': 'Default'} + comments = [] + changes = {} + changes_required = False + + try: + log.trace('Validating cluster_configured state input') + schema = ESXClusterConfigSchema.serialize() + log.trace('schema = {0}'.format(schema)) + try: + jsonschema.validate(cluster_dict, schema) + except jsonschema.exceptions.ValidationError as exc: + raise salt.exceptions.InvalidESXClusterPayloadError(exc) + current = None + si = __salt__['vsphere.get_service_instance_via_proxy']() + try: + current = __salt__['vsphere.list_cluster'](datacenter_name, + cluster_name, + service_instance=si) + except salt.exceptions.VMwareObjectRetrievalError: + changes_required = True + if __opts__['test']: + comments.append('State {0} will create cluster ' + '\'{1}\' in datacenter \'{2}\'.' + ''.format(name, cluster_name, datacenter_name)) + log.info(comments[-1]) + __salt__['vsphere.disconnect'](si) + ret.update({'result': None, + 'comment': '\n'.join(comments)}) + return ret + log.trace('Creating cluster \'{0}\' in datacenter \'{1}\'. ' + ''.format(cluster_name, datacenter_name)) + __salt__['vsphere.create_cluster'](cluster_dict, + datacenter_name, + cluster_name, + service_instance=si) + comments.append('Created cluster \'{0}\' in datacenter \'{1}\'' + ''.format(cluster_name, datacenter_name)) + log.info(comments[-1]) + changes.update({'new': cluster_dict}) + if current: + # Cluster already exists + # We need to handle lists sepparately + ldiff = None + if 'ha' in cluster_dict and 'options' in cluster_dict['ha']: + ldiff = list_diff(current.get('ha', {}).get('options', []), + cluster_dict.get('ha', {}).get('options', []), + 'key') + log.trace('options diffs = {0}'.format(ldiff.diffs)) + # Remove options if exist + del cluster_dict['ha']['options'] + if 'ha' in current and 'options' in current['ha']: + del current['ha']['options'] + diff = recursive_diff(current, cluster_dict) + log.trace('diffs = {0}'.format(diff.diffs)) + if not (diff.diffs or (ldiff and ldiff.diffs)): + # No differences + comments.append('Cluster \'{0}\' in datacenter \'{1}\' is up ' + 'to date. Nothing to be done.' + ''.format(cluster_name, datacenter_name)) + log.info(comments[-1]) + else: + changes_required = True + changes_str = '' + if diff.diffs: + changes_str = '{0}{1}'.format(changes_str, + diff.changes_str) + if ldiff and ldiff.diffs: + changes_str = '{0}\nha:\n options:\n{1}'.format( + changes_str, + '\n'.join([' {0}'.format(l) for l in + ldiff.changes_str2.split('\n')])) + # Apply the changes + if __opts__['test']: + comments.append( + 'State {0} will update cluster \'{1}\' ' + 'in datacenter \'{2}\':\n{3}' + ''.format(name, cluster_name, + datacenter_name, changes_str)) + else: + new_values = diff.new_values + old_values = diff.old_values + if ldiff and ldiff.new_values: + dictupdate.update( + new_values, {'ha': {'options': ldiff.new_values}}) + if ldiff and ldiff.old_values: + dictupdate.update( + old_values, {'ha': {'options': ldiff.old_values}}) + log.trace('new_values = {0}'.format(new_values)) + __salt__['vsphere.update_cluster'](new_values, + datacenter_name, + cluster_name, + service_instance=si) + comments.append('Updated cluster \'{0}\' in datacenter ' + '\'{1}\''.format(cluster_name, + datacenter_name)) + log.info(comments[-1]) + changes.update({'new': new_values, + 'old': old_values}) + __salt__['vsphere.disconnect'](si) + ret_status = True + if __opts__['test'] and changes_required: + ret_status = None + ret.update({'result': ret_status, + 'comment': '\n'.join(comments), + 'changes': changes}) + return ret + except salt.exceptions.CommandExecutionError as exc: + log.error('Error: {0}\n{1}'.format(exc, traceback.format_exc())) + if si: + __salt__['vsphere.disconnect'](si) + ret.update({ + 'result': False, + 'comment': str(exc)}) + return ret + + +def vsan_datastore_configured(name, datastore_name): + ''' + Configures the cluster's VSAN datastore + + WARNING: The VSAN datastore is created automatically after the first + ESXi host is added to the cluster; the state assumes that the datastore + exists and errors if it doesn't. + ''' + + cluster_name, datacenter_name = \ + __salt__['esxcluster.get_details']()['cluster'], \ + __salt__['esxcluster.get_details']()['datacenter'] + display_name = '{0}/{1}'.format(datacenter_name, cluster_name) + log.info('Running vsan_datastore_configured for ' + '\'{0}\''.format(display_name)) + ret = {'name': name, + 'changes': {}, 'result': None, + 'comment': 'Default'} + comments = [] + changes = {} + changes_required = False + + try: + si = __salt__['vsphere.get_service_instance_via_proxy']() + # Checking if we need to rename the vsan datastore + vsan_ds = _get_vsan_datastore(si, cluster_name) + if vsan_ds['name'] == datastore_name: + comments.append('vSAN datastore is correctly named \'{0}\'. ' + 'Nothing to be done.'.format(vsan_ds['name'])) + log.info(comments[-1]) + else: + # vsan_ds needs to be updated + changes_required = True + if __opts__['test']: + comments.append('State {0} will rename the vSAN datastore to ' + '\'{1}\'.'.format(name, datastore_name)) + log.info(comments[-1]) + else: + log.trace('Renaming vSAN datastore \'{0}\' to \'{1}\'' + ''.format(vsan_ds['name'], datastore_name)) + __salt__['vsphere.rename_datastore']( + datastore_name=vsan_ds['name'], + new_datastore_name=datastore_name, + service_instance=si) + comments.append('Renamed vSAN datastore to \'{0}\'.' + ''.format(datastore_name)) + changes = {'vsan_datastore': {'new': {'name': datastore_name}, + 'old': {'name': vsan_ds['name']}}} + log.info(comments[-1]) + __salt__['vsphere.disconnect'](si) + + ret.update({'result': True if (not changes_required) else None if + __opts__['test'] else True, + 'comment': '\n'.join(comments), + 'changes': changes}) + return ret + except salt.exceptions.CommandExecutionError as exc: + log.error('Error: {0}\n{1}'.format(exc, traceback.format_exc())) + if si: + __salt__['vsphere.disconnect'](si) + ret.update({ + 'result': False, + 'comment': exc.strerror}) + return ret + + +def licenses_configured(name, licenses=None): + ''' + Configures licenses on the cluster entity + + Checks if each license exists on the server: + - if it doesn't, it creates it + Check if license is assigned to the cluster: + - if it's not assigned to the cluster: + - assign it to the cluster if there is space + - error if there's no space + - if it's assigned to the cluster nothing needs to be done + ''' + ret = {'name': name, + 'changes': {}, + 'result': None, + 'comment': 'Default'} + if not licenses: + raise salt.exceptions.ArgumentValueError('No licenses provided') + cluster_name, datacenter_name = \ + __salt__['esxcluster.get_details']()['cluster'], \ + __salt__['esxcluster.get_details']()['datacenter'] + display_name = '{0}/{1}'.format(datacenter_name, cluster_name) + log.info('Running licenses configured for \'{0}\''.format(display_name)) + log.trace('licenses = {0}'.format(licenses)) + entity = {'type': 'cluster', + 'datacenter': datacenter_name, + 'cluster': cluster_name} + log.trace('entity = {0}'.format(entity)) + + comments = [] + changes = {} + old_licenses = [] + new_licenses = [] + has_errors = False + needs_changes = False + try: + # Validate licenses + log.trace('Validating licenses') + schema = LicenseSchema.serialize() + try: + jsonschema.validate({'licenses': licenses}, schema) + except jsonschema.exceptions.ValidationError as exc: + raise salt.exceptions.InvalidLicenseError(exc) + + si = __salt__['vsphere.get_service_instance_via_proxy']() + # Retrieve licenses + existing_licenses = __salt__['vsphere.list_licenses']( + service_instance=si) + remaining_licenses = existing_licenses[:] + # Cycle through licenses + for license_name, license in licenses.items(): + # Check if license already exists + filtered_licenses = [l for l in existing_licenses + if l['key'] == license] + # TODO Update license description - not of interest right now + if not filtered_licenses: + # License doesn't exist - add and assign to cluster + needs_changes = True + if __opts__['test']: + # If it doesn't exist it clearly needs to be assigned as + # well so we can stop the check here + comments.append('State {0} will add license \'{1}\', ' + 'and assign it to cluster \'{2}\'.' + ''.format(name, license_name, display_name)) + log.info(comments[-1]) + continue + else: + try: + existing_license = __salt__['vsphere.add_license']( + key=license, description=license_name, + service_instance=si) + except salt.exceptions.VMwareApiError as ex: + comments.append(ex.err_msg) + log.error(comments[-1]) + has_errors = True + continue + comments.append('Added license \'{0}\'.' + ''.format(license_name)) + log.info(comments[-1]) + else: + # License exists let's check if it's assigned to the cluster + comments.append('License \'{0}\' already exists. ' + 'Nothing to be done.'.format(license_name)) + log.info(comments[-1]) + existing_license = filtered_licenses[0] + + log.trace('Checking licensed entities...') + assigned_licenses = __salt__['vsphere.list_assigned_licenses']( + entity=entity, + entity_display_name=display_name, + service_instance=si) + + # Checking if any of the licenses already assigned have the same + # name as the new license; the already assigned license would be + # replaced by the new license + # + # Licenses with different names but matching features would be + # replaced as well, but searching for those would be very complex + # + # the name check if good enough for now + already_assigned_license = assigned_licenses[0] if \ + assigned_licenses else None + + if already_assigned_license and \ + already_assigned_license['key'] == license: + + # License is already assigned to entity + comments.append('License \'{0}\' already assigned to ' + 'cluster \'{1}\'. Nothing to be done.' + ''.format(license_name, display_name)) + log.info(comments[-1]) + continue + + needs_changes = True + # License needs to be assigned to entity + + if existing_license['capacity'] <= existing_license['used']: + # License is already fully used + comments.append('Cannot assign license \'{0}\' to cluster ' + '\'{1}\'. No free capacity available.' + ''.format(license_name, display_name)) + log.error(comments[-1]) + has_errors = True + continue + + # Assign license + if __opts__['test']: + comments.append('State {0} will assign license \'{1}\' ' + 'to cluster \'{2}\'.'.format( + name, license_name, display_name)) + log.info(comments[-1]) + else: + try: + __salt__['vsphere.assign_license']( + license_key=license, + license_name=license_name, + entity=entity, + entity_display_name=display_name, + service_instance=si) + except salt.exceptions.VMwareApiError as ex: + comments.append(ex.err_msg) + log.error(comments[-1]) + has_errors = True + continue + comments.append('Assigned license \'{0}\' to cluster \'{1}\'.' + ''.format(license_name, display_name)) + log.info(comments[-1]) + # Note: Because the already_assigned_license was retrieved + # from the assignment license manager it doesn't have a used + # value - that's a limitation from VMware. The license would + # need to be retrieved again from the license manager to get + # the value + + # Hide license keys + assigned_license = __salt__['vsphere.list_assigned_licenses']( + entity=entity, + entity_display_name=display_name, + service_instance=si)[0] + assigned_license['key'] = '' + if already_assigned_license: + already_assigned_license['key'] = '' + if already_assigned_license and \ + already_assigned_license['capacity'] == sys.maxsize: + + already_assigned_license['capacity'] = 'Unlimited' + + changes[license_name] = {'new': assigned_license, + 'old': already_assigned_license} + continue + __salt__['vsphere.disconnect'](si) + + ret.update({'result': True if (not needs_changes) else None if + __opts__['test'] else False if has_errors else True, + 'comment': '\n'.join(comments), + 'changes': changes if not __opts__['test'] else {}}) + + return ret + except salt.exceptions.CommandExecutionError as exc: + log.error('Error: {0}\n{1}'.format(exc, traceback.format_exc())) + if si: + __salt__['vsphere.disconnect'](si) + ret.update({ + 'result': False, + 'comment': exc.strerror}) + return ret diff --git a/salt/states/file.py b/salt/states/file.py index c8a8a185f1..1d89feb295 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -299,6 +299,7 @@ if salt.utils.platform.is_windows(): # Import 3rd-party libs from salt.ext import six from salt.ext.six.moves import zip_longest +from salt.ext.six.moves.urllib.parse import urlparse as _urlparse # pylint: disable=no-name-in-module if salt.utils.platform.is_windows(): import pywintypes import win32com.client @@ -1530,6 +1531,7 @@ def managed(name, source=None, source_hash='', source_hash_name=None, + keep_source=True, user=None, group=None, mode=None, @@ -1565,7 +1567,7 @@ def managed(name, the salt master and potentially run through a templating system. name - The location of the file to manage + The location of the file to manage, as an absolute path. source The source file to download to the minion, this source file can be @@ -1729,19 +1731,30 @@ def managed(name, .. versionadded:: 2016.3.5 + keep_source : True + Set to ``False`` to discard the cached copy of the source file once the + state completes. This can be useful for larger files to keep them from + taking up space in minion cache. However, keep in mind that discarding + the source file will result in the state needing to re-download the + source file if the state is run again. + + .. versionadded:: 2017.7.3 + user The user to own the file, this defaults to the user salt is running as on the minion group The group ownership set for the file, this defaults to the group salt - is running as on the minion On Windows, this is ignored + is running as on the minion. On Windows, this is ignored mode - The permissions to set on this file, e.g. ``644``, ``0775``, or ``4664``. + The permissions to set on this file, e.g. ``644``, ``0775``, or + ``4664``. - The default mode for new files and directories corresponds umask of salt - process. For existing files and directories it's not enforced. + The default mode for new files and directories corresponds to the + umask of the salt process. The mode of existing files and directories + will only be changed if ``mode`` is specified. .. note:: This option is **not** supported on Windows. @@ -2082,12 +2095,7 @@ def managed(name, - win_inheritance: False ''' if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') name = os.path.expanduser(name) @@ -2443,8 +2451,9 @@ def managed(name, except Exception as exc: ret['changes'] = {} log.debug(traceback.format_exc()) - if os.path.isfile(tmp_filename): - os.remove(tmp_filename) + salt.utils.files.remove(tmp_filename) + if not keep_source and sfn: + salt.utils.files.remove(sfn) return _error(ret, 'Unable to check_cmd file: {0}'.format(exc)) # file being updated to verify using check_cmd @@ -2462,15 +2471,9 @@ def managed(name, cret = mod_run_check_cmd(check_cmd, tmp_filename, **check_cmd_opts) if isinstance(cret, dict): ret.update(cret) - if os.path.isfile(tmp_filename): - os.remove(tmp_filename) - if sfn and os.path.isfile(sfn): - os.remove(sfn) + salt.utils.files.remove(tmp_filename) return ret - if sfn and os.path.isfile(sfn): - os.remove(sfn) - # Since we generated a new tempfile and we are not returning here # lets change the original sfn to the new tempfile or else we will # get file not found @@ -2519,10 +2522,10 @@ def managed(name, log.debug(traceback.format_exc()) return _error(ret, 'Unable to manage file: {0}'.format(exc)) finally: - if tmp_filename and os.path.isfile(tmp_filename): - os.remove(tmp_filename) - if sfn and os.path.isfile(sfn): - os.remove(sfn) + if tmp_filename: + salt.utils.files.remove(tmp_filename) + if not keep_source and sfn: + salt.utils.files.remove(sfn) _RECURSE_TYPES = ['user', 'group', 'mode', 'ignore_files', 'ignore_dirs'] @@ -2589,7 +2592,7 @@ def directory(name, Ensure that a named directory is present and has the right perms name - The location to create or manage a directory + The location to create or manage a directory, as an absolute path user The user to own the directory; this defaults to the user salt is @@ -3051,6 +3054,7 @@ def directory(name, def recurse(name, source, + keep_source=True, clean=False, require=None, user=None, @@ -3060,6 +3064,7 @@ def recurse(name, sym_mode=None, template=None, context=None, + replace=True, defaults=None, include_empty=False, backup='', @@ -3082,6 +3087,15 @@ def recurse(name, located on the master in the directory named spam, and is called eggs, the source string is salt://spam/eggs + keep_source : True + Set to ``False`` to discard the cached copy of the source file once the + state completes. This can be useful for larger files to keep them from + taking up space in minion cache. However, keep in mind that discarding + the source file will result in the state needing to re-download the + source file if the state is run again. + + .. versionadded:: 2017.7.3 + clean Make sure that only files that are set up by salt and required by this function are kept. If this option is set then everything in this @@ -3148,6 +3162,11 @@ def recurse(name, The template option is required when recursively applying templates. + replace : True + If set to ``False`` and the file already exists, the file will not be + modified even if changes would otherwise be made. Permissions and + ownership will still be enforced, however. + context Overrides default context variables passed to the template. @@ -3215,12 +3234,7 @@ def recurse(name, option is usually not needed except in special circumstances. ''' if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') name = os.path.expanduser(sdecode(name)) @@ -3337,8 +3351,8 @@ def recurse(name, if _ret['changes']: ret['changes'][path] = _ret['changes'] - def manage_file(path, source): - if clean and os.path.exists(path) and os.path.isdir(path): + def manage_file(path, source, replace): + if clean and os.path.exists(path) and os.path.isdir(path) and replace: _ret = {'name': name, 'changes': {}, 'result': True, 'comment': ''} if __opts__['test']: _ret['comment'] = u'Replacing directory {0} with a ' \ @@ -3362,12 +3376,14 @@ def recurse(name, _ret = managed( path, source=source, + keep_source=keep_source, user=user, group=group, mode='keep' if keep_mode else file_mode, attrs=None, template=template, makedirs=True, + replace=replace, context=context, defaults=defaults, backup=backup, @@ -3424,7 +3440,7 @@ def recurse(name, for dirname in mng_dirs: manage_directory(dirname) for dest, src in mng_files: - manage_file(dest, src) + manage_file(dest, src, replace) if clean: # TODO: Use directory(clean=True) instead @@ -4369,7 +4385,7 @@ def comment(name, regex, char='#', backup='.bak'): ret['result'] = __salt__['file.search'](name, unanchor_regex, multiline=True) if slines != nlines: - if not salt.utils.istextfile(name): + if not __utils__['files.is_text_file'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4481,7 +4497,7 @@ def uncomment(name, regex, char='#', backup='.bak'): ) if slines != nlines: - if not salt.utils.istextfile(name): + if not __utils__['files.is_text_file'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4724,7 +4740,7 @@ def append(name, nlines = list(slines) nlines.extend(append_lines) if slines != nlines: - if not salt.utils.istextfile(name): + if not __utils__['files.is_text_file'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4749,7 +4765,7 @@ def append(name, nlines = nlines.splitlines() if slines != nlines: - if not salt.utils.istextfile(name): + if not __utils__['files.is_text_file'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4917,7 +4933,7 @@ def prepend(name, if __opts__['test']: nlines = test_lines + slines if slines != nlines: - if not salt.utils.istextfile(name): + if not __utils__['files.is_text_file'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4960,7 +4976,7 @@ def prepend(name, nlines = nlines.splitlines(True) if slines != nlines: - if not salt.utils.istextfile(name): + if not __utils__['files.is_text_file'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -5036,12 +5052,7 @@ def patch(name, hash_ = kwargs.pop('hash', None) if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') name = os.path.expanduser(name) @@ -5694,12 +5705,7 @@ def serialize(name, } ''' if 'env' in kwargs: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop('env') name = os.path.expanduser(name) @@ -6437,3 +6443,314 @@ def shortcut( ret['comment'] += (', but was unable to set ownership to ' '{0}'.format(user)) return ret + + +def cached(name, + source_hash='', + source_hash_name=None, + skip_verify=False, + saltenv='base'): + ''' + .. versionadded:: 2017.7.3 + + Ensures that a file is saved to the minion's cache. This state is primarily + invoked by other states to ensure that we do not re-download a source file + if we do not need to. + + name + The URL of the file to be cached. To cache a file from an environment + other than ``base``, either use the ``saltenv`` argument or include the + saltenv in the URL (e.g. ``salt://path/to/file.conf?saltenv=dev``). + + .. note:: + A list of URLs is not supported, this must be a single URL. If a + local file is passed here, then the state will obviously not try to + download anything, but it will compare a hash if one is specified. + + source_hash + See the documentation for this same argument in the + :py:func:`file.managed ` state. + + .. note:: + For remote files not originating from the ``salt://`` fileserver, + such as http(s) or ftp servers, this state will not re-download the + file if the locally-cached copy matches this hash. This is done to + prevent unnecessary downloading on repeated runs of this state. To + update the cached copy of a file, it is necessary to update this + hash. + + source_hash_name + See the documentation for this same argument in the + :py:func:`file.managed ` state. + + skip_verify + See the documentation for this same argument in the + :py:func:`file.managed ` state. + + .. note:: + Setting this to ``True`` will result in a copy of the file being + downloaded from a remote (http(s), ftp, etc.) source each time the + state is run. + + saltenv + Used to specify the environment from which to download a file from the + Salt fileserver (i.e. those with ``salt://`` URL). + + + This state will in most cases not be useful in SLS files, but it is useful + when writing a state or remote-execution module that needs to make sure + that a file at a given URL has been downloaded to the cachedir. One example + of this is in the :py:func:`archive.extracted ` + state: + + .. code-block:: python + + result = __states__['file.cached'](source_match, + source_hash=source_hash, + source_hash_name=source_hash_name, + skip_verify=skip_verify, + saltenv=__env__) + + This will return a dictionary containing the state's return data, including + a ``result`` key which will state whether or not the state was successful. + Note that this will not catch exceptions, so it is best used within a + try/except. + + Once this state has been run from within another state or remote-execution + module, the actual location of the cached file can be obtained using + :py:func:`cp.is_cached `: + + .. code-block:: python + + cached = __salt__['cp.is_cached'](source_match) + + This function will return the cached path of the file, or an empty string + if the file is not present in the minion cache. + ''' + ret = {'changes': {}, + 'comment': '', + 'name': name, + 'result': False} + + try: + parsed = _urlparse(name) + except Exception: + ret['comment'] = 'Only URLs or local file paths are valid input' + return ret + + # This if statement will keep the state from proceeding if a remote source + # is specified and no source_hash is presented (unless we're skipping hash + # verification). + if not skip_verify \ + and not source_hash \ + and parsed.scheme in salt.utils.files.REMOTE_PROTOS: + ret['comment'] = ( + 'Unable to verify upstream hash of source file {0}, please set ' + 'source_hash or set skip_verify to True'.format(name) + ) + return ret + + if source_hash: + # Get the hash and hash type from the input. This takes care of parsing + # the hash out of a file containing checksums, if that is how the + # source_hash was specified. + try: + source_sum = __salt__['file.get_source_sum']( + source=name, + source_hash=source_hash, + source_hash_name=source_hash_name, + saltenv=saltenv) + except CommandExecutionError as exc: + ret['comment'] = exc.strerror + return ret + else: + if not source_sum: + # We shouldn't get here, problems in retrieving the hash in + # file.get_source_sum should result in a CommandExecutionError + # being raised, which we catch above. Nevertheless, we should + # provide useful information in the event that + # file.get_source_sum regresses. + ret['comment'] = ( + 'Failed to get source hash from {0}. This may be a bug. ' + 'If this error persists, please report it and set ' + 'skip_verify to True to work around it.'.format(source_hash) + ) + return ret + else: + source_sum = {} + + if parsed.scheme in salt.utils.files.LOCAL_PROTOS: + # Source is a local file path + full_path = os.path.realpath(os.path.expanduser(parsed.path)) + if os.path.exists(full_path): + if not skip_verify and source_sum: + # Enforce the hash + local_hash = __salt__['file.get_hash']( + full_path, + source_sum.get('hash_type', __opts__['hash_type'])) + if local_hash == source_sum['hsum']: + ret['result'] = True + ret['comment'] = ( + 'File {0} is present on the minion and has hash ' + '{1}'.format(full_path, local_hash) + ) + else: + ret['comment'] = ( + 'File {0} is present on the minion, but the hash ({1}) ' + 'does not match the specified hash ({2})'.format( + full_path, local_hash, source_sum['hsum'] + ) + ) + return ret + else: + ret['result'] = True + ret['comment'] = 'File {0} is present on the minion'.format( + full_path + ) + return ret + else: + ret['comment'] = 'File {0} is not present on the minion'.format( + full_path + ) + return ret + + local_copy = __salt__['cp.is_cached'](name, saltenv=saltenv) + + if local_copy: + # File is already cached + pre_hash = __salt__['file.get_hash']( + local_copy, + source_sum.get('hash_type', __opts__['hash_type'])) + + if not skip_verify and source_sum: + # Get the local copy's hash to compare with the hash that was + # specified via source_hash. If it matches, we can exit early from + # the state without going any further, because the file is cached + # with the correct hash. + if pre_hash == source_sum['hsum']: + ret['result'] = True + ret['comment'] = ( + 'File is already cached to {0} with hash {1}'.format( + local_copy, pre_hash + ) + ) + else: + pre_hash = None + + # Cache the file. Note that this will not actually download the file if + # either of the following is true: + # 1. source is a salt:// URL and the fileserver determines that the hash + # of the minion's copy matches that of the fileserver. + # 2. File is remote (http(s), ftp, etc.) and the specified source_hash + # matches the cached copy. + # Remote, non salt:// sources _will_ download if a copy of the file was + # not already present in the minion cache. + try: + local_copy = __salt__['cp.cache_file']( + name, + saltenv=saltenv, + source_hash=source_sum.get('hsum')) + except Exception as exc: + ret['comment'] = exc.__str__() + return ret + + if not local_copy: + ret['comment'] = ( + 'Failed to cache {0}, check minion log for more ' + 'information'.format(name) + ) + return ret + + post_hash = __salt__['file.get_hash']( + local_copy, + source_sum.get('hash_type', __opts__['hash_type'])) + + if pre_hash != post_hash: + ret['changes']['hash'] = {'old': pre_hash, 'new': post_hash} + + # Check the hash, if we're enforcing one. Note that this will be the first + # hash check if the file was not previously cached, and the 2nd hash check + # if it was cached and the + if not skip_verify and source_sum: + if post_hash == source_sum['hsum']: + ret['result'] = True + ret['comment'] = ( + 'File is already cached to {0} with hash {1}'.format( + local_copy, post_hash + ) + ) + else: + ret['comment'] = ( + 'File is cached to {0}, but the hash ({1}) does not match ' + 'the specified hash ({2})'.format( + local_copy, post_hash, source_sum['hsum'] + ) + ) + return ret + + # We're not enforcing a hash, and we already know that the file was + # successfully cached, so we know the state was successful. + ret['result'] = True + ret['comment'] = 'File is cached to {0}'.format(local_copy) + return ret + + +def not_cached(name, saltenv='base'): + ''' + .. versionadded:: 2017.7.3 + + Ensures that a file is saved to the minion's cache. This state is primarily + invoked by other states to ensure that we do not re-download a source file + if we do not need to. + + name + The URL of the file to be cached. To cache a file from an environment + other than ``base``, either use the ``saltenv`` argument or include the + saltenv in the URL (e.g. ``salt://path/to/file.conf?saltenv=dev``). + + .. note:: + A list of URLs is not supported, this must be a single URL. If a + local file is passed here, the state will take no action. + + saltenv + Used to specify the environment from which to download a file from the + Salt fileserver (i.e. those with ``salt://`` URL). + ''' + ret = {'changes': {}, + 'comment': '', + 'name': name, + 'result': False} + + try: + parsed = _urlparse(name) + except Exception: + ret['comment'] = 'Only URLs or local file paths are valid input' + return ret + else: + if parsed.scheme in salt.utils.files.LOCAL_PROTOS: + full_path = os.path.realpath(os.path.expanduser(parsed.path)) + ret['result'] = True + ret['comment'] = ( + 'File {0} is a local path, no action taken'.format( + full_path + ) + ) + return ret + + local_copy = __salt__['cp.is_cached'](name, saltenv=saltenv) + + if local_copy: + try: + os.remove(local_copy) + except Exception as exc: + ret['comment'] = 'Failed to delete {0}: {1}'.format( + local_copy, exc.__str__() + ) + else: + ret['result'] = True + ret['changes']['deleted'] = True + ret['comment'] = '{0} was deleted'.format(local_copy) + else: + ret['result'] = True + ret['comment'] = '{0} is not cached'.format(name) + return ret diff --git a/salt/states/git.py b/salt/states/git.py index df6f94ab8c..dc94570d75 100644 --- a/salt/states/git.py +++ b/salt/states/git.py @@ -1303,6 +1303,23 @@ def latest(name, 'if it does not already exist).', comments ) + remote_tags = set([ + x.replace('refs/tags/', '') for x in __salt__['git.ls_remote']( + cwd=target, + remote=remote, + opts="--tags", + user=user, + password=password, + identity=identity, + saltenv=__env__, + ignore_retcode=True, + ).keys() if '^{}' not in x + ]) + if set(all_local_tags) != remote_tags: + has_remote_rev = False + ret['changes']['new_tags'] = list(remote_tags.symmetric_difference( + all_local_tags + )) if not has_remote_rev: try: diff --git a/salt/states/iptables.py b/salt/states/iptables.py index ec4eec4e96..6163e79950 100644 --- a/salt/states/iptables.py +++ b/salt/states/iptables.py @@ -194,7 +194,6 @@ at some point be deprecated in favor of a more generic ``firewall`` state. from __future__ import absolute_import # Import salt libs -import salt.utils from salt.state import STATE_INTERNAL_KEYWORDS as _STATE_INTERNAL_KEYWORDS @@ -810,7 +809,7 @@ def mod_aggregate(low, chunks, running): if low.get('fun') not in agg_enabled: return low for chunk in chunks: - tag = salt.utils.gen_state_tag(chunk) + tag = __utils__['state.gen_tag'](chunk) if tag in running: # Already ran the iptables state, skip aggregation continue diff --git a/salt/states/linux_acl.py b/salt/states/linux_acl.py index 0f733f31c2..4004cef26e 100644 --- a/salt/states/linux_acl.py +++ b/salt/states/linux_acl.py @@ -2,6 +2,8 @@ ''' Linux File Access Control Lists +The Linux ACL state module requires the `getfacl` and `setfacl` binaries. + Ensure a Linux ACL is present .. code-block:: yaml @@ -50,7 +52,7 @@ def __virtual__(): if salt.utils.path.which('getfacl') and salt.utils.path.which('setfacl'): return __virtualname__ - return False + return False, 'The linux_acl state cannot be loaded: the getfacl or setfacl binary is not in the path.' def present(name, acl_type, acl_name='', perms='', recurse=False): @@ -85,11 +87,12 @@ def present(name, acl_type, acl_name='', perms='', recurse=False): # applied to the user/group that owns the file, e.g., # default:group::rwx would be listed as default:group:root:rwx # In this case, if acl_name is empty, we really want to search for root + # but still uses '' for other # We search through the dictionary getfacl returns for the owner of the # file if acl_name is empty. if acl_name == '': - _search_name = __current_perms[name].get('comment').get(_acl_type) + _search_name = __current_perms[name].get('comment').get(_acl_type, '') else: _search_name = acl_name @@ -187,11 +190,12 @@ def absent(name, acl_type, acl_name='', perms='', recurse=False): # applied to the user/group that owns the file, e.g., # default:group::rwx would be listed as default:group:root:rwx # In this case, if acl_name is empty, we really want to search for root + # but still uses '' for other # We search through the dictionary getfacl returns for the owner of the # file if acl_name is empty. if acl_name == '': - _search_name = __current_perms[name].get('comment').get(_acl_type) + _search_name = __current_perms[name].get('comment').get(_acl_type, '') else: _search_name = acl_name diff --git a/salt/states/lvm.py b/salt/states/lvm.py index ffc9054aab..9a2378fe46 100644 --- a/salt/states/lvm.py +++ b/salt/states/lvm.py @@ -268,7 +268,7 @@ def lv_present(name, else: lvpath = '/dev/{0}/{1}'.format(vgname, name) - if __salt__['lvm.lvdisplay'](lvpath): + if __salt__['lvm.lvdisplay'](lvpath, quiet=True): ret['comment'] = 'Logical Volume {0} already present'.format(name) elif __opts__['test']: ret['comment'] = 'Logical Volume {0} is set to be created'.format(name) diff --git a/salt/states/mdadm.py b/salt/states/mdadm.py index 2b1c834087..8981a15dbe 100644 --- a/salt/states/mdadm.py +++ b/salt/states/mdadm.py @@ -25,9 +25,6 @@ import logging # Import salt libs import salt.utils.path -# Import 3rd-party libs -from salt.ext import six - # Set up logger log = logging.getLogger(__name__) @@ -88,69 +85,127 @@ def present(name, # Device exists raids = __salt__['raid.list']() - if raids.get(name): - ret['comment'] = 'Raid {0} already present'.format(name) - return ret + present = raids.get(name) # Decide whether to create or assemble - can_assemble = {} - for dev in devices: - # mdadm -E exits with 0 iff all devices given are part of an array - cmd = 'mdadm -E {0}'.format(dev) - can_assemble[dev] = __salt__['cmd.retcode'](cmd) == 0 + missing = [] + uuid_dict = {} + new_devices = [] - if True in six.itervalues(can_assemble) and False in six.itervalues(can_assemble): - in_raid = sorted([x[0] for x in six.iteritems(can_assemble) if x[1]]) - not_in_raid = sorted([x[0] for x in six.iteritems(can_assemble) if not x[1]]) - ret['comment'] = 'Devices are a mix of RAID constituents ({0}) and '\ - 'non-RAID-constituents({1}).'.format(in_raid, not_in_raid) + for dev in devices: + if dev == 'missing' or not __salt__['file.access'](dev, 'f'): + missing.append(dev) + continue + superblock = __salt__['raid.examine'](dev) + + if 'MD_UUID' in superblock: + uuid = superblock['MD_UUID'] + if uuid not in uuid_dict: + uuid_dict[uuid] = [] + uuid_dict[uuid].append(dev) + else: + new_devices.append(dev) + + if len(uuid_dict) > 1: + ret['comment'] = 'Devices are a mix of RAID constituents with multiple MD_UUIDs: {0}.'.format( + sorted(uuid_dict.keys())) ret['result'] = False return ret - elif next(six.itervalues(can_assemble)): + elif len(uuid_dict) == 1: + uuid = list(uuid_dict.keys())[0] + if present and present['uuid'] != uuid: + ret['comment'] = 'Devices MD_UUIDs: {0} differs from present RAID uuid {1}.'.format(uuid, present['uuid']) + ret['result'] = False + return ret + + devices_with_superblock = uuid_dict[uuid] + else: + devices_with_superblock = [] + + if present: + do_assemble = False + do_create = False + elif len(devices_with_superblock) > 0: do_assemble = True + do_create = False verb = 'assembled' else: + if len(new_devices) == 0: + ret['comment'] = 'All devices are missing: {0}.'.format(missing) + ret['result'] = False + return ret do_assemble = False + do_create = True verb = 'created' # If running with test use the test_mode with create or assemble if __opts__['test']: if do_assemble: res = __salt__['raid.assemble'](name, - devices, + devices_with_superblock, test_mode=True, **kwargs) - else: + elif do_create: res = __salt__['raid.create'](name, level, - devices, + new_devices + ['missing'] * len(missing), test_mode=True, **kwargs) - ret['comment'] = 'Raid will be {0} with: {1}'.format(verb, res) - ret['result'] = None + + if present: + ret['comment'] = 'Raid {0} already present.'.format(name) + + if do_assemble or do_create: + ret['comment'] = 'Raid will be {0} with: {1}'.format(verb, res) + ret['result'] = None + + if (do_assemble or present) and len(new_devices) > 0: + ret['comment'] += ' New devices will be added: {0}'.format(new_devices) + ret['result'] = None + + if len(missing) > 0: + ret['comment'] += ' Missing devices: {0}'.format(missing) + return ret # Attempt to create or assemble the array if do_assemble: __salt__['raid.assemble'](name, - devices, + devices_with_superblock, **kwargs) - else: + elif do_create: __salt__['raid.create'](name, level, - devices, + new_devices + ['missing'] * len(missing), **kwargs) - raids = __salt__['raid.list']() - changes = raids.get(name) - if changes: - ret['comment'] = 'Raid {0} {1}.'.format(name, verb) - ret['changes'] = changes - # Saving config - __salt__['raid.save_config']() + if not present: + raids = __salt__['raid.list']() + changes = raids.get(name) + if changes: + ret['comment'] = 'Raid {0} {1}.'.format(name, verb) + ret['changes'] = changes + # Saving config + __salt__['raid.save_config']() + else: + ret['comment'] = 'Raid {0} failed to be {1}.'.format(name, verb) + ret['result'] = False else: - ret['comment'] = 'Raid {0} failed to be {1}.'.format(name, verb) - ret['result'] = False + ret['comment'] = 'Raid {0} already present.'.format(name) + + if (do_assemble or present) and len(new_devices) > 0 and ret['result']: + for d in new_devices: + res = __salt__['raid.add'](name, d) + if not res: + ret['comment'] += ' Unable to add {0} to {1}.\n'.format(d, name) + ret['result'] = False + else: + ret['comment'] += ' Added new device {0} to {1}.\n'.format(d, name) + if ret['result']: + ret['changes']['added'] = new_devices + + if len(missing) > 0: + ret['comment'] += ' Missing devices: {0}'.format(missing) return ret diff --git a/salt/states/module.py b/salt/states/module.py index 202999e7d8..a253db9ae9 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -348,7 +348,7 @@ def _call_function(name, returner=None, **kwargs): returners = salt.loader.returners(__opts__, __salt__) if returner in returners: returners[returner]({'id': __opts__['id'], 'ret': mret, - 'fun': name, 'jid': salt.utils.jid.gen_jid()}) + 'fun': name, 'jid': salt.utils.jid.gen_jid(__opts__)}) return mret @@ -495,7 +495,7 @@ def _run(name, **kwargs): 'id': __opts__['id'], 'ret': mret, 'fun': name, - 'jid': salt.utils.jid.gen_jid()} + 'jid': salt.utils.jid.gen_jid(__opts__)} returners = salt.loader.returners(__opts__, __salt__) if kwargs['returner'] in returners: returners[kwargs['returner']](ret_ret) diff --git a/salt/states/mount.py b/salt/states/mount.py index fef674ddbc..dd609a2c7a 100644 --- a/salt/states/mount.py +++ b/salt/states/mount.py @@ -197,6 +197,8 @@ def mounted(name, 'result': True, 'comment': ''} + update_mount_cache = False + if device_name_regex is None: device_name_regex = [] @@ -439,6 +441,50 @@ def mounted(name, # don't write remount into fstab if 'remount' in opts: opts.remove('remount') + + # Update the cache + update_mount_cache = True + + mount_cache = __salt__['mount.read_mount_cache'](real_name) + if 'opts' in mount_cache: + _missing = [opt for opt in mount_cache['opts'] + if opt not in opts] + + if _missing: + if __opts__['test']: + ret['result'] = None + ret['comment'] = ('Remount would be forced because' + ' options ({0})' + 'changed'.format(','.join(_missing))) + return ret + else: + # Some file systems require umounting and mounting if options change + # add others to list that require similiar functionality + if fstype in ['nfs', 'cvfs'] or fstype.startswith('fuse'): + ret['changes']['umount'] = "Forced unmount and mount because " \ + + "options ({0}) changed".format(opt) + unmount_result = __salt__['mount.umount'](real_name) + if unmount_result is True: + mount_result = __salt__['mount.mount'](real_name, device, mkmnt=mkmnt, fstype=fstype, opts=opts) + ret['result'] = mount_result + else: + ret['result'] = False + ret['comment'] = 'Unable to unmount {0}: {1}.'.format(real_name, unmount_result) + return ret + else: + ret['changes']['umount'] = "Forced remount because " \ + + "options ({0}) changed".format(opt) + remount_result = __salt__['mount.remount'](real_name, device, mkmnt=mkmnt, fstype=fstype, opts=opts) + ret['result'] = remount_result + # Cleanup after the remount, so we + # don't write remount into fstab + if 'remount' in opts: + opts.remove('remount') + + update_mount_cache = True + else: + update_mount_cache = True + if real_device not in device_list: # name matches but device doesn't - need to umount _device_mismatch_is_ignored = None @@ -469,6 +515,7 @@ def mounted(name, ret['comment'] = "Unable to unmount" ret['result'] = None return ret + update_mount_cache = True else: ret['comment'] = 'Target was already mounted' # using a duplicate check so I can catch the results of a umount @@ -492,6 +539,7 @@ def mounted(name, out = __salt__['mount.mount'](name, device, mkmnt, fstype, opts, user=user) active = __salt__['mount.active'](extended=True) + update_mount_cache = True if isinstance(out, string_types): # Failed to (re)mount, the state has failed! ret['comment'] = out @@ -591,6 +639,13 @@ def mounted(name, config, match_on=match_on) + if update_mount_cache: + cache_result = __salt__['mount.write_mount_cache'](real_name, + device, + mkmnt=mkmnt, + fstype=fstype, + opts=opts) + if out == 'present': ret['comment'] += '. Entry already exists in the fstab.' return ret @@ -730,6 +785,8 @@ def unmounted(name, 'result': True, 'comment': ''} + update_mount_cache = False + # Get the active data active = __salt__['mount.active'](extended=True) if name not in active: @@ -744,8 +801,10 @@ def unmounted(name, return ret if device: out = __salt__['mount.umount'](name, device, user=user) + update_mount_cache = True else: out = __salt__['mount.umount'](name, user=user) + update_mount_cache = True if isinstance(out, string_types): # Failed to umount, the state has failed! ret['comment'] = out @@ -758,6 +817,9 @@ def unmounted(name, ret['comment'] = 'Execute set to False, Target was not unmounted' ret['result'] = True + if update_mount_cache: + cache_result = __salt__['mount.delete_mount_cache'](name) + if persist: # Override default for Mac OS if __grains__['os'] in ['MacOS', 'Darwin'] and config == '/etc/fstab': diff --git a/salt/states/netconfig.py b/salt/states/netconfig.py index 1b2ed44d56..7311baaf1f 100644 --- a/salt/states/netconfig.py +++ b/salt/states/netconfig.py @@ -60,6 +60,7 @@ def _update_config(template_name, template_user='root', template_group='root', template_mode='755', + template_attrs='--------------e----', saltenv=None, template_engine='jinja', skip_verify=False, @@ -83,6 +84,7 @@ def _update_config(template_name, template_user=template_user, template_group=template_group, template_mode=template_mode, + template_attrs=template_attrs, saltenv=saltenv, template_engine=template_engine, skip_verify=skip_verify, @@ -107,6 +109,7 @@ def managed(name, template_user='root', template_group='root', template_mode='755', + template_attrs='--------------e----', saltenv=None, template_engine='jinja', skip_verify=False, @@ -178,9 +181,14 @@ def managed(name, template_user: root Group owner of file. - template_user: 755 + template_mode: 755 Permissions of file + template_attrs: "--------------e----" + Attributes of file (see `man lsattr`) + + .. versionadded:: oxygen + saltenv: base Specifies the template environment. This will influence the relative imports inside the templates. @@ -339,6 +347,7 @@ def managed(name, template_user=template_user, template_group=template_group, template_mode=template_mode, + template_attrs=template_attrs, saltenv=saltenv, template_engine=template_engine, skip_verify=skip_verify, diff --git a/salt/states/nfs_export.py b/salt/states/nfs_export.py new file mode 100644 index 0000000000..7f4f488b0a --- /dev/null +++ b/salt/states/nfs_export.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +''' +Management of NFS exports +=============================================== + +.. versionadded:: Oxygen + +To ensure an NFS export exists: + +.. code-block:: yaml + + add_simple_export: + nfs_export.present: + - name: '/srv/nfs' + - hosts: '10.0.2.0/24' + - options: + - 'rw' + +This creates the following in /etc/exports: + +.. code-block:: bash + + /srv/nfs 10.0.2.0/24(rw) + +For more complex exports with multiple groups of hosts, use 'clients': + +.. code-block:: yaml + + add_complex_export: + nfs_export.present: + - name: '/srv/nfs' + - clients: + # First export, same as simple one above + - hosts: '10.0.2.0/24' + options: + - 'rw' + # Second export + - hosts: '*.example.com' + options: + - 'ro' + - 'subtree_check' + +This creates the following in /etc/exports: + +.. code-block:: bash + + /srv/nfs 10.0.2.0/24(rw) 192.168.0.0/24,172.19.0.0/16(ro,subtree_check) + +Any export of the given path will be modified to match the one specified. + +To ensure an NFS export is absent: + +.. code-block:: yaml + + delete_export: + nfs_export.absent: + - name: '/srv/nfs' + +''' +from __future__ import absolute_import +import salt.utils.path + + +def __virtual__(): + ''' + Only work with nfs tools installed + ''' + cmd = 'exportfs' + if salt.utils.path.which(cmd): + return bool(cmd) + + return( + False, + 'The nfs_exports state module failed to load: ' + 'the exportfs binary is not in the path' + ) + + +def present(name, + clients=None, + hosts=None, + options=None, + exports='/etc/exports'): + ''' + Ensure that the named export is present with the given options + + name + The export path to configure + + clients + A list of hosts and the options applied to them. + This option may not be used in combination with + the 'hosts' or 'options' shortcuts. + + .. code-block:: yaml + + - clients: + # First export + - hosts: '10.0.2.0/24' + options: + - 'rw' + # Second export + - hosts: '*.example.com' + options: + - 'ro' + - 'subtree_check' + + hosts + A string matching a number of hosts, for example: + + .. code-block:: yaml + + hosts: '10.0.2.123' + + hosts: '10.0.2.0/24' + + hosts: 'minion1.example.com' + + hosts: '*.example.com' + + hosts: '*' + + options + A list of NFS options, for example: + + .. code-block:: yaml + + options: + - 'rw' + - 'subtree_check' + + ''' + path = name + ret = {'name': name, + 'changes': {}, + 'result': None, + 'comment': ''} + + if not clients: + if not hosts: + ret['result'] = False + ret['comment'] = 'Either \'clients\' or \'hosts\' must be defined' + return ret + # options being None is handled by add_export() + clients = [{'hosts': hosts, 'options': options}] + + old = __salt__['nfs3.list_exports'](exports) + if path in old: + if old[path] == clients: + ret['result'] = True + ret['comment'] = 'Export {0} already configured'.format(path) + return ret + + ret['changes']['new'] = clients + ret['changes']['old'] = old[path] + if __opts__['test']: + ret['result'] = None + ret['comment'] = 'Export {0} would be changed'.format(path) + return ret + + __salt__['nfs3.del_export'](exports, path) + + else: + ret['changes']['old'] = None + ret['changes']['new'] = clients + if __opts__['test']: + ret['result'] = None + ret['comment'] = 'Export {0} would be added'.format(path) + return ret + + add_export = __salt__['nfs3.add_export'] + for exp in clients: + add_export(exports, path, exp['hosts'], exp['options']) + + ret['changes']['new'] = clients + + try_reload = __salt__['nfs3.reload_exports']() + ret['comment'] = try_reload['stderr'] + ret['result'] = try_reload['result'] + return ret + + +def absent(name, exports='/etc/exports'): + ''' + Ensure that the named path is not exported + + name + The export path to remove + ''' + + path = name + ret = {'name': name, + 'changes': {}, + 'result': None, + 'comment': ''} + + old = __salt__['nfs3.list_exports'](exports) + if path in old: + if __opts__['test']: + ret['comment'] = 'Export {0} would be removed'.format(path) + ret['changes'][path] = old[path] + ret['result'] = None + return ret + + __salt__['nfs3.del_export'](exports, path) + try_reload = __salt__['nfs3.reload_exports']() + if not try_reload['result']: + ret['comment'] = try_reload['stderr'] + else: + ret['comment'] = 'Export {0} removed'.format(path) + + ret['result'] = try_reload['result'] + ret['changes'][path] = old[path] + else: + ret['comment'] = 'Export {0} already absent'.format(path) + ret['result'] = True + + return ret diff --git a/salt/states/panos.py b/salt/states/panos.py index ee30325340..f941ff157a 100644 --- a/salt/states/panos.py +++ b/salt/states/panos.py @@ -54,6 +54,30 @@ commit to the device. panos/commit: panos.commit +Version Specific Configurations +=============================== +Palo Alto devices running different versions will have different supported features and different command structures. In +order to account for this, the proxy module can be leveraged to check if the panos device is at a specific revision +level. + +The proxy['panos.is_required_version'] method will check if a panos device is currently running a version equal or +greater than the passed version. For example, proxy['panos.is_required_version']('7.0.0') would match both 7.1.0 and +8.0.0. + +.. code-block:: yaml + + {% if proxy['panos.is_required_version']('8.0.0') %} + panos/deviceconfig/system/motd-and-banner: + panos.set_config: + - xpath: /config/devices/entry[@name='localhost.localdomain']/deviceconfig/system/motd-and-banner + - value: | + BANNER TEXT + color2 + color18 + yes + - commit: False + {% endif %} + .. seealso:: :prox:`Palo Alto Proxy Module ` @@ -70,6 +94,24 @@ def __virtual__(): return 'panos.commit' in __salt__ +def _build_members(members, anycheck=False): + ''' + Builds a member formatted string for XML operation. + + ''' + if isinstance(members, list): + + # This check will strip down members to a single any statement + if anycheck and 'any' in members: + return "any" + response = "" + for m in members: + response += "{0}".format(m) + return response + else: + return "{0}".format(members) + + def _default_ret(name): ''' Set the default response values. @@ -85,6 +127,135 @@ def _default_ret(name): return ret +def _edit_config(xpath, element): + ''' + Sends an edit request to the device. + + ''' + query = {'type': 'config', + 'action': 'edit', + 'xpath': xpath, + 'element': element} + + response = __proxy__['panos.call'](query) + + return _validate_response(response) + + +def _get_config(xpath): + ''' + Retrieves an xpath from the device. + + ''' + query = {'type': 'config', + 'action': 'get', + 'xpath': xpath} + + response = __proxy__['panos.call'](query) + + return response + + +def _move_after(xpath, target): + ''' + Moves an xpath to the after of its section. + + ''' + query = {'type': 'config', + 'action': 'move', + 'xpath': xpath, + 'where': 'after', + 'dst': target} + + response = __proxy__['panos.call'](query) + + return _validate_response(response) + + +def _move_before(xpath, target): + ''' + Moves an xpath to the bottom of its section. + + ''' + query = {'type': 'config', + 'action': 'move', + 'xpath': xpath, + 'where': 'before', + 'dst': target} + + response = __proxy__['panos.call'](query) + + return _validate_response(response) + + +def _move_bottom(xpath): + ''' + Moves an xpath to the bottom of its section. + + ''' + query = {'type': 'config', + 'action': 'move', + 'xpath': xpath, + 'where': 'bottom'} + + response = __proxy__['panos.call'](query) + + return _validate_response(response) + + +def _move_top(xpath): + ''' + Moves an xpath to the top of its section. + + ''' + query = {'type': 'config', + 'action': 'move', + 'xpath': xpath, + 'where': 'top'} + + response = __proxy__['panos.call'](query) + + return _validate_response(response) + + +def _set_config(xpath, element): + ''' + Sends a set request to the device. + + ''' + query = {'type': 'config', + 'action': 'set', + 'xpath': xpath, + 'element': element} + + response = __proxy__['panos.call'](query) + + return _validate_response(response) + + +def _validate_response(response): + ''' + Validates a response from a Palo Alto device. Used to verify success of commands. + + ''' + if not response: + return False, "Error during move configuration. Verify connectivity to device." + elif 'msg' in response: + if response['msg'] == 'command succeeded': + return True, response['msg'] + else: + return False, response['msg'] + elif 'line' in response: + if response['line'] == 'already at the top': + return True, response['line'] + elif response['line'] == 'already at the bottom': + return True, response['line'] + else: + return False, response['line'] + else: + return False, "Error during move configuration. Verify connectivity to device." + + def add_config_lock(name): ''' Prevent other users from changing configuration until the lock is released. @@ -162,7 +333,7 @@ def clone_config(name, xpath=None, newname=None, commit=False): return ret -def commit(name): +def commit_config(name): ''' Commits the candidate configuration to the running configuration. @@ -173,7 +344,7 @@ def commit(name): .. code-block:: yaml panos/commit: - panos.commit + panos.commit_config ''' ret = _default_ret(name) @@ -314,6 +485,8 @@ def edit_config(name, xpath=None, value=None, commit=False): You can replace an existing object hierarchy at a specified location in the configuration with a new value. Use the xpath parameter to specify the location of the object, including the node to be replaced. + This is the recommended state to enforce configurations on a xpath. + name: The name of the module function to execute. xpath(str): The XPATH of the configuration API tree to control. @@ -335,24 +508,17 @@ def edit_config(name, xpath=None, value=None, commit=False): ''' ret = _default_ret(name) - if not xpath: - return ret - - if not value: - return ret - - query = {'type': 'config', - 'action': 'edit', - 'xpath': xpath, - 'element': value} - - response = __proxy__['panos.call'](query) + result, msg = _edit_config(xpath, value) ret.update({ - 'changes': response, - 'result': True + 'comment': msg, + 'result': result }) + # Ensure we do not commit after a failed action + if not result: + return ret + if commit is True: ret.update({ 'commit': __salt__['panos.commit'](), @@ -380,7 +546,8 @@ def move_config(name, xpath=None, where=None, dst=None, commit=False): dst(str): Optional. Specifies the destination to utilize for a move action. This is ignored for the top or bottom action. - commit(bool): If true the firewall will commit the changes, if false do not commit changes. + commit(bool): If true the firewall will commit the changes, if false do not commit changes. If the operation is + not successful, it will not commit. SLS Example: @@ -409,35 +576,21 @@ def move_config(name, xpath=None, where=None, dst=None, commit=False): return ret if where == 'after': - query = {'type': 'config', - 'action': 'move', - 'xpath': xpath, - 'where': 'after', - 'dst': dst} + result, msg = _move_after(xpath, dst) elif where == 'before': - query = {'type': 'config', - 'action': 'move', - 'xpath': xpath, - 'where': 'before', - 'dst': dst} + result, msg = _move_before(xpath, dst) elif where == 'top': - query = {'type': 'config', - 'action': 'move', - 'xpath': xpath, - 'where': 'top'} + result, msg = _move_top(xpath) elif where == 'bottom': - query = {'type': 'config', - 'action': 'move', - 'xpath': xpath, - 'where': 'bottom'} - - response = __proxy__['panos.call'](query) + result, msg = _move_bottom(xpath) ret.update({ - 'changes': response, - 'result': True + 'result': result }) + if not result: + return ret + if commit is True: ret.update({ 'commit': __salt__['panos.commit'](), @@ -523,6 +676,350 @@ def rename_config(name, xpath=None, newname=None, commit=False): return ret +def security_rule_exists(name, + rulename=None, + vsys='1', + action=None, + disabled=None, + sourcezone=None, + destinationzone=None, + source=None, + destination=None, + application=None, + service=None, + description=None, + logsetting=None, + logstart=None, + logend=None, + negatesource=None, + negatedestination=None, + profilegroup=None, + datafilter=None, + fileblock=None, + spyware=None, + urlfilter=None, + virus=None, + vulnerability=None, + wildfire=None, + move=None, + movetarget=None, + commit=False): + ''' + Ensures that a security rule exists on the device. Also, ensure that all configurations are set appropriately. + + This method will create the rule if it does not exist. If the rule does exist, it will ensure that the + configurations are set appropriately. + + If the rule does not exist and is created, any value that is not provided will be provided as the default. + The action, to, from, source, destination, application, and service fields are mandatory and must be provided. + + This will enforce the exact match of the rule. For example, if the rule is currently configured with the log-end + option, but this option is not specified in the state method, it will be removed and reset to the system default. + + It is strongly recommended to specify all options to ensure proper operation. + + When defining the profile group settings, the device can only support either a profile group or individual settings. + If both are specified, the profile group will be preferred and the individual settings are ignored. If neither are + specified, the value will be set to system default of none. + + name: The name of the module function to execute. + + rulename(str): The name of the security rule. The name is case-sensitive and can have up to 31 characters, which + can be letters, numbers, spaces, hyphens, and underscores. The name must be unique on a firewall and, on Panorama, + unique within its device group and any ancestor or descendant device groups. + + vsys(str): The string representation of the VSYS ID. Defaults to VSYS 1. + + action(str): The action that the security rule will enforce. Valid options are: allow, deny, drop, reset-client, + reset-server, reset-both. + + disabled(bool): Controls if the rule is disabled. Set 'True' to disable and 'False' to enable. + + sourcezone(str, list): The source zone(s). The value 'any' will match all zones. + + destinationzone(str, list): The destination zone(s). The value 'any' will match all zones. + + source(str, list): The source address(es). The value 'any' will match all addresses. + + destination(str, list): The destination address(es). The value 'any' will match all addresses. + + application(str, list): The application(s) matched. The value 'any' will match all applications. + + service(str, list): The service(s) matched. The value 'any' will match all services. The value + 'application-default' will match based upon the application defined ports. + + description(str): A description for the policy (up to 255 characters). + + logsetting(str): The name of a valid log forwarding profile. + + logstart(bool): Generates a traffic log entry for the start of a session (disabled by default). + + logend(bool): Generates a traffic log entry for the end of a session (enabled by default). + + negatesource(bool): Match all but the specified source addresses. + + negatedestination(bool): Match all but the specified destination addresses. + + profilegroup(str): A valid profile group name. + + datafilter(str): A valid data filter profile name. Ignored with the profilegroup option set. + + fileblock(str): A valid file blocking profile name. Ignored with the profilegroup option set. + + spyware(str): A valid spyware profile name. Ignored with the profilegroup option set. + + urlfilter(str): A valid URL filtering profile name. Ignored with the profilegroup option set. + + virus(str): A valid virus profile name. Ignored with the profilegroup option set. + + vulnerability(str): A valid vulnerability profile name. Ignored with the profilegroup option set. + + wildfire(str): A valid vulnerability profile name. Ignored with the profilegroup option set. + + move(str): An optional argument that ensure the rule is moved to a specific location. Valid options are 'top', + 'bottom', 'before', or 'after'. The 'before' and 'after' options require the use of the 'movetarget' argument + to define the location of the move request. + + movetarget(str): An optional argument that defines the target of the move operation if the move argument is + set to 'before' or 'after'. + + commit(bool): If true the firewall will commit the changes, if false do not commit changes. + + SLS Example: + + .. code-block:: yaml + + panos/rulebase/security/rule01: + panos.security_rule_exists: + - rulename: rule01 + - vsys: 1 + - action: allow + - disabled: False + - sourcezone: untrust + - destinationzone: trust + - source: + - 10.10.10.0/24 + - 1.1.1.1 + - destination: + - 2.2.2.2-2.2.2.4 + - application: + - any + - service: + - tcp-25 + - description: My test security rule + - logsetting: logprofile + - logstart: False + - logend: True + - negatesource: False + - negatedestination: False + - profilegroup: myprofilegroup + - move: top + - commit: False + + panos/rulebase/security/rule01: + panos.security_rule_exists: + - rulename: rule01 + - vsys: 1 + - action: allow + - disabled: False + - sourcezone: untrust + - destinationzone: trust + - source: + - 10.10.10.0/24 + - 1.1.1.1 + - destination: + - 2.2.2.2-2.2.2.4 + - application: + - any + - service: + - tcp-25 + - description: My test security rule + - logsetting: logprofile + - logstart: False + - logend: False + - datafilter: foobar + - fileblock: foobar + - spyware: foobar + - urlfilter: foobar + - virus: foobar + - vulnerability: foobar + - wildfire: foobar + - move: after + - movetarget: rule02 + - commit: False + ''' + ret = _default_ret(name) + + if not rulename: + return ret + + # Check if rule currently exists + rule = __salt__['panos.get_security_rule'](rulename, vsys) + + # Build the rule element + element = "" + if sourcezone: + element += "{0}".format(_build_members(sourcezone, True)) + else: + ret.update({'comment': "The sourcezone field must be provided."}) + return ret + + if destinationzone: + element += "{0}".format(_build_members(destinationzone, True)) + else: + ret.update({'comment': "The destinationzone field must be provided."}) + return ret + + if source: + element += "{0}".format(_build_members(source, True)) + else: + ret.update({'comment': "The source field must be provided."}) + return + + if destination: + element += "{0}".format(_build_members(destination, True)) + else: + ret.update({'comment': "The destination field must be provided."}) + return ret + + if application: + element += "{0}".format(_build_members(application, True)) + else: + ret.update({'comment': "The application field must be provided."}) + return ret + + if service: + element += "{0}".format(_build_members(service, True)) + else: + ret.update({'comment': "The service field must be provided."}) + return ret + + if action: + element += "{0}".format(action) + else: + ret.update({'comment': "The action field must be provided."}) + return ret + + if disabled is not None: + if disabled: + element += "yes" + else: + element += "no" + + if description: + element += "{0}".format(description) + + if logsetting: + element += "{0}".format(logsetting) + + if logstart is not None: + if logstart: + element += "yes" + else: + element += "no" + + if logend is not None: + if logend: + element += "yes" + else: + element += "no" + + if negatesource is not None: + if negatesource: + element += "yes" + else: + element += "no" + + if negatedestination is not None: + if negatedestination: + element += "yes" + else: + element += "no" + + # Build the profile settings + profile_string = None + if profilegroup: + profile_string = "{0}".format(profilegroup) + else: + member_string = "" + if datafilter: + member_string += "{0}".format(datafilter) + if fileblock: + member_string += "{0}".format(fileblock) + if spyware: + member_string += "{0}".format(spyware) + if urlfilter: + member_string += "{0}".format(urlfilter) + if virus: + member_string += "{0}".format(virus) + if vulnerability: + member_string += "{0}".format(vulnerability) + if wildfire: + member_string += "{0}".format(wildfire) + if member_string != "": + profile_string = "{0}".format(member_string) + + if profile_string: + element += "{0}".format(profile_string) + + full_element = "{1}".format(rulename, element) + + create_rule = False + + if 'result' in rule: + if rule['result'] == "None": + create_rule = True + + if create_rule: + xpath = "/config/devices/entry[@name=\'localhost.localdomain\']/vsys/entry[@name=\'vsys{0}\']/rulebase/" \ + "security/rules".format(vsys) + + result, msg = _set_config(xpath, full_element) + if not result: + ret['changes']['set'] = msg + return ret + else: + xpath = "/config/devices/entry[@name=\'localhost.localdomain\']/vsys/entry[@name=\'vsys{0}\']/rulebase/" \ + "security/rules/entry[@name=\'{1}\']".format(vsys, rulename) + + result, msg = _edit_config(xpath, full_element) + if not result: + ret['changes']['edit'] = msg + return ret + + if move: + movepath = "/config/devices/entry[@name=\'localhost.localdomain\']/vsys/entry[@name=\'vsys{0}\']/rulebase/" \ + "security/rules/entry[@name=\'{1}\']".format(vsys, rulename) + move_result = False + move_msg = '' + if move == "before" and movetarget: + move_result, move_msg = _move_before(movepath, movetarget) + elif move == "after": + move_result, move_msg = _move_after(movepath, movetarget) + elif move == "top": + move_result, move_msg = _move_top(movepath) + elif move == "bottom": + move_result, move_msg = _move_bottom(movepath) + + if not move_result: + ret['changes']['move'] = move_msg + return ret + + if commit is True: + ret.update({ + 'commit': __salt__['panos.commit'](), + 'comment': 'Security rule verified successfully.', + 'result': True + }) + else: + ret.update({ + 'comment': 'Security rule verified successfully.', + 'result': True + }) + + return ret + + def set_config(name, xpath=None, value=None, commit=False): ''' Sets a Palo Alto XPATH to a specific value. This will always overwrite the existing value, even if it is not @@ -552,24 +1049,17 @@ def set_config(name, xpath=None, value=None, commit=False): ''' ret = _default_ret(name) - if not xpath: - return ret - - if not value: - return ret - - query = {'type': 'config', - 'action': 'set', - 'xpath': xpath, - 'element': value} - - response = __proxy__['panos.call'](query) + result, msg = _set_config(xpath, value) ret.update({ - 'changes': response, - 'result': True + 'comment': msg, + 'result': result }) + # Ensure we do not commit after a failed action + if not result: + return ret + if commit is True: ret.update({ 'commit': __salt__['panos.commit'](), diff --git a/salt/states/pkg.py b/salt/states/pkg.py index 103907eab8..159f110cbc 100644 --- a/salt/states/pkg.py +++ b/salt/states/pkg.py @@ -81,7 +81,6 @@ import os import re # Import Salt libs -import salt.utils # Can be removed once gen_state_tag is moved import salt.utils.pkg import salt.utils.platform import salt.utils.versions @@ -3071,7 +3070,7 @@ def mod_aggregate(low, chunks, running): if low.get('fun') not in agg_enabled: return low for chunk in chunks: - tag = salt.utils.gen_state_tag(chunk) + tag = __utils__['state.gen_tag'](chunk) if tag in running: # Already ran the pkg state, skip aggregation continue diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index ae48a7e334..0095401356 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -30,7 +30,6 @@ import time # Import salt libs import salt.syspaths -import salt.utils # Can be removed once check_state_result is moved import salt.utils.event import salt.utils.versions from salt.ext import six @@ -342,7 +341,7 @@ def state(name, except KeyError: m_state = False if m_state: - m_state = salt.utils.check_state_result(m_ret, recurse=True) + m_state = __utils__['state.check_result'](m_ret, recurse=True) if not m_state: if minion not in fail_minions: @@ -742,18 +741,26 @@ def runner(name, **kwargs): if isinstance(runner_return, dict) and 'Error' in runner_return: out['success'] = False if not out.get('success', True): + cmt = "Runner function '{0}' failed{1}.".format( + name, + ' with return {0}'.format(runner_return) if runner_return else '', + ) ret = { 'name': name, 'result': False, 'changes': {}, - 'comment': runner_return if runner_return else "Runner function '{0}' failed without comment.".format(name) + 'comment': cmt, } else: + cmt = "Runner function '{0}' executed{1}.".format( + name, + ' with return {0}'.format(runner_return) if runner_return else '', + ) ret = { 'name': name, 'result': True, - 'changes': runner_return if runner_return else {}, - 'comment': "Runner function '{0}' executed.".format(name) + 'changes': {}, + 'comment': cmt, } ret['__orchestration__'] = True @@ -802,14 +809,14 @@ def wheel(name, **kwargs): **kwargs) ret['result'] = True - ret['comment'] = "Wheel function '{0}' executed.".format(name) - ret['__orchestration__'] = True if 'jid' in out: ret['__jid__'] = out['jid'] runner_return = out.get('return') - if runner_return: - ret['changes'] = runner_return + ret['comment'] = "Wheel function '{0}' executed{1}.".format( + name, + ' with return {0}'.format(runner_return) if runner_return else '', + ) return ret diff --git a/salt/states/selinux.py b/salt/states/selinux.py index 8187ea8338..3c2a3ee817 100644 --- a/salt/states/selinux.py +++ b/salt/states/selinux.py @@ -310,17 +310,27 @@ def module_remove(name): def fcontext_policy_present(name, sel_type, filetype='a', sel_user=None, sel_level=None): ''' - Makes sure a SELinux policy for a given filespec (name), - filetype and SELinux context type is present. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. - sel_type: SELinux context type. There are many. - filetype: The SELinux filetype specification. - Use one of [a, f, d, c, b, s, l, p]. - See also `man semanage-fcontext`. - Defaults to 'a' (all files) - sel_user: The SELinux user. - sel_level: The SELinux MLS range + Makes sure a SELinux policy for a given filespec (name), filetype + and SELinux context type is present. + + name + filespec of the file or directory. Regex syntax is allowed. + + sel_type + SELinux context type. There are many. + + filetype + The SELinux filetype specification. Use one of [a, f, d, c, b, + s, l, p]. See also `man semanage-fcontext`. Defaults to 'a' + (all files). + + sel_user + The SELinux user. + + sel_level + The SELinux MLS range. ''' ret = {'name': name, 'result': False, 'changes': {}, 'comment': ''} new_state = {} @@ -383,17 +393,27 @@ def fcontext_policy_present(name, sel_type, filetype='a', sel_user=None, sel_lev def fcontext_policy_absent(name, filetype='a', sel_type=None, sel_user=None, sel_level=None): ''' - Makes sure an SELinux file context policy for a given filespec (name), - filetype and SELinux context type is absent. + .. versionadded:: 2017.7.0 - name: filespec of the file or directory. Regex syntax is allowed. - filetype: The SELinux filetype specification. - Use one of [a, f, d, c, b, s, l, p]. - See also `man semanage-fcontext`. - Defaults to 'a' (all files). - sel_type: The SELinux context type. There are many. - sel_user: The SELinux user. - sel_level: The SELinux MLS range + Makes sure an SELinux file context policy for a given filespec + (name), filetype and SELinux context type is absent. + + name + filespec of the file or directory. Regex syntax is allowed. + + filetype + The SELinux filetype specification. Use one of [a, f, d, c, b, + s, l, p]. See also `man semanage-fcontext`. Defaults to 'a' + (all files). + + sel_type + The SELinux context type. There are many. + + sel_user + The SELinux user. + + sel_level + The SELinux MLS range. ''' ret = {'name': name, 'result': False, 'changes': {}, 'comment': ''} new_state = {} @@ -433,7 +453,10 @@ def fcontext_policy_absent(name, filetype='a', sel_type=None, sel_user=None, sel def fcontext_policy_applied(name, recursive=False): ''' - Checks and makes sure the SELinux policies for a given filespec are applied. + .. versionadded:: 2017.7.0 + + Checks and makes sure the SELinux policies for a given filespec are + applied. ''' ret = {'name': name, 'result': False, 'changes': {}, 'comment': ''} diff --git a/salt/states/ssh_known_hosts.py b/salt/states/ssh_known_hosts.py index 1b35732110..93325aa18a 100644 --- a/salt/states/ssh_known_hosts.py +++ b/salt/states/ssh_known_hosts.py @@ -12,6 +12,7 @@ Manage the information stored in the known_hosts files. - present - user: root - fingerprint: 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48 + - fingerprint_hash_type: md5 example.com: ssh_known_hosts: diff --git a/salt/states/win_iis.py b/salt/states/win_iis.py index 69d35e5c4a..6407315c21 100644 --- a/salt/states/win_iis.py +++ b/salt/states/win_iis.py @@ -481,7 +481,6 @@ def container_setting(name, container, settings=None): :param str container: The type of IIS container. The container types are: AppPools, Sites, SslBindings :param str settings: A dictionary of the setting names and their values. - Example of usage for the ``AppPools`` container: .. code-block:: yaml @@ -510,6 +509,8 @@ def container_setting(name, container, settings=None): logFile.period: Daily limits.maxUrlSegments: 32 ''' + + identityType_map2string = {0: 'LocalSystem', 1: 'LocalService', 2: 'NetworkService', 3: 'SpecificUser', 4: 'ApplicationPoolIdentity'} ret = {'name': name, 'changes': {}, 'comment': str(), @@ -529,6 +530,10 @@ def container_setting(name, container, settings=None): container=container, settings=settings.keys()) for setting in settings: + # map identity type from numeric to string for comparing + if setting == 'processModel.identityType' and settings[setting] in identityType_map2string.keys(): + settings[setting] = identityType_map2string[settings[setting]] + if str(settings[setting]) != str(current_settings[setting]): ret_settings['changes'][setting] = {'old': current_settings[setting], 'new': settings[setting]} @@ -541,8 +546,8 @@ def container_setting(name, container, settings=None): ret['changes'] = ret_settings return ret - __salt__['win_iis.set_container_setting'](name=name, container=container, - settings=settings) + __salt__['win_iis.set_container_setting'](name=name, container=container, settings=settings) + new_settings = __salt__['win_iis.get_container_setting'](name=name, container=container, settings=settings.keys()) diff --git a/salt/syspaths.py b/salt/syspaths.py index 95efc7246b..7aeb6e45e4 100644 --- a/salt/syspaths.py +++ b/salt/syspaths.py @@ -34,13 +34,13 @@ try: import salt._syspaths as __generated_syspaths # pylint: disable=no-name-in-module except ImportError: import types - __generated_syspaths = types.ModuleType('salt._syspaths') # future lint: disable=non-unicode-string - for key in (u'ROOT_DIR', u'CONFIG_DIR', u'CACHE_DIR', u'SOCK_DIR', - u'SRV_ROOT_DIR', u'BASE_FILE_ROOTS_DIR', - u'BASE_PILLAR_ROOTS_DIR', u'BASE_THORIUM_ROOTS_DIR', - u'BASE_MASTER_ROOTS_DIR', u'LOGS_DIR', u'PIDFILE_DIR', - u'SPM_FORMULA_PATH', u'SPM_PILLAR_PATH', u'SPM_REACTOR_PATH', - u'SHARE_DIR'): + __generated_syspaths = types.ModuleType('salt._syspaths') + for key in ('ROOT_DIR', 'CONFIG_DIR', 'CACHE_DIR', 'SOCK_DIR', + 'SRV_ROOT_DIR', 'BASE_FILE_ROOTS_DIR', 'HOME_DIR', + 'BASE_PILLAR_ROOTS_DIR', 'BASE_THORIUM_ROOTS_DIR', + 'BASE_MASTER_ROOTS_DIR', 'LOGS_DIR', 'PIDFILE_DIR', + 'SPM_FORMULA_PATH', 'SPM_PILLAR_PATH', 'SPM_REACTOR_PATH', + 'SHARE_DIR'): setattr(__generated_syspaths, key, None) @@ -139,6 +139,10 @@ SPM_REACTOR_PATH = __generated_syspaths.SPM_REACTOR_PATH if SPM_REACTOR_PATH is None: SPM_REACTOR_PATH = os.path.join(SRV_ROOT_DIR, u'spm', u'reactor') +HOME_DIR = __generated_syspaths.HOME_DIR +if HOME_DIR is None: + HOME_DIR = os.path.expanduser('~') + __all__ = [ u'ROOT_DIR', diff --git a/salt/template.py b/salt/template.py index 7a3d00ebfc..c2a3de7582 100644 --- a/salt/template.py +++ b/salt/template.py @@ -51,12 +51,7 @@ def compile_template(template, log.debug(u'compile template: %s', template) if u'env' in kwargs: - salt.utils.versions.warn_until( - u'Oxygen', - u'Parameter \'env\' has been detected in the argument list. This ' - u'parameter is no longer used and has been replaced by \'saltenv\' ' - u'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kwargs.pop(u'env') if template != u':string:': diff --git a/salt/templates/rh_ip/rh7_eth.jinja b/salt/templates/rh_ip/rh7_eth.jinja index c9c50cd8cd..741b964946 100644 --- a/salt/templates/rh_ip/rh7_eth.jinja +++ b/salt/templates/rh_ip/rh7_eth.jinja @@ -15,6 +15,8 @@ DEVICE="{{name}}" {%endif%}{% if onparent %}ONPARENT={{onparent}} {%endif%}{% if ipv4_failure_fatal %}IPV4_FAILURE_FATAL="{{ipv4_failure_fatal}}" {%endif%}{% if ipaddr %}IPADDR="{{ipaddr}}" +{%endif%}{% if ipaddr_start %}IPADDR_START="{{ipaddr_start}}" +{%endif%}{% if ipaddr_end %}IPADDR_END="{{ipaddr_end}}" {%endif%}{% if netmask %}NETMASK="{{netmask}}" {%endif%}{% if prefix %}PREFIX="{{prefix}}" {%endif%}{% if gateway %}GATEWAY="{{gateway}}" diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py index 3d35982c31..7bb5409baf 100644 --- a/salt/transport/zeromq.py +++ b/salt/transport/zeromq.py @@ -827,8 +827,9 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel): match_targets = ["pcre", "glob", "list"] if self.opts['zmq_filtering'] and load['tgt_type'] in match_targets: # Fetch a list of minions that match - match_ids = self.ckminions.check_minions(load['tgt'], - tgt_type=load['tgt_type']) + _res = self.ckminions.check_minions(load['tgt'], + tgt_type=load['tgt_type']) + match_ids = _res['minions'] log.debug("Publish Side Match: {0}".format(match_ids)) # Send list of miions thru so zmq can target them diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 3526b6ff31..c83bccdf6f 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -19,9 +19,7 @@ import fnmatch import hashlib import json import logging -import numbers import os -import posixpath import random import re import shlex @@ -29,10 +27,8 @@ import shutil import socket import sys import pstats -import tempfile import time import types -import warnings import string import subprocess import getpass @@ -40,10 +36,8 @@ import getpass # Import 3rd-party libs from salt.ext import six # pylint: disable=import-error -from salt.ext.six.moves.urllib.parse import urlparse # pylint: disable=no-name-in-module # pylint: disable=redefined-builtin from salt.ext.six.moves import range -from salt.ext.six.moves import zip # pylint: enable=import-error,redefined-builtin if six.PY3: @@ -126,7 +120,6 @@ import salt.utils.dictupdate import salt.utils.versions import salt.version from salt.utils.decorators.jinja import jinja_filter -from salt.textformat import TextFormat from salt.exceptions import ( CommandExecutionError, SaltClientError, CommandNotFoundError, SaltSystemExit, @@ -135,84 +128,6 @@ from salt.exceptions import ( log = logging.getLogger(__name__) -_empty = object() - - -def get_color_theme(theme): - ''' - Return the color theme to use - ''' - # Keep the heavy lifting out of the module space - import yaml - if not os.path.isfile(theme): - log.warning('The named theme {0} if not available'.format(theme)) - - # Late import to avoid circular import. - import salt.utils.files - try: - with salt.utils.files.fopen(theme, 'rb') as fp_: - colors = yaml.safe_load(fp_.read()) - ret = {} - for color in colors: - ret[color] = '\033[{0}m'.format(colors[color]) - if not isinstance(colors, dict): - log.warning('The theme file {0} is not a dict'.format(theme)) - return {} - return ret - except Exception: - log.warning('Failed to read the color theme {0}'.format(theme)) - return {} - - -def get_colors(use=True, theme=None): - ''' - Return the colors as an easy to use dict. Pass `False` to deactivate all - colors by setting them to empty strings. Pass a string containing only the - name of a single color to be used in place of all colors. Examples: - - .. code-block:: python - - colors = get_colors() # enable all colors - no_colors = get_colors(False) # disable all colors - red_colors = get_colors('RED') # set all colors to red - ''' - - colors = { - 'BLACK': TextFormat('black'), - 'DARK_GRAY': TextFormat('bold', 'black'), - 'RED': TextFormat('red'), - 'LIGHT_RED': TextFormat('bold', 'red'), - 'GREEN': TextFormat('green'), - 'LIGHT_GREEN': TextFormat('bold', 'green'), - 'YELLOW': TextFormat('yellow'), - 'LIGHT_YELLOW': TextFormat('bold', 'yellow'), - 'BLUE': TextFormat('blue'), - 'LIGHT_BLUE': TextFormat('bold', 'blue'), - 'MAGENTA': TextFormat('magenta'), - 'LIGHT_MAGENTA': TextFormat('bold', 'magenta'), - 'CYAN': TextFormat('cyan'), - 'LIGHT_CYAN': TextFormat('bold', 'cyan'), - 'LIGHT_GRAY': TextFormat('white'), - 'WHITE': TextFormat('bold', 'white'), - 'DEFAULT_COLOR': TextFormat('default'), - 'ENDC': TextFormat('reset'), - } - if theme: - colors.update(get_color_theme(theme)) - - if not use: - for color in colors: - colors[color] = '' - if isinstance(use, six.string_types): - # Try to set all of the colors to the passed color - if use in colors: - for color in colors: - # except for color reset - if color == 'ENDC': - continue - colors[color] = colors[use] - - return colors def get_context(template, line, num_lines=5, marker=None): @@ -944,7 +859,7 @@ def format_call(fun, aspec = salt.utils.args.get_function_argspec(fun, is_class_method=is_class_method) - arg_data = arg_lookup(fun, aspec) + arg_data = salt.utils.args.arg_lookup(fun, aspec) args = arg_data['args'] kwargs = arg_data['kwargs'] @@ -1052,63 +967,6 @@ def format_call(fun, return ret -def arg_lookup(fun, aspec=None): - ''' - Return a dict containing the arguments and default arguments to the - function. - ''' - import salt.utils.args - ret = {'kwargs': {}} - if aspec is None: - aspec = salt.utils.args.get_function_argspec(fun) - if aspec.defaults: - ret['kwargs'] = dict(zip(aspec.args[::-1], aspec.defaults[::-1])) - ret['args'] = [arg for arg in aspec.args if arg not in ret['kwargs']] - return ret - - -@jinja_filter('is_text_file') -def istextfile(fp_, blocksize=512): - ''' - Uses heuristics to guess whether the given file is text or binary, - by reading a single block of bytes from the file. - If more than 30% of the chars in the block are non-text, or there - are NUL ('\x00') bytes in the block, assume this is a binary file. - ''' - # Late import to avoid circular import. - import salt.utils.files - - int2byte = (lambda x: bytes((x,))) if six.PY3 else chr - text_characters = ( - b''.join(int2byte(i) for i in range(32, 127)) + - b'\n\r\t\f\b') - try: - block = fp_.read(blocksize) - except AttributeError: - # This wasn't an open filehandle, so treat it as a file path and try to - # open the file - try: - with salt.utils.files.fopen(fp_, 'rb') as fp2_: - block = fp2_.read(blocksize) - except IOError: - # Unable to open file, bail out and return false - return False - if b'\x00' in block: - # Files with null bytes are binary - return False - elif not block: - # An empty file is considered a valid text file - return True - try: - block.decode('utf-8') - return True - except UnicodeDecodeError: - pass - - nontext = block.translate(None, text_characters) - return float(len(nontext)) / len(block) <= 0.30 - - @jinja_filter('sorted_ignorecase') def isorted(to_sort): ''' @@ -1454,148 +1312,6 @@ def check_include_exclude(path_str, include_pat=None, exclude_pat=None): return ret -def gen_state_tag(low): - ''' - Generate the running dict tag string from the low data structure - ''' - return '{0[state]}_|-{0[__id__]}_|-{0[name]}_|-{0[fun]}'.format(low) - - -def search_onfail_requisites(sid, highstate): - """ - For a particular low chunk, search relevant onfail related - states - """ - onfails = [] - if '_|-' in sid: - st = salt.state.split_low_tag(sid) - else: - st = {'__id__': sid} - for fstate, fchunks in six.iteritems(highstate): - if fstate == st['__id__']: - continue - else: - for mod_, fchunk in six.iteritems(fchunks): - if ( - not isinstance(mod_, six.string_types) or - mod_.startswith('__') - ): - continue - else: - if not isinstance(fchunk, list): - continue - else: - # bydefault onfail will fail, but you can - # set onfail_stop: False to prevent the highstate - # to stop if you handle it - onfail_handled = False - for fdata in fchunk: - if not isinstance(fdata, dict): - continue - onfail_handled = (fdata.get('onfail_stop', True) - is False) - if onfail_handled: - break - if not onfail_handled: - continue - for fdata in fchunk: - if not isinstance(fdata, dict): - continue - for knob, fvalue in six.iteritems(fdata): - if knob != 'onfail': - continue - for freqs in fvalue: - for fmod, fid in six.iteritems(freqs): - if not ( - fid == st['__id__'] and - fmod == st.get('state', fmod) - ): - continue - onfails.append((fstate, mod_, fchunk)) - return onfails - - -def check_onfail_requisites(state_id, state_result, running, highstate): - ''' - When a state fail and is part of a highstate, check - if there is onfail requisites. - When we find onfail requisites, we will consider the state failed - only if at least one of those onfail requisites also failed - - Returns: - - True: if onfail handlers suceeded - False: if one on those handler failed - None: if the state does not have onfail requisites - - ''' - nret = None - if ( - state_id and state_result and - highstate and isinstance(highstate, dict) - ): - onfails = search_onfail_requisites(state_id, highstate) - if onfails: - for handler in onfails: - fstate, mod_, fchunk = handler - ofresult = True - for rstateid, rstate in six.iteritems(running): - if '_|-' in rstateid: - st = salt.state.split_low_tag(rstateid) - # in case of simple state, try to guess - else: - id_ = rstate.get('__id__', rstateid) - if not id_: - raise ValueError('no state id') - st = {'__id__': id_, 'state': mod_} - if mod_ == st['state'] and fstate == st['__id__']: - ofresult = rstate.get('result', _empty) - if ofresult in [False, True]: - nret = ofresult - if ofresult is False: - # as soon as we find an errored onfail, we stop - break - # consider that if we parsed onfailes without changing - # the ret, that we have failed - if nret is None: - nret = False - return nret - - -def check_state_result(running, recurse=False, highstate=None): - ''' - Check the total return value of the run and determine if the running - dict has any issues - ''' - if not isinstance(running, dict): - return False - - if not running: - return False - - ret = True - for state_id, state_result in six.iteritems(running): - if not recurse and not isinstance(state_result, dict): - ret = False - if ret and isinstance(state_result, dict): - result = state_result.get('result', _empty) - if result is False: - ret = False - # only override return value if we are not already failed - elif result is _empty and isinstance(state_result, dict) and ret: - ret = check_state_result( - state_result, recurse=True, highstate=highstate) - # if we detect a fail, check for onfail requisites - if not ret: - # ret can be None in case of no onfail reqs, recast it to bool - ret = bool(check_onfail_requisites(state_id, state_result, - running, highstate)) - # return as soon as we got a failure - if not ret: - break - return ret - - def st_mode_to_octal(mode): ''' Convert the st_mode value from a stat(2) call (as returned from os.stat()) @@ -1661,7 +1377,7 @@ def is_true(value=None): pass # Now check for truthiness - if isinstance(value, (int, float)): + if isinstance(value, (six.integer_types, float)): return value > 0 elif isinstance(value, six.string_types): return str(value).lower() == 'true' @@ -1976,52 +1692,6 @@ def compare_lists(old=None, new=None): return ret -def argspec_report(functions, module=''): - ''' - Pass in a functions dict as it is returned from the loader and return the - argspec function signatures - ''' - import salt.utils.args - ret = {} - if '*' in module or '.' in module: - for fun in fnmatch.filter(functions, module): - try: - aspec = salt.utils.args.get_function_argspec(functions[fun]) - except TypeError: - # this happens if not callable - continue - - args, varargs, kwargs, defaults = aspec - - ret[fun] = {} - ret[fun]['args'] = args if args else None - ret[fun]['defaults'] = defaults if defaults else None - ret[fun]['varargs'] = True if varargs else None - ret[fun]['kwargs'] = True if kwargs else None - - else: - # "sys" should just match sys without also matching sysctl - moduledot = module + '.' - - for fun in functions: - if fun.startswith(moduledot): - try: - aspec = salt.utils.args.get_function_argspec(functions[fun]) - except TypeError: - # this happens if not callable - continue - - args, varargs, kwargs, defaults = aspec - - ret[fun] = {} - ret[fun]['args'] = args if args else None - ret[fun]['defaults'] = defaults if defaults else None - ret[fun]['varargs'] = True if varargs else None - ret[fun]['kwargs'] = True if kwargs else None - - return ret - - @jinja_filter('json_decode_list') def decode_list(data): ''' @@ -2140,7 +1810,7 @@ def repack_dictlist(data, if val_cb is None: val_cb = lambda x, y: y - valid_non_dict = (six.string_types, int, float) + valid_non_dict = (six.string_types, six.integer_types, float) if isinstance(data, list): for element in data: if isinstance(element, valid_non_dict): @@ -2817,6 +2487,44 @@ def shlex_split(s, **kwargs): return salt.utils.args.shlex_split(s, **kwargs) +def arg_lookup(fun, aspec=None): + ''' + Return a dict containing the arguments and default arguments to the + function. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.args + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.arg_lookup\' detected. This function has been ' + 'moved to \'salt.utils.args.arg_lookup\' as of Salt Oxygen. This ' + 'warning will be removed in Salt Neon.' + ) + return salt.utils.args.arg_lookup(fun, aspec=aspec) + + +def argspec_report(functions, module=''): + ''' + Pass in a functions dict as it is returned from the loader and return the + argspec function signatures + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.args + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.argspec_report\' detected. This function has been ' + 'moved to \'salt.utils.args.argspec_report\' as of Salt Oxygen. This ' + 'warning will be removed in Salt Neon.' + ) + return salt.utils.args.argspec_report(functions, module=module) + + def which(exe=None): ''' Python clone of /usr/bin/which @@ -3272,6 +2980,28 @@ def mkstemp(*args, **kwargs): return salt.utils.files.mkstemp(*args, **kwargs) +@jinja_filter('is_text_file') +def istextfile(fp_, blocksize=512): + ''' + Uses heuristics to guess whether the given file is text or binary, + by reading a single block of bytes from the file. + If more than 30% of the chars in the block are non-text, or there + are NUL ('\x00') bytes in the block, assume this is a binary file. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.files + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.istextfile\' detected. This function has been moved ' + 'to \'salt.utils.files.is_text_file\' as of Salt Oxygen. This warning will ' + 'be removed in Salt Neon.' + ) + return salt.utils.files.is_text_file(fp_, blocksize=blocksize) + + def str_version_to_evr(verstring): ''' Split the package version string into epoch, version and release. @@ -3435,3 +3165,135 @@ def kwargs_warn_until(kwargs, stacklevel=stacklevel, _version_info_=_version_info_, _dont_call_warnings=_dont_call_warnings) + + +def get_color_theme(theme): + ''' + Return the color theme to use + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.color + import salt.utils.versions + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_color_theme\' detected. This function has ' + 'been moved to \'salt.utils.color.get_color_theme\' as of Salt ' + 'Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.color.get_color_theme(theme) + + +def get_colors(use=True, theme=None): + ''' + Return the colors as an easy to use dict. Pass `False` to deactivate all + colors by setting them to empty strings. Pass a string containing only the + name of a single color to be used in place of all colors. Examples: + + .. code-block:: python + + colors = get_colors() # enable all colors + no_colors = get_colors(False) # disable all colors + red_colors = get_colors('RED') # set all colors to red + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.color + import salt.utils.versions + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_colors\' detected. This function has ' + 'been moved to \'salt.utils.color.get_colors\' as of Salt ' + 'Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.color.get_colors(use=use, theme=theme) + + +def gen_state_tag(low): + ''' + Generate the running dict tag string from the low data structure + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.state + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.gen_state_tag\' detected. This function has been ' + 'moved to \'salt.utils.state.gen_tag\' as of Salt Oxygen. This warning ' + 'will be removed in Salt Neon.' + ) + return salt.utils.state.gen_tag(low) + + +def search_onfail_requisites(sid, highstate): + ''' + For a particular low chunk, search relevant onfail related states + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.state + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.search_onfail_requisites\' detected. This function ' + 'has been moved to \'salt.utils.state.search_onfail_requisites\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.state.search_onfail_requisites(sid, highstate) + + +def check_onfail_requisites(state_id, state_result, running, highstate): + ''' + When a state fail and is part of a highstate, check + if there is onfail requisites. + When we find onfail requisites, we will consider the state failed + only if at least one of those onfail requisites also failed + + Returns: + + True: if onfail handlers suceeded + False: if one on those handler failed + None: if the state does not have onfail requisites + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.state + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.check_onfail_requisites\' detected. This function ' + 'has been moved to \'salt.utils.state.check_onfail_requisites\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.state.check_onfail_requisites( + state_id, state_result, running, highstate + ) + + +def check_state_result(running, recurse=False, highstate=None): + ''' + Check the total return value of the run and determine if the running + dict has any issues + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.state + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.check_state_result\' detected. This function ' + 'has been moved to \'salt.utils.state.check_result\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.state.check_result( + running, recurse=recurse, highstate=highstate + ) diff --git a/salt/utils/args.py b/salt/utils/args.py index 8c55f845c6..7d47c52862 100644 --- a/salt/utils/args.py +++ b/salt/utils/args.py @@ -2,19 +2,19 @@ ''' Functions used for CLI argument handling ''' -from __future__ import absolute_import # Import python libs +from __future__ import absolute_import +import fnmatch +import inspect import re import shlex -import inspect # Import salt libs -import salt.utils.jid from salt.exceptions import SaltInvocationError - -# Import 3rd-party libs from salt.ext import six +from salt.ext.six.moves import zip # pylint: disable=import-error,redefined-builtin +import salt.utils.jid if six.PY3: @@ -268,3 +268,62 @@ def shlex_split(s, **kwargs): return shlex.split(s, **kwargs) else: return s + + +def arg_lookup(fun, aspec=None): + ''' + Return a dict containing the arguments and default arguments to the + function. + ''' + ret = {'kwargs': {}} + if aspec is None: + aspec = get_function_argspec(fun) + if aspec.defaults: + ret['kwargs'] = dict(zip(aspec.args[::-1], aspec.defaults[::-1])) + ret['args'] = [arg for arg in aspec.args if arg not in ret['kwargs']] + return ret + + +def argspec_report(functions, module=''): + ''' + Pass in a functions dict as it is returned from the loader and return the + argspec function signatures + ''' + ret = {} + if '*' in module or '.' in module: + for fun in fnmatch.filter(functions, module): + try: + aspec = get_function_argspec(functions[fun]) + except TypeError: + # this happens if not callable + continue + + args, varargs, kwargs, defaults = aspec + + ret[fun] = {} + ret[fun]['args'] = args if args else None + ret[fun]['defaults'] = defaults if defaults else None + ret[fun]['varargs'] = True if varargs else None + ret[fun]['kwargs'] = True if kwargs else None + + else: + # "sys" should just match sys without also matching sysctl + module_dot = module + '.' + + for fun in functions: + if fun.startswith(module_dot): + try: + aspec = get_function_argspec(functions[fun]) + except TypeError: + # this happens if not callable + continue + + args, varargs, kwargs, defaults = aspec + + ret[fun] = {} + ret[fun]['args'] = args if args else None + ret[fun]['defaults'] = defaults if defaults else None + ret[fun]['varargs'] = True if varargs else None + ret[fun]['kwargs'] = True if kwargs else None + + return ret diff --git a/salt/utils/boto.py b/salt/utils/boto.py index ee0a795ee6..86b09a8e7d 100644 --- a/salt/utils/boto.py +++ b/salt/utils/boto.py @@ -160,7 +160,7 @@ def cache_id_func(service): ''' Returns a partial `cache_id` function for the provided service. - ... code-block:: python + .. code-block:: python cache_id = __utils__['boto.cache_id_func']('ec2') cache_id('myinstance', 'i-a1b2c3') @@ -209,7 +209,7 @@ def get_connection_func(service, module=None): ''' Returns a partial `get_connection` function for the provided service. - ... code-block:: python + .. code-block:: python get_conn = __utils__['boto.get_connection_func']('ec2') conn = get_conn() diff --git a/salt/utils/boto3.py b/salt/utils/boto3.py index 2d0fa12fc1..866ab7ed17 100644 --- a/salt/utils/boto3.py +++ b/salt/utils/boto3.py @@ -182,7 +182,7 @@ def cache_id_func(service): ''' Returns a partial `cache_id` function for the provided service. - ... code-block:: python + .. code-block:: python cache_id = __utils__['boto.cache_id_func']('ec2') cache_id('myinstance', 'i-a1b2c3') @@ -233,7 +233,7 @@ def get_connection_func(service, module=None): ''' Returns a partial `get_connection` function for the provided service. - ... code-block:: python + .. code-block:: python get_conn = __utils__['boto.get_connection_func']('ec2') conn = get_conn() @@ -329,16 +329,6 @@ def paged_call(function, *args, **kwargs): kwargs[marker_arg] = marker -def get_role_arn(name, region=None, key=None, keyid=None, profile=None): - if name.startswith('arn:aws:iam:'): - return name - - account_id = __salt__['boto_iam.get_account_id']( - region=region, key=key, keyid=keyid, profile=profile - ) - return 'arn:aws:iam::{0}:role/{1}'.format(account_id, name) - - def ordered(obj): if isinstance(obj, (list, tuple)): return sorted(ordered(x) for x in obj) diff --git a/salt/utils/cloud.py b/salt/utils/cloud.py index 7203ca346a..b013c48f30 100644 --- a/salt/utils/cloud.py +++ b/salt/utils/cloud.py @@ -292,12 +292,14 @@ def salt_config_to_yaml(configuration, line_break='\n'): Dumper=SafeOrderedDumper) -def bootstrap(vm_, opts): +def bootstrap(vm_, opts=None): ''' This is the primary entry point for logging into any system (POSIX or Windows) to install Salt. It will make the decision on its own as to which deploy function to call. ''' + if opts is None: + opts = __opts__ deploy_config = salt.config.get_cloud_config_value( 'deploy', vm_, opts, default=False) @@ -457,7 +459,7 @@ def bootstrap(vm_, opts): 'wait_for_passwd_maxtries', vm_, opts, default=15 ), 'preflight_cmds': salt.config.get_cloud_config_value( - 'preflight_cmds', vm_, __opts__, default=[] + 'preflight_cmds', vm_, opts, default=[] ), 'cloud_grains': {'driver': vm_['driver'], 'provider': vm_['provider'], diff --git a/salt/utils/color.py b/salt/utils/color.py new file mode 100644 index 0000000000..21efb315dc --- /dev/null +++ b/salt/utils/color.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +''' +Functions used for CLI color themes. +''' + +# Import Python libs +from __future__ import absolute_import +import logging +import os + +# Import Salt libs +from salt.ext import six +from salt.textformat import TextFormat +import salt.utils.files + +log = logging.getLogger(__name__) + + +def get_color_theme(theme): + ''' + Return the color theme to use + ''' + # Keep the heavy lifting out of the module space + import yaml + if not os.path.isfile(theme): + log.warning('The named theme {0} if not available'.format(theme)) + + try: + with salt.utils.files.fopen(theme, 'rb') as fp_: + colors = yaml.safe_load(fp_.read()) + ret = {} + for color in colors: + ret[color] = '\033[{0}m'.format(colors[color]) + if not isinstance(colors, dict): + log.warning('The theme file {0} is not a dict'.format(theme)) + return {} + return ret + except Exception: + log.warning('Failed to read the color theme {0}'.format(theme)) + return {} + + +def get_colors(use=True, theme=None): + ''' + Return the colors as an easy to use dict. Pass `False` to deactivate all + colors by setting them to empty strings. Pass a string containing only the + name of a single color to be used in place of all colors. Examples: + + .. code-block:: python + + colors = get_colors() # enable all colors + no_colors = get_colors(False) # disable all colors + red_colors = get_colors('RED') # set all colors to red + + ''' + + colors = { + 'BLACK': TextFormat('black'), + 'DARK_GRAY': TextFormat('bold', 'black'), + 'RED': TextFormat('red'), + 'LIGHT_RED': TextFormat('bold', 'red'), + 'GREEN': TextFormat('green'), + 'LIGHT_GREEN': TextFormat('bold', 'green'), + 'YELLOW': TextFormat('yellow'), + 'LIGHT_YELLOW': TextFormat('bold', 'yellow'), + 'BLUE': TextFormat('blue'), + 'LIGHT_BLUE': TextFormat('bold', 'blue'), + 'MAGENTA': TextFormat('magenta'), + 'LIGHT_MAGENTA': TextFormat('bold', 'magenta'), + 'CYAN': TextFormat('cyan'), + 'LIGHT_CYAN': TextFormat('bold', 'cyan'), + 'LIGHT_GRAY': TextFormat('white'), + 'WHITE': TextFormat('bold', 'white'), + 'DEFAULT_COLOR': TextFormat('default'), + 'ENDC': TextFormat('reset'), + } + if theme: + colors.update(get_color_theme(theme)) + + if not use: + for color in colors: + colors[color] = '' + if isinstance(use, six.string_types): + # Try to set all of the colors to the passed color + if use in colors: + for color in colors: + # except for color reset + if color == 'ENDC': + continue + colors[color] = colors[use] + + return colors diff --git a/salt/utils/configparser.py b/salt/utils/configparser.py new file mode 100644 index 0000000000..d060f9ddd2 --- /dev/null +++ b/salt/utils/configparser.py @@ -0,0 +1,269 @@ +# -*- coding: utf-8 -*- +# Import Python libs +from __future__ import absolute_import +import re + +# Import Salt libs +import salt.utils.stringutils + +# Import 3rd-party libs +from salt.ext import six +from salt.ext.six.moves.configparser import * # pylint: disable=no-name-in-module,wildcard-import + +try: + from collections import OrderedDict as _default_dict +except ImportError: + # fallback for setup.py which hasn't yet built _collections + _default_dict = dict + + +# pylint: disable=string-substitution-usage-error +class GitConfigParser(RawConfigParser, object): # pylint: disable=undefined-variable + ''' + Custom ConfigParser which reads and writes git config files. + + READ A GIT CONFIG FILE INTO THE PARSER OBJECT + + >>> import salt.utils.configparser + >>> conf = salt.utils.configparser.GitConfigParser() + >>> conf.read('/home/user/.git/config') + + MAKE SOME CHANGES + + >>> # Change user.email + >>> conf.set('user', 'email', 'myaddress@mydomain.tld') + >>> # Add another refspec to the "origin" remote's "fetch" multivar + >>> conf.set_multivar('remote "origin"', 'fetch', '+refs/tags/*:refs/tags/*') + + WRITE THE CONFIG TO A FILEHANDLE + + >>> import salt.utils.files + >>> with salt.utils.files.fopen('/home/user/.git/config', 'w') as fh: + ... conf.write(fh) + >>> + ''' + DEFAULTSECT = u'DEFAULT' + SPACEINDENT = u' ' * 8 + + def __init__(self, defaults=None, dict_type=_default_dict, + allow_no_value=True): + ''' + Changes default value for allow_no_value from False to True + ''' + super(GitConfigParser, self).__init__( + defaults, dict_type, allow_no_value) + + def _read(self, fp, fpname): + ''' + Makes the following changes from the RawConfigParser: + + 1. Strip leading tabs from non-section-header lines. + 2. Treat 8 spaces at the beginning of a line as a tab. + 3. Treat lines beginning with a tab as options. + 4. Drops support for continuation lines. + 5. Multiple values for a given option are stored as a list. + 6. Keys and values are decoded to the system encoding. + ''' + cursect = None # None, or a dictionary + optname = None + lineno = 0 + e = None # None, or an exception + while True: + line = fp.readline() + if six.PY2: + line = line.decode(__salt_system_encoding__) + if not line: + break + lineno = lineno + 1 + # comment or blank line? + if line.strip() == u'' or line[0] in u'#;': + continue + if line.split(None, 1)[0].lower() == u'rem' and line[0] in u'rR': + # no leading whitespace + continue + # Replace space indentation with a tab. Allows parser to work + # properly in cases where someone has edited the git config by hand + # and indented using spaces instead of tabs. + if line.startswith(self.SPACEINDENT): + line = u'\t' + line[len(self.SPACEINDENT):] + # is it a section header? + mo = self.SECTCRE.match(line) + if mo: + sectname = mo.group(u'header') + if sectname in self._sections: + cursect = self._sections[sectname] + elif sectname == self.DEFAULTSECT: + cursect = self._defaults + else: + cursect = self._dict() + self._sections[sectname] = cursect + # So sections can't start with a continuation line + optname = None + # no section header in the file? + elif cursect is None: + raise MissingSectionHeaderError( # pylint: disable=undefined-variable + salt.utils.stringutils.to_str(fpname), + lineno, + salt.utils.stringutils.to_str(line)) + # an option line? + else: + mo = self._optcre.match(line.lstrip()) + if mo: + optname, vi, optval = mo.group(u'option', u'vi', u'value') + optname = self.optionxform(optname.rstrip()) + if optval is None: + optval = u'' + if optval: + if vi in (u'=', u':') and u';' in optval: + # ';' is a comment delimiter only if it follows + # a spacing character + pos = optval.find(u';') + if pos != -1 and optval[pos-1].isspace(): + optval = optval[:pos] + optval = optval.strip() + # Empty strings should be considered as blank strings + if optval in (u'""', u"''"): + optval = u'' + self._add_option(cursect, optname, optval) + else: + # a non-fatal parsing error occurred. set up the + # exception but keep going. the exception will be + # raised at the end of the file and will contain a + # list of all bogus lines + if not e: + e = ParsingError(fpname) # pylint: disable=undefined-variable + e.append(lineno, repr(line)) + # if any parsing errors occurred, raise an exception + if e: + raise e # pylint: disable=raising-bad-type + + def _string_check(self, value, allow_list=False): + ''' + Based on the string-checking code from the SafeConfigParser's set() + function, this enforces string values for config options. + ''' + if self._optcre is self.OPTCRE or value: + is_list = isinstance(value, list) + if is_list and not allow_list: + raise TypeError('option value cannot be a list unless allow_list is True') # future lint: disable=non-unicode-string + elif not is_list: + value = [value] + if not all(isinstance(x, six.string_types) for x in value): + raise TypeError('option values must be strings') # future lint: disable=non-unicode-string + + def get(self, section, option, as_list=False): + ''' + Adds an optional "as_list" argument to ensure a list is returned. This + is helpful when iterating over an option which may or may not be a + multivar. + ''' + ret = super(GitConfigParser, self).get(section, option) + if as_list and not isinstance(ret, list): + ret = [ret] + return ret + + def set(self, section, option, value=u''): + ''' + This is overridden from the RawConfigParser merely to change the + default value for the 'value' argument. + ''' + self._string_check(value) + super(GitConfigParser, self).set(section, option, value) + + def _add_option(self, sectdict, key, value): + if isinstance(value, list): + sectdict[key] = value + elif isinstance(value, six.string_types): + try: + sectdict[key].append(value) + except KeyError: + # Key not present, set it + sectdict[key] = value + except AttributeError: + # Key is present but the value is not a list. Make it into a list + # and then append to it. + sectdict[key] = [sectdict[key]] + sectdict[key].append(value) + else: + raise TypeError('Expected str or list for option value, got %s' % type(value).__name__) # future lint: disable=non-unicode-string + + def set_multivar(self, section, option, value=u''): + ''' + This function is unique to the GitConfigParser. It will add another + value for the option if it already exists, converting the option's + value to a list if applicable. + + If "value" is a list, then any existing values for the specified + section and option will be replaced with the list being passed. + ''' + self._string_check(value, allow_list=True) + if not section or section == self.DEFAULTSECT: + sectdict = self._defaults + else: + try: + sectdict = self._sections[section] + except KeyError: + raise NoSectionError( # pylint: disable=undefined-variable + salt.utils.stringutils.to_str(section)) + key = self.optionxform(option) + self._add_option(sectdict, key, value) + + def remove_option_regexp(self, section, option, expr): + ''' + Remove an option with a value matching the expression. Works on single + values and multivars. + ''' + if not section or section == self.DEFAULTSECT: + sectdict = self._defaults + else: + try: + sectdict = self._sections[section] + except KeyError: + raise NoSectionError( # pylint: disable=undefined-variable + salt.utils.stringutils.to_str(section)) + option = self.optionxform(option) + if option not in sectdict: + return False + regexp = re.compile(expr) + if isinstance(sectdict[option], list): + new_list = [x for x in sectdict[option] if not regexp.search(x)] + # Revert back to a list if we removed all but one item + if len(new_list) == 1: + new_list = new_list[0] + existed = new_list != sectdict[option] + if existed: + del sectdict[option] + sectdict[option] = new_list + del new_list + else: + existed = bool(regexp.search(sectdict[option])) + if existed: + del sectdict[option] + return existed + + def write(self, fp_): + ''' + Makes the following changes from the RawConfigParser: + + 1. Prepends options with a tab character. + 2. Does not write a blank line between sections. + 3. When an option's value is a list, a line for each option is written. + This allows us to support multivars like a remote's "fetch" option. + 4. Drops support for continuation lines. + ''' + convert = salt.utils.stringutils.to_bytes \ + if u'b' in fp_.mode \ + else salt.utils.stringutils.to_str + if self._defaults: + fp_.write(convert(u'[%s]\n' % self.DEFAULTSECT)) + for (key, value) in six.iteritems(self._defaults): + value = salt.utils.stringutils.to_unicode(value).replace(u'\n', u'\n\t') + fp_.write(convert(u'%s = %s\n' % (key, value))) + for section in self._sections: + fp_.write(convert(u'[%s]\n' % section)) + for (key, value) in six.iteritems(self._sections[section]): + if (value is not None) or (self._optcre == self.OPTCRE): + if not isinstance(value, list): + value = [value] + for item in value: + fp_.write(convert(u'\t%s\n' % u' = '.join((key, item)).rstrip())) diff --git a/salt/utils/dictdiffer.py b/salt/utils/dictdiffer.py index 2438a75ebc..b007742083 100644 --- a/salt/utils/dictdiffer.py +++ b/salt/utils/dictdiffer.py @@ -8,6 +8,8 @@ Originally posted at http://stackoverflow.com/questions/1165352/fast-comparison-between-two-python-dictionary/1165552#1165552 Available at repository: https://github.com/hughdbrown/dictdiffer + + Added the ability to recursively compare dictionaries ''' from __future__ import absolute_import from copy import deepcopy @@ -77,3 +79,308 @@ def deep_diff(old, new, ignore=None): if new: res['new'] = new return res + + +def recursive_diff(past_dict, current_dict, ignore_missing_keys=True): + ''' + Returns a RecursiveDictDiffer object that computes the recursive diffs + between two dictionaries + + past_dict + Past dictionary + + current_dict + Current dictionary + + ignore_missing_keys + Flag specifying whether to ignore keys that no longer exist in the + current_dict, but exist in the past_dict. If true, the diff will + not contain the missing keys. + Default is True. + ''' + return RecursiveDictDiffer(past_dict, current_dict, ignore_missing_keys) + + +class RecursiveDictDiffer(DictDiffer): + ''' + Calculates a recursive diff between the current_dict and the past_dict + creating a diff in the format + + {'new': new_value, 'old': old_value} + + It recursively searches differences in common keys whose values are + dictionaries creating a diff dict in the format + + {'common_key' : {'new': new_value, 'old': old_value} + + The class overrides all DictDiffer methods, returning lists of keys and + subkeys using the . notation (i.e 'common_key1.common_key2.changed_key') + + The class provides access to: + (1) the added, removed, changes keys and subkeys (using the . notation) + ``added``, ``removed``, ``changed`` methods + (2) the diffs in the format aboce (diff property) + ``diffs`` property + (3) a dict with the new changed values only (new_values property) + ``new_values`` property + (4) a dict with the old changed values only (old_values property) + ``old_values`` property + (5) a string representation of the changes in the format: + ``changes_str`` property + + Note: + The <_null_> value is a reserved value + +.. code-block:: text + + common_key1: + common_key2: + changed_key1 from '' to '' + changed_key2 from '[, ..]' to '[, ..]' + common_key3: + changed_key3 from to + + ''' + NONE_VALUE = '<_null_>' + + def __init__(self, past_dict, current_dict, ignore_missing_keys): + ''' + past_dict + Past dictionary. + + current_dict + Current dictionary. + + ignore_missing_keys + Flag specifying whether to ignore keys that no longer exist in the + current_dict, but exist in the past_dict. If true, the diff will + not contain the missing keys. + ''' + super(RecursiveDictDiffer, self).__init__(current_dict, past_dict) + self._diffs = \ + self._get_diffs(self.current_dict, self.past_dict, + ignore_missing_keys) + # Ignores unet values when assessing the changes + self.ignore_unset_values = True + + @classmethod + def _get_diffs(cls, dict1, dict2, ignore_missing_keys): + ''' + Returns a dict with the differences between dict1 and dict2 + + Notes: + Keys that only exist in dict2 are not included in the diff if + ignore_missing_keys is True, otherwise they are + Simple compares are done on lists + ''' + ret_dict = {} + for p in dict1.keys(): + if p not in dict2: + ret_dict.update({p: {'new': dict1[p], 'old': cls.NONE_VALUE}}) + elif dict1[p] != dict2[p]: + if isinstance(dict1[p], dict) and isinstance(dict2[p], dict): + sub_diff_dict = cls._get_diffs(dict1[p], dict2[p], + ignore_missing_keys) + if sub_diff_dict: + ret_dict.update({p: sub_diff_dict}) + else: + ret_dict.update({p: {'new': dict1[p], 'old': dict2[p]}}) + if not ignore_missing_keys: + for p in dict2.keys(): + if p not in dict1.keys(): + ret_dict.update({p: {'new': cls.NONE_VALUE, + 'old': dict2[p]}}) + return ret_dict + + @classmethod + def _get_values(cls, diff_dict, type='new'): + ''' + Returns a dictionaries with the 'new' values in a diff dict. + + type + Which values to return, 'new' or 'old' + ''' + ret_dict = {} + for p in diff_dict.keys(): + if type in diff_dict[p].keys(): + ret_dict.update({p: diff_dict[p][type]}) + else: + ret_dict.update( + {p: cls._get_values(diff_dict[p], type=type)}) + return ret_dict + + @classmethod + def _get_changes(cls, diff_dict): + ''' + Returns a list of string message with the differences in a diff dict. + + Each inner difference is tabulated two space deeper + ''' + changes_strings = [] + for p in diff_dict.keys(): + if sorted(diff_dict[p].keys()) == ['new', 'old']: + # Some string formatting + old_value = diff_dict[p]['old'] + if diff_dict[p]['old'] == cls.NONE_VALUE: + old_value = 'nothing' + elif isinstance(diff_dict[p]['old'], str): + old_value = '\'{0}\''.format(diff_dict[p]['old']) + elif isinstance(diff_dict[p]['old'], list): + old_value = '\'{0}\''.format( + ', '.join(diff_dict[p]['old'])) + new_value = diff_dict[p]['new'] + if diff_dict[p]['new'] == cls.NONE_VALUE: + new_value = 'nothing' + elif isinstance(diff_dict[p]['new'], str): + new_value = '\'{0}\''.format(diff_dict[p]['new']) + elif isinstance(diff_dict[p]['new'], list): + new_value = '\'{0}\''.format(', '.join(diff_dict[p]['new'])) + changes_strings.append('{0} from {1} to {2}'.format( + p, old_value, new_value)) + else: + sub_changes = cls._get_changes(diff_dict[p]) + if sub_changes: + changes_strings.append('{0}:'.format(p)) + changes_strings.extend([' {0}'.format(c) + for c in sub_changes]) + return changes_strings + + def added(self): + ''' + Returns all keys that have been added. + + If the keys are in child dictionaries they will be represented with + . notation + ''' + def _added(diffs, prefix): + keys = [] + for key in diffs.keys(): + if isinstance(diffs[key], dict) and 'old' not in diffs[key]: + keys.extend(_added(diffs[key], + prefix='{0}{1}.'.format(prefix, key))) + elif diffs[key]['old'] == self.NONE_VALUE: + if isinstance(diffs[key]['new'], dict): + keys.extend( + _added(diffs[key]['new'], + prefix='{0}{1}.'.format(prefix, key))) + else: + keys.append('{0}{1}'.format(prefix, key)) + return keys + + return _added(self._diffs, prefix='') + + def removed(self): + ''' + Returns all keys that have been removed. + + If the keys are in child dictionaries they will be represented with + . notation + ''' + def _removed(diffs, prefix): + keys = [] + for key in diffs.keys(): + if isinstance(diffs[key], dict) and 'old' not in diffs[key]: + keys.extend(_removed(diffs[key], + prefix='{0}{1}.'.format(prefix, key))) + elif diffs[key]['new'] == self.NONE_VALUE: + keys.append('{0}{1}'.format(prefix, key)) + elif isinstance(diffs[key]['new'], dict): + keys.extend( + _removed(diffs[key]['new'], + prefix='{0}{1}.'.format(prefix, key))) + return keys + + return _removed(self._diffs, prefix='') + + def changed(self): + ''' + Returns all keys that have been changed. + + If the keys are in child dictionaries they will be represented with + . notation + ''' + def _changed(diffs, prefix): + keys = [] + for key in diffs.keys(): + if not isinstance(diffs[key], dict): + continue + + if isinstance(diffs[key], dict) and 'old' not in diffs[key]: + keys.extend(_changed(diffs[key], + prefix='{0}{1}.'.format(prefix, key))) + continue + if self.ignore_unset_values: + if 'old' in diffs[key] and 'new' in diffs[key] and \ + diffs[key]['old'] != self.NONE_VALUE and \ + diffs[key]['new'] != self.NONE_VALUE: + if isinstance(diffs[key]['new'], dict): + keys.extend( + _changed(diffs[key]['new'], + prefix='{0}{1}.'.format(prefix, key))) + else: + keys.append('{0}{1}'.format(prefix, key)) + elif isinstance(diffs[key], dict): + keys.extend( + _changed(diffs[key], + prefix='{0}{1}.'.format(prefix, key))) + else: + if 'old' in diffs[key] and 'new' in diffs[key]: + if isinstance(diffs[key]['new'], dict): + keys.extend( + _changed(diffs[key]['new'], + prefix='{0}{1}.'.format(prefix, key))) + else: + keys.append('{0}{1}'.format(prefix, key)) + elif isinstance(diffs[key], dict): + keys.extend( + _changed(diffs[key], + prefix='{0}{1}.'.format(prefix, key))) + + return keys + + return _changed(self._diffs, prefix='') + + def unchanged(self): + ''' + Returns all keys that have been unchanged. + + If the keys are in child dictionaries they will be represented with + . notation + ''' + def _unchanged(current_dict, diffs, prefix): + keys = [] + for key in current_dict.keys(): + if key not in diffs: + keys.append('{0}{1}'.format(prefix, key)) + elif isinstance(current_dict[key], dict): + if 'new' in diffs[key]: + # There is a diff + continue + else: + keys.extend( + _unchanged(current_dict[key], + diffs[key], + prefix='{0}{1}.'.format(prefix, key))) + + return keys + return _unchanged(self.current_dict, self._diffs, prefix='') + + @property + def diffs(self): + '''Returns a dict with the recursive diffs current_dict - past_dict''' + return self._diffs + + @property + def new_values(self): + '''Returns a dictionary with the new values''' + return self._get_values(self._diffs, type='new') + + @property + def old_values(self): + '''Returns a dictionary with the old values''' + return self._get_values(self._diffs, type='old') + + @property + def changes_str(self): + '''Returns a string describing the changes''' + return '\n'.join(self._get_changes(self._diffs)) diff --git a/salt/utils/files.py b/salt/utils/files.py index 0d87ab6219..c55ac86324 100644 --- a/salt/utils/files.py +++ b/salt/utils/files.py @@ -25,6 +25,7 @@ from salt.utils.decorators.jinja import jinja_filter # Import 3rd-party libs from salt.ext import six +from salt.ext.six.moves import range try: import fcntl HAS_FCNTL = True @@ -34,10 +35,21 @@ except ImportError: log = logging.getLogger(__name__) +LOCAL_PROTOS = ('', 'file') REMOTE_PROTOS = ('http', 'https', 'ftp', 'swift', 's3') VALID_PROTOS = ('salt', 'file') + REMOTE_PROTOS TEMPFILE_PREFIX = '__salt.tmp.' +HASHES = { + 'sha512': 128, + 'sha384': 96, + 'sha256': 64, + 'sha224': 56, + 'sha1': 40, + 'md5': 32, +} +HASHES_REVMAP = dict([(y, x) for x, y in six.iteritems(HASHES)]) + def guess_archive_type(name): ''' @@ -498,3 +510,53 @@ def safe_filepath(file_path_name): return os.sep.join([drive, path]) else: return path + + +@jinja_filter('is_text_file') +def is_text_file(fp_, blocksize=512): + ''' + Uses heuristics to guess whether the given file is text or binary, + by reading a single block of bytes from the file. + If more than 30% of the chars in the block are non-text, or there + are NUL ('\x00') bytes in the block, assume this is a binary file. + ''' + int2byte = (lambda x: bytes((x,))) if six.PY3 else chr + text_characters = ( + b''.join(int2byte(i) for i in range(32, 127)) + + b'\n\r\t\f\b') + try: + block = fp_.read(blocksize) + except AttributeError: + # This wasn't an open filehandle, so treat it as a file path and try to + # open the file + try: + with fopen(fp_, 'rb') as fp2_: + block = fp2_.read(blocksize) + except IOError: + # Unable to open file, bail out and return false + return False + if b'\x00' in block: + # Files with null bytes are binary + return False + elif not block: + # An empty file is considered a valid text file + return True + try: + block.decode('utf-8') + return True + except UnicodeDecodeError: + pass + + nontext = block.translate(None, text_characters) + return float(len(nontext)) / len(block) <= 0.30 + + +def remove(path): + ''' + Runs os.remove(path) and suppresses the OSError if the file doesn't exist + ''' + try: + os.remove(path) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py index 8c7750c596..a0b0b20ca1 100644 --- a/salt/utils/gitfs.py +++ b/salt/utils/gitfs.py @@ -21,6 +21,7 @@ from datetime import datetime # Import salt libs import salt.utils +import salt.utils.configparser import salt.utils.files import salt.utils.itertools import salt.utils.path @@ -29,13 +30,12 @@ import salt.utils.stringutils import salt.utils.url import salt.utils.versions import salt.fileserver -from salt.config import DEFAULT_MASTER_OPTS as __DEFAULT_MASTER_OPTS +from salt.config import DEFAULT_MASTER_OPTS as _DEFAULT_MASTER_OPTS from salt.utils.odict import OrderedDict from salt.utils.process import os_is_running as pid_exists from salt.exceptions import ( FileserverConfigError, GitLockError, - GitRemoteError, get_error_message ) from salt.utils.event import tagify @@ -44,8 +44,7 @@ from salt.utils.versions import LooseVersion as _LooseVersion # Import third party libs from salt.ext import six -VALID_PROVIDERS = ('pygit2', 'gitpython') -VALID_REF_TYPES = __DEFAULT_MASTER_OPTS['gitfs_ref_types'] +VALID_REF_TYPES = _DEFAULT_MASTER_OPTS['gitfs_ref_types'] # Optional per-remote params that can only be used on a per-remote basis, and # thus do not have defaults in salt/config.py. @@ -176,7 +175,7 @@ class GitProvider(object): directly. self.provider should be set in the sub-class' __init__ function before - invoking GitProvider.__init__(). + invoking the parent class' __init__. ''' def __init__(self, opts, remote, per_remote_defaults, per_remote_only, override_params, cache_root, role='gitfs'): @@ -328,6 +327,28 @@ class GitProvider(object): setattr(self, '_' + key, self.conf[key]) self.add_conf_overlay(key) + if not hasattr(self, 'refspecs'): + # This was not specified as a per-remote overrideable parameter + # when instantiating an instance of a GitBase subclass. Make sure + # that we set this attribute so we at least have a sane default and + # are able to fetch. + key = '{0}_refspecs'.format(self.role) + try: + default_refspecs = _DEFAULT_MASTER_OPTS[key] + except KeyError: + log.critical( + 'The \'%s\' option has no default value in ' + 'salt/config/__init__.py.', key + ) + failhard(self.role) + + setattr(self, 'refspecs', default_refspecs) + log.debug( + 'The \'refspecs\' option was not explicitly defined as a ' + 'configurable parameter. Falling back to %s for %s remote ' + '\'%s\'.', default_refspecs, self.role, self.id + ) + for item in ('env_whitelist', 'env_blacklist'): val = getattr(self, item, None) if val: @@ -494,12 +515,6 @@ class GitProvider(object): return strip_sep(getattr(self, '_' + name)) setattr(cls, name, _getconf) - def add_refspecs(self, *refspecs): - ''' - This function must be overridden in a sub-class - ''' - raise NotImplementedError() - def check_root(self): ''' Check if the relative root path exists in the checked-out copy of the @@ -592,55 +607,95 @@ class GitProvider(object): success.append(msg) return success, failed - def configure_refspecs(self): + def enforce_git_config(self): ''' - Ensure that the configured refspecs are set + For the config options which need to be maintained in the git config, + ensure that the git config file is configured as desired. ''' - try: - refspecs = set(self.get_refspecs()) - except (git.exc.GitCommandError, GitRemoteError) as exc: - log.error( - 'Failed to get refspecs for %s remote \'%s\': %s', - self.role, - self.id, - exc - ) - return - - desired_refspecs = set(self.refspecs) - to_delete = refspecs - desired_refspecs if refspecs else set() - if to_delete: - # There is no native unset support in Pygit2, and GitPython just - # wraps the CLI anyway. So we'll just use the git CLI to - # --unset-all the config value. Then, we will add back all - # configured refspecs. This is more foolproof than trying to remove - # specific refspecs, as removing specific ones necessitates - # formulating a regex to match, and the fact that slashes and - # asterisks are in refspecs complicates this. - cmd_str = 'git config --unset-all remote.origin.fetch' - cmd = subprocess.Popen( - shlex.split(cmd_str), - close_fds=not salt.utils.platform.is_windows(), - cwd=os.path.dirname(self.gitdir), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - output = cmd.communicate()[0] - if cmd.returncode != 0: - log.error( - 'Failed to unset git config value for %s remote \'%s\'. ' - 'Output from \'%s\' follows:\n%s', - self.role, self.id, cmd_str, output - ) - return - # Since we had to remove all refspecs, we now need to add all - # desired refspecs to achieve the desired configuration. - to_add = desired_refspecs + git_config = os.path.join(self.gitdir, 'config') + conf = salt.utils.configparser.GitConfigParser() + if not conf.read(git_config): + log.error('Failed to read from git config file %s', git_config) else: - # We didn't need to delete any refspecs, so we'll only need to add - # the desired refspecs that aren't currently configured. - to_add = desired_refspecs - refspecs + # We are currently enforcing the following git config items: + # 1. Fetch URL + # 2. refspecs used in fetch + # 3. http.sslVerify + conf_changed = False + remote_section = 'remote "origin"' - self.add_refspecs(*to_add) + # 1. URL + try: + url = conf.get(remote_section, 'url') + except salt.utils.configparser.NoSectionError: + # First time we've init'ed this repo, we need to add the + # section for the remote to the git config + conf.add_section(remote_section) + conf_changed = True + url = None + log.debug( + 'Current fetch URL for %s remote \'%s\': %s (desired: %s)', + self.role, self.id, url, self.url + ) + if url != self.url: + conf.set(remote_section, 'url', self.url) + log.debug( + 'Fetch URL for %s remote \'%s\' set to %s', + self.role, self.id, self.url + ) + conf_changed = True + + # 2. refspecs + try: + refspecs = sorted( + conf.get(remote_section, 'fetch', as_list=True)) + except salt.utils.configparser.NoOptionError: + # No 'fetch' option present in the remote section. Should never + # happen, but if it does for some reason, don't let it cause a + # traceback. + refspecs = [] + desired_refspecs = sorted(self.refspecs) + log.debug( + 'Current refspecs for %s remote \'%s\': %s (desired: %s)', + self.role, self.id, refspecs, desired_refspecs + ) + if refspecs != desired_refspecs: + conf.set_multivar(remote_section, 'fetch', self.refspecs) + log.debug( + 'Refspecs for %s remote \'%s\' set to %s', + self.role, self.id, desired_refspecs + ) + conf_changed = True + + # 3. http.sslVerify + try: + ssl_verify = conf.get('http', 'sslVerify') + except salt.utils.configparser.NoSectionError: + conf.add_section('http') + ssl_verify = None + except salt.utils.configparser.NoOptionError: + ssl_verify = None + desired_ssl_verify = six.text_type(self.ssl_verify).lower() + log.debug( + 'Current http.sslVerify for %s remote \'%s\': %s (desired: %s)', + self.role, self.id, ssl_verify, desired_ssl_verify + ) + if ssl_verify != desired_ssl_verify: + conf.set('http', 'sslVerify', desired_ssl_verify) + log.debug( + 'http.sslVerify for %s remote \'%s\' set to %s', + self.role, self.id, desired_ssl_verify + ) + conf_changed = True + + # Write changes, if necessary + if conf_changed: + with salt.utils.files.fopen(git_config, 'w') as fp_: + conf.write(fp_) + log.debug( + 'Config updates for %s remote \'%s\' written to %s', + self.role, self.id, git_config + ) def fetch(self): ''' @@ -854,12 +909,6 @@ class GitProvider(object): else target return self.branch - def get_refspecs(self): - ''' - This function must be overridden in a sub-class - ''' - raise NotImplementedError() - def get_tree(self, tgt_env): ''' Return a tree object for the specified environment @@ -931,25 +980,10 @@ class GitPython(GitProvider): def __init__(self, opts, remote, per_remote_defaults, per_remote_only, override_params, cache_root, role='gitfs'): self.provider = 'gitpython' - GitProvider.__init__(self, opts, remote, per_remote_defaults, - per_remote_only, override_params, cache_root, role) - - def add_refspecs(self, *refspecs): - ''' - Add the specified refspecs to the "origin" remote - ''' - for refspec in refspecs: - try: - self.repo.git.config('--add', 'remote.origin.fetch', refspec) - log.debug( - 'Added refspec \'%s\' to %s remote \'%s\'', - refspec, self.role, self.id - ) - except git.exc.GitCommandError as exc: - log.error( - 'Failed to add refspec \'%s\' to %s remote \'%s\': %s', - refspec, self.role, self.id, exc - ) + super(GitPython, self).__init__( + opts, remote, per_remote_defaults, per_remote_only, + override_params, cache_root, role + ) def checkout(self): ''' @@ -1038,29 +1072,7 @@ class GitPython(GitProvider): return new self.gitdir = salt.utils.path.join(self.repo.working_dir, '.git') - - if not self.repo.remotes: - try: - self.repo.create_remote('origin', self.url) - except os.error: - # This exception occurs when two processes are trying to write - # to the git config at once, go ahead and pass over it since - # this is the only write. This should place a lock down. - pass - else: - new = True - - try: - ssl_verify = self.repo.git.config('--get', 'http.sslVerify') - except git.exc.GitCommandError: - ssl_verify = '' - desired_ssl_verify = str(self.ssl_verify).lower() - if ssl_verify != desired_ssl_verify: - self.repo.git.config('http.sslVerify', desired_ssl_verify) - - # Ensure that refspecs for the "origin" remote are set up as configured - if hasattr(self, 'refspecs'): - self.configure_refspecs() + self.enforce_git_config() return new @@ -1133,7 +1145,7 @@ class GitPython(GitProvider): new_objs = True cleaned = self.clean_stale_refs() - return bool(new_objs or cleaned) + return True if (new_objs or cleaned) else None def file_list(self, tgt_env): ''' @@ -1212,13 +1224,6 @@ class GitPython(GitProvider): return blob, blob.hexsha, blob.mode return None, None, None - def get_refspecs(self): - ''' - Return the configured refspecs - ''' - refspecs = self.repo.git.config('--get-all', 'remote.origin.fetch') - return [x.strip() for x in refspecs.splitlines()] - def get_tree_from_branch(self, ref): ''' Return a git.Tree object matching a head ref fetched into @@ -1266,29 +1271,10 @@ class Pygit2(GitProvider): def __init__(self, opts, remote, per_remote_defaults, per_remote_only, override_params, cache_root, role='gitfs'): self.provider = 'pygit2' - GitProvider.__init__(self, opts, remote, per_remote_defaults, - per_remote_only, override_params, cache_root, role) - - def add_refspecs(self, *refspecs): - ''' - Add the specified refspecs to the "origin" remote - ''' - for refspec in refspecs: - try: - self.repo.config.set_multivar( - 'remote.origin.fetch', - 'FOO', - refspec - ) - log.debug( - 'Added refspec \'%s\' to %s remote \'%s\'', - refspec, self.role, self.id - ) - except Exception as exc: - log.error( - 'Failed to add refspec \'%s\' to %s remote \'%s\': %s', - refspec, self.role, self.id, exc - ) + super(Pygit2, self).__init__( + opts, remote, per_remote_defaults, per_remote_only, + override_params, cache_root, role + ) def checkout(self): ''' @@ -1516,30 +1502,7 @@ class Pygit2(GitProvider): return new self.gitdir = salt.utils.path.join(self.repo.workdir, '.git') - - if not self.repo.remotes: - try: - self.repo.create_remote('origin', self.url) - except os.error: - # This exception occurs when two processes are trying to write - # to the git config at once, go ahead and pass over it since - # this is the only write. This should place a lock down. - pass - else: - new = True - - try: - ssl_verify = self.repo.config.get_bool('http.sslVerify') - except KeyError: - ssl_verify = None - if ssl_verify != self.ssl_verify: - self.repo.config.set_multivar('http.sslVerify', - '', - str(self.ssl_verify).lower()) - - # Ensure that refspecs for the "origin" remote are set up as configured - if hasattr(self, 'refspecs'): - self.configure_refspecs() + self.enforce_git_config() return new @@ -1658,7 +1621,9 @@ class Pygit2(GitProvider): log.debug('%s remote \'%s\' is up-to-date', self.role, self.id) refs_post = self.repo.listall_references() cleaned = self.clean_stale_refs(local_refs=refs_post) - return bool(received_objects or refs_pre != refs_post or cleaned) + return True \ + if (received_objects or refs_pre != refs_post or cleaned) \ + else None def file_list(self, tgt_env): ''' @@ -1757,14 +1722,6 @@ class Pygit2(GitProvider): return blob, blob.hex, mode return None, None, None - def get_refspecs(self): - ''' - Return the configured refspecs - ''' - if not [x for x in self.repo.config if x.startswith('remote.origin.')]: - raise GitRemoteError('\'origin\' remote not not present') - return list(self.repo.config.get_multivar('remote.origin.fetch')) - def get_tree_from_branch(self, ref): ''' Return a pygit2.Tree object matching a head ref fetched into @@ -1952,11 +1909,17 @@ class Pygit2(GitProvider): fp_.write(blob.data) +GIT_PROVIDERS = { + 'pygit2': Pygit2, + 'gitpython': GitPython, +} + + class GitBase(object): ''' Base class for gitfs/git_pillar ''' - def __init__(self, opts, valid_providers=VALID_PROVIDERS, cache_root=None): + def __init__(self, opts, git_providers=None, cache_root=None): ''' IMPORTANT: If specifying a cache_root, understand that this is also where the remotes will be cloned. A non-default cache_root is only @@ -1964,8 +1927,9 @@ class GitBase(object): out into the winrepo locations and not within the cachedir. ''' self.opts = opts - self.valid_providers = valid_providers - self.get_provider() + self.git_providers = git_providers if git_providers is not None \ + else GIT_PROVIDERS + self.verify_provider() if cache_root is not None: self.cache_root = self.remote_root = cache_root else: @@ -2023,7 +1987,7 @@ class GitBase(object): self.remotes = [] for remote in remotes: - repo_obj = self.provider_class( + repo_obj = self.git_providers[self.provider]( self.opts, remote, per_remote_defaults, @@ -2277,7 +2241,7 @@ class GitBase(object): # Hash file won't exist if no files have yet been served up pass - def get_provider(self): + def verify_provider(self): ''' Determine which provider to use ''' @@ -2298,12 +2262,12 @@ class GitBase(object): # Should only happen if someone does something silly like # set the provider to a numeric value. desired_provider = str(desired_provider).lower() - if desired_provider not in self.valid_providers: + if desired_provider not in self.git_providers: log.critical( 'Invalid {0}_provider \'{1}\'. Valid choices are: {2}' .format(self.role, desired_provider, - ', '.join(self.valid_providers)) + ', '.join(self.git_providers)) ) failhard(self.role) elif desired_provider == 'pygit2' and self.verify_pygit2(): @@ -2316,17 +2280,13 @@ class GitBase(object): .format(self.role) ) failhard(self.role) - if self.provider == 'pygit2': - self.provider_class = Pygit2 - elif self.provider == 'gitpython': - self.provider_class = GitPython def verify_gitpython(self, quiet=False): ''' Check if GitPython is available and at a compatible version (>= 0.3.0) ''' def _recommend(): - if HAS_PYGIT2 and 'pygit2' in self.valid_providers: + if HAS_PYGIT2 and 'pygit2' in self.git_providers: log.error(_RECOMMEND_PYGIT2.format(self.role)) if not HAS_GITPYTHON: @@ -2337,7 +2297,7 @@ class GitBase(object): ) _recommend() return False - elif 'gitpython' not in self.valid_providers: + elif 'gitpython' not in self.git_providers: return False # pylint: disable=no-member @@ -2377,7 +2337,7 @@ class GitBase(object): Pygit2 must be at least 0.20.3 and libgit2 must be at least 0.20.0. ''' def _recommend(): - if HAS_GITPYTHON and 'gitpython' in self.valid_providers: + if HAS_GITPYTHON and 'gitpython' in self.git_providers: log.error(_RECOMMEND_GITPYTHON.format(self.role)) if not HAS_PYGIT2: @@ -2388,7 +2348,7 @@ class GitBase(object): ) _recommend() return False - elif 'pygit2' not in self.valid_providers: + elif 'pygit2' not in self.git_providers: return False # pylint: disable=no-member @@ -2507,7 +2467,7 @@ class GitFS(GitBase): ''' def __init__(self, opts): self.role = 'gitfs' - GitBase.__init__(self, opts) + super(GitFS, self).__init__(opts) def dir_list(self, load): ''' @@ -2636,12 +2596,7 @@ class GitFS(GitBase): Return a chunk from a file based on the data received ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') ret = {'data': '', @@ -2676,12 +2631,7 @@ class GitFS(GitBase): Return a file hash, the hash type is set in the master config file ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if not all(x in load for x in ('path', 'saltenv')): @@ -2710,12 +2660,7 @@ class GitFS(GitBase): Return a dict containing the file lists for files and dirs ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if not os.path.isdir(self.file_list_cachedir): @@ -2784,12 +2729,7 @@ class GitFS(GitBase): Return a dict of all symlinks based on a given path in the repo ''' if 'env' in load: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". load.pop('env') if not salt.utils.stringutils.is_hex(load['saltenv']) \ @@ -2811,7 +2751,7 @@ class GitPillar(GitBase): ''' def __init__(self, opts): self.role = 'git_pillar' - GitBase.__init__(self, opts) + super(GitPillar, self).__init__(opts) def checkout(self): ''' @@ -2901,7 +2841,7 @@ class WinRepo(GitBase): ''' def __init__(self, opts, winrepo_dir): self.role = 'winrepo' - GitBase.__init__(self, opts, cache_root=winrepo_dir) + super(WinRepo, self).__init__(opts, cache_root=winrepo_dir) def checkout(self): ''' diff --git a/salt/utils/jid.py b/salt/utils/jid.py index 3f4ef296a2..b65293d8d5 100644 --- a/salt/utils/jid.py +++ b/salt/utils/jid.py @@ -9,12 +9,22 @@ import os from salt.ext import six +LAST_JID_DATETIME = None -def gen_jid(): + +def gen_jid(opts): ''' Generate a jid ''' - return '{0:%Y%m%d%H%M%S%f}'.format(datetime.datetime.now()) + global LAST_JID_DATETIME # pylint: disable=global-statement + + if not opts.get('unique_jid', False): + return '{0:%Y%m%d%H%M%S%f}'.format(datetime.datetime.now()) + jid_dt = datetime.datetime.now() + if LAST_JID_DATETIME and LAST_JID_DATETIME >= jid_dt: + jid_dt = LAST_JID_DATETIME + datetime.timedelta(microseconds=1) + LAST_JID_DATETIME = jid_dt + return '{0:%Y%m%d%H%M%S%f}_{1}'.format(jid_dt, os.getpid()) def is_jid(jid): @@ -23,10 +33,10 @@ def is_jid(jid): ''' if not isinstance(jid, six.string_types): return False - if len(jid) != 20: + if len(jid) != 20 and (len(jid) <= 21 or jid[20] != '_'): return False try: - int(jid) + int(jid[:20]) return True except ValueError: return False @@ -37,7 +47,7 @@ def jid_to_time(jid): Convert a salt job id into the time when the job was invoked ''' jid = str(jid) - if len(jid) != 20: + if len(jid) != 20 and (len(jid) <= 21 or jid[20] != '_'): return '' year = jid[:4] month = jid[4:6] @@ -45,7 +55,7 @@ def jid_to_time(jid): hour = jid[8:10] minute = jid[10:12] second = jid[12:14] - micro = jid[14:] + micro = jid[14:20] ret = '{0}, {1} {2} {3}:{4}:{5}.{6}'.format(year, months[int(month)], diff --git a/salt/utils/job.py b/salt/utils/job.py index c37e034c32..a10098019a 100644 --- a/salt/utils/job.py +++ b/salt/utils/job.py @@ -18,7 +18,7 @@ def store_job(opts, load, event=None, mminion=None): Store job information using the configured master_job_cache ''' # Generate EndTime - endtime = salt.utils.jid.jid_to_time(salt.utils.jid.gen_jid()) + endtime = salt.utils.jid.jid_to_time(salt.utils.jid.gen_jid(opts)) # If the return data is invalid, just ignore it if any(key not in load for key in ('return', 'jid', 'id')): return False diff --git a/salt/utils/listdiffer.py b/salt/utils/listdiffer.py new file mode 100644 index 0000000000..d0451766c9 --- /dev/null +++ b/salt/utils/listdiffer.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- +''' +Compare lists of dictionaries by a specified key. + +The following can be retrieved: + (1) List of added, removed, intersect elements + (2) List of diffs having the following format: + : {, 'new': }} + A recursive diff is done between the the values (dicts) with the same + key + (3) List with the new values for each key + (4) List with the old values for each key + (5) List of changed items in the format + ('.', {'old': , 'new': }) + (5) String representations of the list diff + +Note: All dictionaries keys are expected to be strings +''' +from __future__ import absolute_import +from salt.utils.dictdiffer import recursive_diff + + +def list_diff(list_a, list_b, key): + return ListDictDiffer(list_a, list_b, key) + + +class ListDictDiffer(object): + ''' + Calculates the differences between two lists of dictionaries. + + It matches the items based on a given key and uses the recursive_diff to + diff the two values. + ''' + def __init__(self, current_list, next_list, key): + self._intersect = [] + self._removed = [] + self._added = [] + self._new = next_list + self._current = current_list + self._key = key + for current_item in current_list: + if key not in current_item: + raise ValueError('The supplied key \'{0}\' does not ' + 'exist in item, the available keys are: {1}' + ''.format(key, current_item.keys())) + for next_item in next_list: + if key not in next_item: + raise ValueError('The supplied key \'{0}\' does not ' + 'exist in item, the available keys are: ' + '{1}'.format(key, next_item.keys())) + if next_item[key] == current_item[key]: + item = {key: next_item[key], + 'old': current_item, + 'new': next_item} + self._intersect.append(item) + break + else: + self._removed.append(current_item) + + for next_item in next_list: + for current_item in current_list: + if next_item[key] == current_item[key]: + break + else: + self._added.append(next_item) + + def _get_recursive_difference(self, type): + '''Returns the recursive diff between dict values''' + if type == 'intersect': + return [recursive_diff(item['old'], item['new']) for item in self._intersect] + elif type == 'added': + return [recursive_diff({}, item) for item in self._added] + elif type == 'removed': + return [recursive_diff(item, {}, ignore_missing_keys=False) + for item in self._removed] + elif type == 'all': + recursive_list = [] + recursive_list.extend([recursive_diff(item['old'], item['new']) for item in self._intersect]) + recursive_list.extend([recursive_diff({}, item) for item in self._added]) + recursive_list.extend([recursive_diff(item, {}, + ignore_missing_keys=False) + for item in self._removed]) + return recursive_list + else: + raise ValueError('The given type for recursive list matching ' + 'is not supported.') + + @property + def removed(self): + '''Returns the objects which are removed from the list''' + return self._removed + + @property + def added(self): + '''Returns the objects which are added to the list''' + return self._added + + @property + def intersect(self): + '''Returns the intersect objects''' + return self._intersect + + def remove_diff(self, diff_key=None, diff_list='intersect'): + '''Deletes an attribute from all of the intersect objects''' + if diff_list == 'intersect': + for item in self._intersect: + item['old'].pop(diff_key, None) + item['new'].pop(diff_key, None) + if diff_list == 'removed': + for item in self._removed: + item.pop(diff_key, None) + + @property + def diffs(self): + ''' + Returns a list of dictionaries with key value pairs. + The values are the differences between the items identified by the key. + ''' + differences = [] + for item in self._get_recursive_difference(type='all'): + if item.diffs: + if item.past_dict: + differences.append({item.past_dict[self._key]: item.diffs}) + elif item.current_dict: + differences.append({item.current_dict[self._key]: item.diffs}) + return differences + + @property + def changes_str(self): + '''Returns a string describing the changes''' + changes = '' + for item in self._get_recursive_difference(type='intersect'): + if item.diffs: + changes = ''.join([changes, + # Tabulate comment deeper, show the key attribute and the value + # Next line should be tabulated even deeper, + # every change should be tabulated 1 deeper + '\tidentified by {0} {1}:\n\t{2}\n'.format( + self._key, + item.past_dict[self._key], + item.changes_str.replace('\n', '\n\t'))]) + for item in self._get_recursive_difference(type='removed'): + if item.past_dict: + changes = ''.join([changes, + # Tabulate comment deeper, show the key attribute and the value + '\tidentified by {0} {1}:' + '\n\twill be removed\n'.format(self._key, + item.past_dict[self._key])]) + for item in self._get_recursive_difference(type='added'): + if item.current_dict: + changes = ''.join([changes, + # Tabulate comment deeper, show the key attribute and the value + '\tidentified by {0} {1}:' + '\n\twill be added\n'.format(self._key, + item.current_dict[self._key])]) + return changes + + @property + def changes_str2(self, tab_string=' '): + ''' + Returns a string in a more compact format describing the changes. + + The output better alligns with the one in recursive_diff. + ''' + changes = [] + for item in self._get_recursive_difference(type='intersect'): + if item.diffs: + changes.append('{tab}{0}={1} (updated):\n{tab}{tab}{2}' + ''.format(self._key, item.past_dict[self._key], + item.changes_str.replace( + '\n', + '\n{0}{0}'.format(tab_string)), + tab=tab_string)) + for item in self._get_recursive_difference(type='removed'): + if item.past_dict: + changes.append('{tab}{0}={1} (removed)'.format( + self._key, item.past_dict[self._key], tab=tab_string)) + for item in self._get_recursive_difference(type='added'): + if item.current_dict: + changes.append('{tab}{0}={1} (added): {2}'.format( + self._key, item.current_dict[self._key], + dict(item.current_dict), tab=tab_string)) + return '\n'.join(changes) + + @property + def new_values(self): + '''Returns the new values from the diff''' + def get_new_values_and_key(item): + values = item.new_values + if item.past_dict: + values.update({self._key: item.past_dict[self._key]}) + else: + # This is a new item as it has no past_dict + values.update({self._key: item.current_dict[self._key]}) + return values + + return [get_new_values_and_key(el) + for el in self._get_recursive_difference('all') + if el.diffs and el.current_dict] + + @property + def old_values(self): + '''Returns the old values from the diff''' + def get_old_values_and_key(item): + values = item.old_values + values.update({self._key: item.past_dict[self._key]}) + return values + + return [get_old_values_and_key(el) + for el in self._get_recursive_difference('all') + if el.diffs and el.past_dict] + + def changed(self, selection='all'): + ''' + Returns the list of changed values. + The key is added to each item. + + selection + Specifies the desired changes. + Supported values are + ``all`` - all changed items are included in the output + ``intersect`` - changed items present in both lists are included + ''' + changed = [] + if selection == 'all': + for recursive_item in self._get_recursive_difference(type='all'): + # We want the unset values as well + recursive_item.ignore_unset_values = False + key_val = str(recursive_item.past_dict[self._key]) \ + if self._key in recursive_item.past_dict \ + else str(recursive_item.current_dict[self._key]) + + for change in recursive_item.changed(): + if change != self._key: + changed.append('.'.join([self._key, key_val, change])) + return changed + elif selection == 'intersect': + # We want the unset values as well + for recursive_item in self._get_recursive_difference(type='intersect'): + recursive_item.ignore_unset_values = False + key_val = str(recursive_item.past_dict[self._key]) \ + if self._key in recursive_item.past_dict \ + else str(recursive_item.current_dict[self._key]) + + for change in recursive_item.changed(): + if change != self._key: + changed.append('.'.join([self._key, key_val, change])) + return changed + + @property + def current_list(self): + return self._current + + @property + def new_list(self): + return self._new diff --git a/salt/utils/master.py b/salt/utils/master.py index 0750fd4ee2..b57a81f93a 100644 --- a/salt/utils/master.py +++ b/salt/utils/master.py @@ -248,7 +248,8 @@ class MasterPillarUtil(object): # Return a list of minion ids that match the target and tgt_type minion_ids = [] ckminions = salt.utils.minions.CkMinions(self.opts) - minion_ids = ckminions.check_minions(self.tgt, self.tgt_type) + _res = ckminions.check_minions(self.tgt, self.tgt_type) + minion_ids = _res['minions'] if len(minion_ids) == 0: log.debug('No minions matched for tgt="{0}" and tgt_type="{1}"'.format(self.tgt, self.tgt_type)) return {} diff --git a/salt/utils/minions.py b/salt/utils/minions.py index 43f875dc24..0a49f2c24d 100644 --- a/salt/utils/minions.py +++ b/salt/utils/minions.py @@ -201,7 +201,8 @@ class CkMinions(object): ''' Return the minions found by looking via globs ''' - return fnmatch.filter(self._pki_minions(), expr) + return {'minions': fnmatch.filter(self._pki_minions(), expr), + 'missing': []} def _check_list_minions(self, expr, greedy): # pylint: disable=unused-argument ''' @@ -210,14 +211,16 @@ class CkMinions(object): if isinstance(expr, six.string_types): expr = [m for m in expr.split(',') if m] minions = self._pki_minions() - return [x for x in expr if x in minions] + return {'minions': [x for x in expr if x in minions], + 'missing': [x for x in expr if x not in minions]} def _check_pcre_minions(self, expr, greedy): # pylint: disable=unused-argument ''' Return the minions found by looking via regular expressions ''' reg = re.compile(expr) - return [m for m in self._pki_minions() if reg.match(m)] + return {'minions': [m for m in self._pki_minions() if reg.match(m)], + 'missing': []} def _pki_minions(self): ''' @@ -265,7 +268,8 @@ class CkMinions(object): elif cache_enabled: minions = list_cached_minions() else: - return [] + return {'minions': [], + 'missing': []} if cache_enabled: if greedy: @@ -273,7 +277,8 @@ class CkMinions(object): else: cminions = minions if not cminions: - return minions + return {'minions': minions, + 'missing': []} minions = set(minions) for id_ in cminions: if greedy and id_ not in minions: @@ -291,7 +296,8 @@ class CkMinions(object): exact_match=exact_match): minions.remove(id_) minions = list(minions) - return minions + return {'minions': minions, + 'missing': []} def _check_grain_minions(self, expr, delimiter, greedy): ''' @@ -346,7 +352,8 @@ class CkMinions(object): elif cache_enabled: minions = self.cache.list('minions') else: - return [] + return {'minions': [], + 'missing': []} if cache_enabled: if greedy: @@ -354,7 +361,8 @@ class CkMinions(object): else: cminions = minions if cminions is None: - return minions + return {'minions': minions, + 'missing': []} tgt = expr try: @@ -366,7 +374,8 @@ class CkMinions(object): tgt = ipaddress.ip_network(tgt) except: # pylint: disable=bare-except log.error('Invalid IP/CIDR target: {0}'.format(tgt)) - return [] + return {'minions': [], + 'missing': []} proto = 'ipv{0}'.format(tgt.version) minions = set(minions) @@ -387,7 +396,8 @@ class CkMinions(object): if not match and id_ in minions: minions.remove(id_) - return list(minions) + return {'minions': list(minions), + 'missing': []} def _check_range_minions(self, expr, greedy): ''' @@ -412,11 +422,14 @@ class CkMinions(object): for fn_ in salt.utils.isorted(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): if not fn_.startswith('.') and os.path.isfile(os.path.join(self.opts['pki_dir'], self.acc, fn_)): mlist.append(fn_) - return mlist + return {'minions': mlist, + 'missing': []} elif cache_enabled: - return self.cache.list('minions') + return {'minions': self.cache.list('minions'), + 'missing': []} else: - return list() + return {'minions': [], + 'missing': []} def _check_compound_pillar_exact_minions(self, expr, delimiter, greedy): ''' @@ -437,10 +450,9 @@ class CkMinions(object): ''' Return the minions found by looking via compound matcher ''' - log.debug('_check_compound_minions({0}, {1}, {2}, {3})'.format(expr, delimiter, greedy, pillar_exact)) if not isinstance(expr, six.string_types) and not isinstance(expr, (list, tuple)): log.error('Compound target that is neither string, list nor tuple') - return [] + return {'minions': [], 'missing': []} minions = set(self._pki_minions()) log.debug('minions: {0}'.format(minions)) @@ -461,6 +473,7 @@ class CkMinions(object): results = [] unmatched = [] opers = ['and', 'or', 'not', '(', ')'] + missing = [] if isinstance(expr, six.string_types): words = expr.split() @@ -475,7 +488,7 @@ class CkMinions(object): if results: if results[-1] == '(' and word in ('and', 'or'): log.error('Invalid beginning operator after "(": {0}'.format(word)) - return [] + return {'minions': [], 'missing': []} if word == 'not': if not results[-1] in ('&', '|', '('): results.append('&') @@ -495,7 +508,7 @@ class CkMinions(object): log.error('Invalid compound expr (unexpected ' 'right parenthesis): {0}' .format(expr)) - return [] + return {'minions': [], 'missing': []} results.append(word) unmatched.pop() if unmatched and unmatched[-1] == '-': @@ -504,7 +517,7 @@ class CkMinions(object): else: # Won't get here, unless oper is added log.error('Unhandled oper in compound expr: {0}' .format(expr)) - return [] + return {'minions': [], 'missing': []} else: # seq start with oper, fail if word == 'not': @@ -520,13 +533,13 @@ class CkMinions(object): 'Expression may begin with' ' binary operator: {0}'.format(word) ) - return [] + return {'minions': [], 'missing': []} elif target_info and target_info['engine']: if 'N' == target_info['engine']: # Nodegroups should already be expanded/resolved to other engines log.error('Detected nodegroup expansion failure of "{0}"'.format(word)) - return [] + return {'minions': [], 'missing': []} engine = ref.get(target_info['engine']) if not engine: # If an unknown engine is called at any time, fail out @@ -537,21 +550,24 @@ class CkMinions(object): word, ) ) - return [] + return {'minions': [], 'missing': []} engine_args = [target_info['pattern']] if target_info['engine'] in ('G', 'P', 'I', 'J'): engine_args.append(target_info['delimiter'] or ':') engine_args.append(greedy) - results.append(str(set(engine(*engine_args)))) + _results = engine(*engine_args) + results.append(str(set(_results['minions']))) + missing.extend(_results['missing']) if unmatched and unmatched[-1] == '-': results.append(')') unmatched.pop() else: # The match is not explicitly defined, evaluate as a glob - results.append(str(set(self._check_glob_minions(word, True)))) + _results = self._check_glob_minions(word, True) + results.append(str(set(_results['minions']))) if unmatched and unmatched[-1] == '-': results.append(')') unmatched.pop() @@ -563,12 +579,14 @@ class CkMinions(object): log.debug('Evaluating final compound matching expr: {0}' .format(results)) try: - return list(eval(results)) # pylint: disable=W0123 + minions = list(eval(results)) # pylint: disable=W0123 + return {'minions': minions, 'missing': missing} except Exception: log.error('Invalid compound target: {0}'.format(expr)) - return [] + return {'minions': [], 'missing': []} - return list(minions) + return {'minions': list(minions), + 'missing': []} def connected_ids(self, subset=None, show_ipv4=False, include_localhost=False): ''' @@ -620,7 +638,7 @@ class CkMinions(object): for fn_ in salt.utils.isorted(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): if not fn_.startswith('.') and os.path.isfile(os.path.join(self.opts['pki_dir'], self.acc, fn_)): mlist.append(fn_) - return mlist + return {'minions': mlist, 'missing': []} def check_minions(self, expr, @@ -633,6 +651,7 @@ class CkMinions(object): match the regex, this will then be used to parse the returns to make sure everyone has checked back in. ''' + try: if expr is None: expr = '' @@ -644,15 +663,15 @@ class CkMinions(object): 'pillar_exact', 'compound', 'compound_pillar_exact'): - minions = check_func(expr, delimiter, greedy) + _res = check_func(expr, delimiter, greedy) else: - minions = check_func(expr, greedy) + _res = check_func(expr, greedy) except Exception: log.exception( 'Failed matching available minions with {0} pattern: {1}' .format(tgt_type, expr)) - minions = [] - return minions + _res = {'minions': [], 'missing': []} + return _res def _expand_matching(self, auth_entry): ref = {'G': 'grain', @@ -672,7 +691,8 @@ class CkMinions(object): v_matcher = ref.get(target_info['engine']) v_expr = target_info['pattern'] - return set(self.check_minions(v_expr, v_matcher)) + _res = self.check_minions(v_expr, v_matcher) + return set(_res['minions']) def validate_tgt(self, valid, expr, tgt_type, minions=None, expr_form=None): ''' @@ -692,7 +712,8 @@ class CkMinions(object): v_minions = self._expand_matching(valid) if minions is None: - minions = set(self.check_minions(expr, tgt_type)) + _res = self.check_minions(expr, tgt_type) + minions = set(_res['minions']) else: minions = set(minions) d_bool = not bool(minions.difference(v_minions)) @@ -789,8 +810,12 @@ class CkMinions(object): v_tgt_type = 'pillar_exact' elif tgt_type.lower() == 'compound': v_tgt_type = 'compound_pillar_exact' - v_minions = set(self.check_minions(tgt, v_tgt_type)) - minions = set(self.check_minions(tgt, tgt_type)) + _res = self.check_minions(tgt, v_tgt_type) + v_minions = set(_res['minions']) + + _res = self.check_minions(tgt, tgt_type) + minions = set(_res['minions']) + mismatch = bool(minions.difference(v_minions)) # If the non-exact match gets more minions than the exact match # then pillar globbing or PCRE is being used, and we have a @@ -877,8 +902,12 @@ class CkMinions(object): v_tgt_type = 'pillar_exact' elif tgt_type.lower() == 'compound': v_tgt_type = 'compound_pillar_exact' - v_minions = set(self.check_minions(tgt, v_tgt_type)) - minions = set(self.check_minions(tgt, tgt_type)) + _res = self.check_minions(tgt, v_tgt_type) + v_minions = set(_res['minions']) + + _res = self.check_minions(tgt, tgt_type) + minions = set(_res['minions']) + mismatch = bool(minions.difference(v_minions)) # If the non-exact match gets more minions than the exact match # then pillar globbing or PCRE is being used, and we have a @@ -956,7 +985,11 @@ class CkMinions(object): if form != 'cloud': comps = fun.split('.') if len(comps) != 2: - return False + # Hint at a syntax error when command is passed improperly, + # rather than returning an authentication error of some kind. + # See Issue #21969 for more information. + return {'error': {'name': 'SaltInvocationError', + 'message': 'A command invocation error occurred: Check syntax.'}} mod_name = comps[0] fun_name = comps[1] else: @@ -1049,9 +1082,10 @@ def mine_get(tgt, fun, tgt_type='glob', opts=None): ret = {} serial = salt.payload.Serial(opts) checker = CkMinions(opts) - minions = checker.check_minions( + _res = checker.check_minions( tgt, tgt_type) + minions = _res['minions'] cache = salt.cache.factory(opts) for minion in minions: mdata = cache.fetch('minions/{0}'.format(minion), 'mine') diff --git a/salt/utils/mount.py b/salt/utils/mount.py new file mode 100644 index 0000000000..09dbc8bfc1 --- /dev/null +++ b/salt/utils/mount.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +''' +Common functions for managing mounts +''' + +# Import python libs +from __future__ import absolute_import +import logging +import os +import yaml + +# Import Salt libs +import salt.utils.files +import salt.utils.stringutils +import salt.utils.versions + +from salt.utils.yamldumper import SafeOrderedDumper + +log = logging.getLogger(__name__) + + +def _read_file(path): + ''' + Reads and returns the contents of a text file + ''' + try: + with salt.utils.files.fopen(path, 'rb') as contents: + return yaml.safe_load(contents.read()) + except (OSError, IOError): + return {} + + +def get_cache(opts): + ''' + Return the mount cache file location. + ''' + return os.path.join(opts['cachedir'], 'mounts') + + +def read_cache(opts): + ''' + Write the mount cache file. + ''' + cache_file = get_cache(opts) + return _read_file(cache_file) + + +def write_cache(cache, opts): + ''' + Write the mount cache file. + ''' + cache_file = get_cache(opts) + + try: + _cache = salt.utils.stringutils.to_bytes( + yaml.dump( + cache, + Dumper=SafeOrderedDumper + ) + ) + with salt.utils.files.fopen(cache_file, 'wb+') as fp_: + fp_.write(_cache) + return True + except (IOError, OSError): + log.error('Failed to cache mounts', + exc_info_on_loglevel=logging.DEBUG) + return False diff --git a/salt/utils/network.py b/salt/utils/network.py index 77289d21b6..119aeed25a 100644 --- a/salt/utils/network.py +++ b/salt/utils/network.py @@ -1039,7 +1039,8 @@ def interface_ip(iface): iface_info, error = _get_iface_info(iface) if error is False: - return iface_info.get(iface, {}).get('inet', {})[0].get('address', '') + inet = iface_info.get(iface, {}).get('inet', None) + return inet[0].get('address', '') if inet else '' else: return error diff --git a/salt/utils/parsers.py b/salt/utils/parsers.py index 26e031c69d..cf926a015e 100644 --- a/salt/utils/parsers.py +++ b/salt/utils/parsers.py @@ -968,7 +968,14 @@ class DaemonMixIn(six.with_metaclass(MixInMeta, object)): # We've loaded and merged options into the configuration, it's safe # to query about the pidfile if self.check_pidfile(): - os.unlink(self.config['pidfile']) + try: + os.unlink(self.config['pidfile']) + except OSError as err: + self.info( + 'PIDfile could not be deleted: {0}'.format( + self.config['pidfile'] + ) + ) def set_pidfile(self): from salt.utils.process import set_pidfile @@ -1547,7 +1554,7 @@ class CloudQueriesMixIn(six.with_metaclass(MixInMeta, object)): action='store', help='Display a list of configured profiles. Pass in a cloud ' 'provider to view the provider\'s associated profiles, ' - 'such as digital_ocean, or pass in "all" to list all the ' + 'such as digitalocean, or pass in "all" to list all the ' 'configured profiles.' ) self.add_option_group(group) @@ -2402,6 +2409,16 @@ class SaltKeyOptionParser(six.with_metaclass(OptionParserMeta, 'Default: %default.') ) + self.add_option( + '--preserve-minions', + default=False, + help=('Setting this to True prevents the master from deleting ' + 'the minion cache when keys are deleted, this may have ' + 'security implications if compromised minions auth with ' + 'a previous deleted minion ID. ' + 'Default: %default.') + ) + key_options_group = optparse.OptionGroup( self, 'Key Generation Options' ) @@ -2501,6 +2518,13 @@ class SaltKeyOptionParser(six.with_metaclass(OptionParserMeta, elif self.options.rotate_aes_key.lower() == 'false': self.options.rotate_aes_key = False + def process_preserve_minions(self): + if hasattr(self.options, 'preserve_minions') and isinstance(self.options.preserve_minions, str): + if self.options.preserve_minions.lower() == 'true': + self.options.preserve_minions = True + elif self.options.preserve_minions.lower() == 'false': + self.options.preserve_minions = False + def process_list(self): # Filter accepted list arguments as soon as possible if not self.options.list: diff --git a/salt/utils/pydsl.py b/salt/utils/pydsl.py index 1587e61d93..fb62598ba1 100644 --- a/salt/utils/pydsl.py +++ b/salt/utils/pydsl.py @@ -88,7 +88,6 @@ from __future__ import absolute_import from uuid import uuid4 as _uuid # Import salt libs -import salt.utils.versions from salt.utils.odict import OrderedDict from salt.state import HighState @@ -140,12 +139,7 @@ class Sls(object): def include(self, *sls_names, **kws): if 'env' in kws: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kws.pop('env') saltenv = kws.get('saltenv', self.saltenv) diff --git a/salt/utils/reactor.py b/salt/utils/reactor.py index 58c7e98c23..de1581c510 100644 --- a/salt/utils/reactor.py +++ b/salt/utils/reactor.py @@ -7,6 +7,7 @@ import glob import logging # Import salt libs +import salt.client import salt.runner import salt.state import salt.utils @@ -14,6 +15,7 @@ import salt.utils.cache import salt.utils.event import salt.utils.files import salt.utils.process +import salt.wheel import salt.defaults.exitcodes # Import 3rd-party libs @@ -22,6 +24,15 @@ from salt.ext import six log = logging.getLogger(__name__) +REACTOR_INTERNAL_KEYWORDS = frozenset([ + '__id__', + '__sls__', + 'name', + 'order', + 'fun', + 'state', +]) + class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.state.Compiler): ''' @@ -30,6 +41,10 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat The reactor has the capability to execute pre-programmed executions as reactions to events ''' + aliases = { + 'cmd': 'local', + } + def __init__(self, opts, log_queue=None): super(Reactor, self).__init__(log_queue=log_queue) local_minion_opts = opts.copy() @@ -172,6 +187,16 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat return {'status': False, 'comment': 'Reactor does not exists.'} + def resolve_aliases(self, chunks): + ''' + Preserve backward compatibility by rewriting the 'state' key in the low + chunks if it is using a legacy type. + ''' + for idx, _ in enumerate(chunks): + new_state = self.aliases.get(chunks[idx]['state']) + if new_state is not None: + chunks[idx]['state'] = new_state + def reactions(self, tag, data, reactors): ''' Render a list of reactor files and returns a reaction struct @@ -192,6 +217,7 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat except Exception as exc: log.error('Exception trying to compile reactions: {0}'.format(exc), exc_info=True) + self.resolve_aliases(chunks) return chunks def call_reactions(self, chunks): @@ -249,12 +275,19 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat class ReactWrap(object): ''' - Create a wrapper that executes low data for the reaction system + Wrapper that executes low data for the Reactor System ''' # class-wide cache of clients client_cache = None event_user = 'Reactor' + reaction_class = { + 'local': salt.client.LocalClient, + 'runner': salt.runner.RunnerClient, + 'wheel': salt.wheel.Wheel, + 'caller': salt.client.Caller, + } + def __init__(self, opts): self.opts = opts if ReactWrap.client_cache is None: @@ -265,21 +298,49 @@ class ReactWrap(object): queue_size=self.opts['reactor_worker_hwm'] # queue size for those workers ) + def populate_client_cache(self, low): + ''' + Populate the client cache with an instance of the specified type + ''' + reaction_type = low['state'] + if reaction_type not in self.client_cache: + log.debug('Reactor is populating %s client cache', reaction_type) + if reaction_type in ('runner', 'wheel'): + # Reaction types that run locally on the master want the full + # opts passed. + self.client_cache[reaction_type] = \ + self.reaction_class[reaction_type](self.opts) + # The len() function will cause the module functions to load if + # they aren't already loaded. We want to load them so that the + # spawned threads don't need to load them. Loading in the + # spawned threads creates race conditions such as sometimes not + # finding the required function because another thread is in + # the middle of loading the functions. + len(self.client_cache[reaction_type].functions) + else: + # Reactions which use remote pubs only need the conf file when + # instantiating a client instance. + self.client_cache[reaction_type] = \ + self.reaction_class[reaction_type](self.opts['conf_file']) + def run(self, low): ''' - Execute the specified function in the specified state by passing the - low data + Execute a reaction by invoking the proper wrapper func ''' - l_fun = getattr(self, low['state']) + self.populate_client_cache(low) try: - f_call = salt.utils.format_call(l_fun, low) - kwargs = f_call.get('kwargs', {}) - if 'arg' not in kwargs: - kwargs['arg'] = [] - if 'kwarg' not in kwargs: - kwargs['kwarg'] = {} + l_fun = getattr(self, low['state']) + except AttributeError: + log.error( + 'ReactWrap is missing a wrapper function for \'%s\'', + low['state'] + ) - # TODO: Setting the user doesn't seem to work for actual remote publishes + try: + wrap_call = salt.utils.format_call(l_fun, low) + args = wrap_call.get('args', ()) + kwargs = wrap_call.get('kwargs', {}) + # TODO: Setting user doesn't seem to work for actual remote pubs if low['state'] in ('runner', 'wheel'): # Update called function's low data with event user to # segregate events fired by reactor and avoid reaction loops @@ -287,81 +348,106 @@ class ReactWrap(object): # Replace ``state`` kwarg which comes from high data compiler. # It breaks some runner functions and seems unnecessary. kwargs['__state__'] = kwargs.pop('state') + # NOTE: if any additional keys are added here, they will also + # need to be added to filter_kwargs() - l_fun(*f_call.get('args', ()), **kwargs) + if 'args' in kwargs: + # New configuration + reactor_args = kwargs.pop('args') + for item in ('arg', 'kwarg'): + if item in low: + log.warning( + 'Reactor \'%s\' is ignoring \'%s\' param %s due to ' + 'presence of \'args\' param. Check the Reactor System ' + 'documentation for the correct argument format.', + low['__id__'], item, low[item] + ) + if low['state'] == 'caller' \ + and isinstance(reactor_args, list) \ + and not salt.utils.is_dictlist(reactor_args): + # Legacy 'caller' reactors were already using the 'args' + # param, but only supported a list of positional arguments. + # If low['args'] is a list but is *not* a dictlist, then + # this is actually using the legacy configuration. So, put + # the reactor args into kwarg['arg'] so that the wrapper + # interprets them as positional args. + kwargs['arg'] = reactor_args + kwargs['kwarg'] = {} + else: + kwargs['arg'] = () + kwargs['kwarg'] = reactor_args + if not isinstance(kwargs['kwarg'], dict): + kwargs['kwarg'] = salt.utils.repack_dictlist(kwargs['kwarg']) + if not kwargs['kwarg']: + log.error( + 'Reactor \'%s\' failed to execute %s \'%s\': ' + 'Incorrect argument format, check the Reactor System ' + 'documentation for the correct format.', + low['__id__'], low['state'], low['fun'] + ) + return + else: + # Legacy configuration + react_call = {} + if low['state'] in ('runner', 'wheel'): + if 'arg' not in kwargs or 'kwarg' not in kwargs: + # Runner/wheel execute on the master, so we can use + # format_call to get the functions args/kwargs + react_fun = self.client_cache[low['state']].functions.get(low['fun']) + if react_fun is None: + log.error( + 'Reactor \'%s\' failed to execute %s \'%s\': ' + 'function not available', + low['__id__'], low['state'], low['fun'] + ) + return + + react_call = salt.utils.format_call( + react_fun, + low, + expected_extra_kws=REACTOR_INTERNAL_KEYWORDS + ) + + if 'arg' not in kwargs: + kwargs['arg'] = react_call.get('args', ()) + if 'kwarg' not in kwargs: + kwargs['kwarg'] = react_call.get('kwargs', {}) + + # Execute the wrapper with the proper args/kwargs. kwargs['arg'] + # and kwargs['kwarg'] contain the positional and keyword arguments + # that will be passed to the client interface to execute the + # desired runner/wheel/remote-exec/etc. function. + l_fun(*args, **kwargs) + except SystemExit: + log.warning( + 'Reactor \'%s\' attempted to exit. Ignored.', low['__id__'] + ) except Exception: log.error( - 'Failed to execute {0}: {1}\n'.format(low['state'], l_fun), - exc_info=True - ) - - def local(self, *args, **kwargs): - ''' - Wrap LocalClient for running :ref:`execution modules ` - ''' - if 'local' not in self.client_cache: - self.client_cache['local'] = salt.client.LocalClient(self.opts['conf_file']) - try: - self.client_cache['local'].cmd_async(*args, **kwargs) - except SystemExit: - log.warning('Attempt to exit reactor. Ignored.') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) - - cmd = local + 'Reactor \'%s\' failed to execute %s \'%s\'', + low['__id__'], low['state'], low['fun'], exc_info=True + ) def runner(self, fun, **kwargs): ''' Wrap RunnerClient for executing :ref:`runner modules ` ''' - if 'runner' not in self.client_cache: - self.client_cache['runner'] = salt.runner.RunnerClient(self.opts) - # The len() function will cause the module functions to load if - # they aren't already loaded. We want to load them so that the - # spawned threads don't need to load them. Loading in the spawned - # threads creates race conditions such as sometimes not finding - # the required function because another thread is in the middle - # of loading the functions. - len(self.client_cache['runner'].functions) - try: - self.pool.fire_async(self.client_cache['runner'].low, args=(fun, kwargs)) - except SystemExit: - log.warning('Attempt to exit in reactor by runner. Ignored') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) + self.pool.fire_async(self.client_cache['runner'].low, args=(fun, kwargs)) def wheel(self, fun, **kwargs): ''' Wrap Wheel to enable executing :ref:`wheel modules ` ''' - if 'wheel' not in self.client_cache: - self.client_cache['wheel'] = salt.wheel.Wheel(self.opts) - # The len() function will cause the module functions to load if - # they aren't already loaded. We want to load them so that the - # spawned threads don't need to load them. Loading in the spawned - # threads creates race conditions such as sometimes not finding - # the required function because another thread is in the middle - # of loading the functions. - len(self.client_cache['wheel'].functions) - try: - self.pool.fire_async(self.client_cache['wheel'].low, args=(fun, kwargs)) - except SystemExit: - log.warning('Attempt to in reactor by whell. Ignored.') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) + self.pool.fire_async(self.client_cache['wheel'].low, args=(fun, kwargs)) - def caller(self, fun, *args, **kwargs): + def local(self, fun, tgt, **kwargs): ''' - Wrap Caller to enable executing :ref:`caller modules ` + Wrap LocalClient for running :ref:`execution modules ` ''' - log.debug("in caller with fun {0} args {1} kwargs {2}".format(fun, args, kwargs)) - args = kwargs.get('args', []) - kwargs = kwargs.get('kwargs', {}) - if 'caller' not in self.client_cache: - self.client_cache['caller'] = salt.client.Caller(self.opts['conf_file']) - try: - self.client_cache['caller'].cmd(fun, *args, **kwargs) - except SystemExit: - log.warning('Attempt to exit reactor. Ignored.') - except Exception as exc: - log.warning('Exception caught by reactor: {0}'.format(exc)) + self.client_cache['local'].cmd_async(tgt, fun, **kwargs) + + def caller(self, fun, **kwargs): + ''' + Wrap LocalCaller to execute remote exec functions locally on the Minion + ''' + self.client_cache['caller'].cmd(fun, *kwargs['arg'], **kwargs['kwarg']) diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index 61e087e607..d29a3bf314 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -385,7 +385,7 @@ class Schedule(object): ''' instance = None - def __new__(cls, opts, functions, returners=None, intervals=None, cleanup=None, proxy=None): + def __new__(cls, opts, functions, returners=None, intervals=None, cleanup=None, proxy=None, standalone=False): ''' Only create one instance of Schedule ''' @@ -395,33 +395,36 @@ class Schedule(object): # it in a WeakValueDictionary-- which will remove the item if no one # references it-- this forces a reference while we return to the caller cls.instance = object.__new__(cls) - cls.instance.__singleton_init__(opts, functions, returners, intervals, cleanup, proxy) + cls.instance.__singleton_init__(opts, functions, returners, intervals, cleanup, proxy, standalone) else: log.debug('Re-using Schedule') return cls.instance # has to remain empty for singletons, since __init__ will *always* be called - def __init__(self, opts, functions, returners=None, intervals=None, cleanup=None, proxy=None): + def __init__(self, opts, functions, returners=None, intervals=None, cleanup=None, proxy=None, standalone=False): pass # an init for the singleton instance to call - def __singleton_init__(self, opts, functions, returners=None, intervals=None, cleanup=None, proxy=None): + def __singleton_init__(self, opts, functions, returners=None, intervals=None, cleanup=None, proxy=None, standalone=False): self.opts = opts self.proxy = proxy self.functions = functions + self.standalone = standalone if isinstance(intervals, dict): self.intervals = intervals else: self.intervals = {} - if hasattr(returners, '__getitem__'): - self.returners = returners - else: - self.returners = returners.loader.gen_functions() + if not self.standalone: + if hasattr(returners, '__getitem__'): + self.returners = returners + else: + self.returners = returners.loader.gen_functions() self.time_offset = self.functions.get('timezone.get_offset', lambda: '0000')() self.schedule_returner = self.option('schedule_returner') # Keep track of the lowest loop interval needed in this variable self.loop_interval = six.MAXSIZE - clean_proc_dir(opts) + if not self.standalone: + clean_proc_dir(opts) if cleanup: for prefix in cleanup: self.delete_job_prefix(prefix) @@ -764,7 +767,7 @@ class Schedule(object): 'fun': func, 'fun_args': [], 'schedule': data['name'], - 'jid': salt.utils.jid.gen_jid()} + 'jid': salt.utils.jid.gen_jid(self.opts)} if 'metadata' in data: if isinstance(data['metadata'], dict): @@ -778,36 +781,37 @@ class Schedule(object): salt.utils.appendproctitle('{0} {1}'.format(self.__class__.__name__, ret['jid'])) - proc_fn = os.path.join( - salt.minion.get_proc_dir(self.opts['cachedir']), - ret['jid'] - ) + if not self.standalone: + proc_fn = os.path.join( + salt.minion.get_proc_dir(self.opts['cachedir']), + ret['jid'] + ) - # Check to see if there are other jobs with this - # signature running. If there are more than maxrunning - # jobs present then don't start another. - # If jid_include is False for this job we can ignore all this - # NOTE--jid_include defaults to True, thus if it is missing from the data - # dict we treat it like it was there and is True - if 'jid_include' not in data or data['jid_include']: - jobcount = 0 - for job in salt.utils.minion.running(self.opts): - if 'schedule' in job: - log.debug('schedule.handle_func: Checking job against ' - 'fun {0}: {1}'.format(ret['fun'], job)) - if ret['schedule'] == job['schedule'] \ - and salt.utils.process.os_is_running(job['pid']): - jobcount += 1 - log.debug( - 'schedule.handle_func: Incrementing jobcount, now ' - '{0}, maxrunning is {1}'.format( - jobcount, data['maxrunning'])) - if jobcount >= data['maxrunning']: + # Check to see if there are other jobs with this + # signature running. If there are more than maxrunning + # jobs present then don't start another. + # If jid_include is False for this job we can ignore all this + # NOTE--jid_include defaults to True, thus if it is missing from the data + # dict we treat it like it was there and is True + if 'jid_include' not in data or data['jid_include']: + jobcount = 0 + for job in salt.utils.minion.running(self.opts): + if 'schedule' in job: + log.debug('schedule.handle_func: Checking job against ' + 'fun {0}: {1}'.format(ret['fun'], job)) + if ret['schedule'] == job['schedule'] \ + and salt.utils.process.os_is_running(job['pid']): + jobcount += 1 log.debug( - 'schedule.handle_func: The scheduled job {0} ' - 'was not started, {1} already running'.format( - ret['schedule'], data['maxrunning'])) - return False + 'schedule.handle_func: Incrementing jobcount, now ' + '{0}, maxrunning is {1}'.format( + jobcount, data['maxrunning'])) + if jobcount >= data['maxrunning']: + log.debug( + 'schedule.handle_func: The scheduled job {0} ' + 'was not started, {1} already running'.format( + ret['schedule'], data['maxrunning'])) + return False if multiprocessing_enabled and not salt.utils.platform.is_windows(): # Reconfigure multiprocessing logging after daemonizing @@ -820,12 +824,13 @@ class Schedule(object): try: ret['pid'] = os.getpid() - if 'jid_include' not in data or data['jid_include']: - log.debug('schedule.handle_func: adding this job to the jobcache ' - 'with data {0}'.format(ret)) - # write this to /var/cache/salt/minion/proc - with salt.utils.files.fopen(proc_fn, 'w+b') as fp_: - fp_.write(salt.payload.Serial(self.opts).dumps(ret)) + if not self.standalone: + if 'jid_include' not in data or data['jid_include']: + log.debug('schedule.handle_func: adding this job to the jobcache ' + 'with data {0}'.format(ret)) + # write this to /var/cache/salt/minion/proc + with salt.utils.files.fopen(proc_fn, 'w+b') as fp_: + fp_.write(salt.payload.Serial(self.opts).dumps(ret)) args = tuple() if 'args' in data: @@ -848,39 +853,41 @@ class Schedule(object): if argspec.keywords: # this function accepts **kwargs, pack in the publish data for key, val in six.iteritems(ret): - kwargs['__pub_{0}'.format(key)] = copy.deepcopy(val) + if key is not 'kwargs': + kwargs['__pub_{0}'.format(key)] = copy.deepcopy(val) ret['return'] = self.functions[func](*args, **kwargs) - # runners do not provide retcode - if 'retcode' in self.functions.pack['__context__']: - ret['retcode'] = self.functions.pack['__context__']['retcode'] + if not self.standalone: + # runners do not provide retcode + if 'retcode' in self.functions.pack['__context__']: + ret['retcode'] = self.functions.pack['__context__']['retcode'] - ret['success'] = True + ret['success'] = True - data_returner = data.get('returner', None) - if data_returner or self.schedule_returner: - if 'return_config' in data: - ret['ret_config'] = data['return_config'] - if 'return_kwargs' in data: - ret['ret_kwargs'] = data['return_kwargs'] - rets = [] - for returner in [data_returner, self.schedule_returner]: - if isinstance(returner, six.string_types): - rets.append(returner) - elif isinstance(returner, list): - rets.extend(returner) - # simple de-duplication with order retained - for returner in OrderedDict.fromkeys(rets): - ret_str = '{0}.returner'.format(returner) - if ret_str in self.returners: - self.returners[ret_str](ret) - else: - log.info( - 'Job {0} using invalid returner: {1}. Ignoring.'.format( - func, returner + data_returner = data.get('returner', None) + if data_returner or self.schedule_returner: + if 'return_config' in data: + ret['ret_config'] = data['return_config'] + if 'return_kwargs' in data: + ret['ret_kwargs'] = data['return_kwargs'] + rets = [] + for returner in [data_returner, self.schedule_returner]: + if isinstance(returner, six.string_types): + rets.append(returner) + elif isinstance(returner, list): + rets.extend(returner) + # simple de-duplication with order retained + for returner in OrderedDict.fromkeys(rets): + ret_str = '{0}.returner'.format(returner) + if ret_str in self.returners: + self.returners[ret_str](ret) + else: + log.info( + 'Job {0} using invalid returner: {1}. Ignoring.'.format( + func, returner + ) ) - ) except Exception: log.exception("Unhandled exception running {0}".format(ret['fun'])) @@ -922,24 +929,25 @@ class Schedule(object): except Exception as exc: log.exception("Unhandled exception firing event: {0}".format(exc)) - log.debug('schedule.handle_func: Removing {0}'.format(proc_fn)) + if not self.standalone: + log.debug('schedule.handle_func: Removing {0}'.format(proc_fn)) - try: - os.unlink(proc_fn) - except OSError as exc: - if exc.errno == errno.EEXIST or exc.errno == errno.ENOENT: - # EEXIST and ENOENT are OK because the file is gone and that's what - # we wanted - pass - else: - log.error("Failed to delete '{0}': {1}".format(proc_fn, exc.errno)) - # Otherwise, failing to delete this file is not something - # we can cleanly handle. - raise - finally: - if multiprocessing_enabled: - # Let's make sure we exit the process! - sys.exit(salt.defaults.exitcodes.EX_GENERIC) + try: + os.unlink(proc_fn) + except OSError as exc: + if exc.errno == errno.EEXIST or exc.errno == errno.ENOENT: + # EEXIST and ENOENT are OK because the file is gone and that's what + # we wanted + pass + else: + log.error("Failed to delete '{0}': {1}".format(proc_fn, exc.errno)) + # Otherwise, failing to delete this file is not something + # we can cleanly handle. + raise + finally: + if multiprocessing_enabled: + # Let's make sure we exit the process! + sys.exit(salt.defaults.exitcodes.EX_GENERIC) def eval(self): ''' diff --git a/salt/utils/state.py b/salt/utils/state.py new file mode 100644 index 0000000000..3251e6b3bd --- /dev/null +++ b/salt/utils/state.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +''' +Utility functions for state functions + +.. versionadded:: Oxygen +''' + +# Import Python Libs +from __future__ import absolute_import + +# Import Salt libs +from salt.ext import six +import salt.state + +_empty = object() + + +def gen_tag(low): + ''' + Generate the running dict tag string from the low data structure + ''' + return '{0[state]}_|-{0[__id__]}_|-{0[name]}_|-{0[fun]}'.format(low) + + +def search_onfail_requisites(sid, highstate): + ''' + For a particular low chunk, search relevant onfail related states + ''' + onfails = [] + if '_|-' in sid: + st = salt.state.split_low_tag(sid) + else: + st = {'__id__': sid} + for fstate, fchunks in six.iteritems(highstate): + if fstate == st['__id__']: + continue + else: + for mod_, fchunk in six.iteritems(fchunks): + if ( + not isinstance(mod_, six.string_types) or + mod_.startswith('__') + ): + continue + else: + if not isinstance(fchunk, list): + continue + else: + # bydefault onfail will fail, but you can + # set onfail_stop: False to prevent the highstate + # to stop if you handle it + onfail_handled = False + for fdata in fchunk: + if not isinstance(fdata, dict): + continue + onfail_handled = (fdata.get('onfail_stop', True) + is False) + if onfail_handled: + break + if not onfail_handled: + continue + for fdata in fchunk: + if not isinstance(fdata, dict): + continue + for knob, fvalue in six.iteritems(fdata): + if knob != 'onfail': + continue + for freqs in fvalue: + for fmod, fid in six.iteritems(freqs): + if not ( + fid == st['__id__'] and + fmod == st.get('state', fmod) + ): + continue + onfails.append((fstate, mod_, fchunk)) + return onfails + + +def check_onfail_requisites(state_id, state_result, running, highstate): + ''' + When a state fail and is part of a highstate, check + if there is onfail requisites. + When we find onfail requisites, we will consider the state failed + only if at least one of those onfail requisites also failed + + Returns: + + True: if onfail handlers suceeded + False: if one on those handler failed + None: if the state does not have onfail requisites + + ''' + nret = None + if ( + state_id and state_result and + highstate and isinstance(highstate, dict) + ): + onfails = search_onfail_requisites(state_id, highstate) + if onfails: + for handler in onfails: + fstate, mod_, fchunk = handler + for rstateid, rstate in six.iteritems(running): + if '_|-' in rstateid: + st = salt.state.split_low_tag(rstateid) + # in case of simple state, try to guess + else: + id_ = rstate.get('__id__', rstateid) + if not id_: + raise ValueError('no state id') + st = {'__id__': id_, 'state': mod_} + if mod_ == st['state'] and fstate == st['__id__']: + ofresult = rstate.get('result', _empty) + if ofresult in [False, True]: + nret = ofresult + if ofresult is False: + # as soon as we find an errored onfail, we stop + break + # consider that if we parsed onfailes without changing + # the ret, that we have failed + if nret is None: + nret = False + return nret + + +def check_result(running, recurse=False, highstate=None): + ''' + Check the total return value of the run and determine if the running + dict has any issues + ''' + if not isinstance(running, dict): + return False + + if not running: + return False + + ret = True + for state_id, state_result in six.iteritems(running): + if not recurse and not isinstance(state_result, dict): + ret = False + if ret and isinstance(state_result, dict): + result = state_result.get('result', _empty) + if result is False: + ret = False + # only override return value if we are not already failed + elif result is _empty and isinstance(state_result, dict) and ret: + ret = check_result( + state_result, recurse=True, highstate=highstate) + # if we detect a fail, check for onfail requisites + if not ret: + # ret can be None in case of no onfail reqs, recast it to bool + ret = bool(check_onfail_requisites(state_id, state_result, + running, highstate)) + # return as soon as we got a failure + if not ret: + break + return ret + + +def merge_subreturn(original_return, sub_return, subkey=None): + ''' + Update an existing state return (`original_return`) in place + with another state return (`sub_return`), i.e. for a subresource. + + Returns: + dict: The updated state return. + + The existing state return does not need to have all the required fields, + as this is meant to be called from the internals of a state function, + but any existing data will be kept and respected. + + It is important after using this function to check the return value + to see if it is False, in which case the main state should return. + Prefer to check `_ret['result']` instead of `ret['result']`, + as the latter field may not yet be populated. + + Code Example: + + .. code-block:: python + def state_func(name, config, alarm=None): + ret = {'name': name, 'comment': '', 'changes': {}} + if alarm: + _ret = __states__['subresource.managed'](alarm) + __utils__['state.merge_subreturn'](ret, _ret) + if _ret['result'] is False: + return ret + ''' + if not subkey: + subkey = sub_return['name'] + + if sub_return['result'] is False: + # True or None stay the same + original_return['result'] = sub_return['result'] + + sub_comment = sub_return['comment'] + if not isinstance(sub_comment, list): + sub_comment = [sub_comment] + original_return.setdefault('comment', []) + if isinstance(original_return['comment'], list): + original_return['comment'].extend(sub_comment) + else: + if original_return['comment']: + # Skip for empty original comments + original_return['comment'] += u'\n' + original_return['comment'] += u'\n'.join(sub_comment) + + if sub_return['changes']: # changes always exists + original_return.setdefault('changes', {}) + original_return['changes'][subkey] = sub_return['changes'] + + if sub_return.get('pchanges'): # pchanges may or may not exist + original_return.setdefault('pchanges', {}) + original_return['pchanges'][subkey] = sub_return['pchanges'] + + return original_return diff --git a/salt/utils/stringutils.py b/salt/utils/stringutils.py index fc194930b9..89e91b0be7 100644 --- a/salt/utils/stringutils.py +++ b/salt/utils/stringutils.py @@ -170,3 +170,30 @@ def contains_whitespace(text): Returns True if there are any whitespace characters in the string ''' return any(x.isspace() for x in text) + + +def human_to_bytes(size): + ''' + Given a human-readable byte string (e.g. 2G, 30M), + return the number of bytes. Will return 0 if the argument has + unexpected form. + + .. versionadded:: Oxygen + ''' + sbytes = size[:-1] + unit = size[-1] + if sbytes.isdigit(): + sbytes = int(sbytes) + if unit == 'P': + sbytes *= 1125899906842624 + elif unit == 'T': + sbytes *= 1099511627776 + elif unit == 'G': + sbytes *= 1073741824 + elif unit == 'M': + sbytes *= 1048576 + else: + sbytes = 0 + else: + sbytes = 0 + return sbytes diff --git a/salt/utils/url.py b/salt/utils/url.py index ccf112f5d6..ff02517f9d 100644 --- a/salt/utils/url.py +++ b/salt/utils/url.py @@ -27,12 +27,7 @@ def parse(url): resource = url.split('salt://', 1)[-1] if '?env=' in resource: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the salt:// URL. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". path, saltenv = resource.split('?env=', 1)[0], None elif '?saltenv=' in resource: path, saltenv = resource.split('?saltenv=', 1) diff --git a/salt/utils/validate/path.py b/salt/utils/validate/path.py index 1385b9bbce..87f2d789c7 100644 --- a/salt/utils/validate/path.py +++ b/salt/utils/validate/path.py @@ -64,3 +64,14 @@ def is_readable(path): # The path does not exist return False + + +def is_executable(path): + ''' + Check if a given path is executable by the current user. + + :param path: The path to check + :returns: True or False + ''' + + return os.access(path, os.X_OK) diff --git a/salt/utils/value.py b/salt/utils/value.py new file mode 100644 index 0000000000..222dd10813 --- /dev/null +++ b/salt/utils/value.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +''' +Utility functions used for values. + +.. versionadded:: Oxygen +''' + +# Import Python libs +from __future__ import absolute_import + + +def xor(*variables): + ''' + XOR definition for multiple variables + ''' + sum_ = False + for value in variables: + sum_ = sum_ ^ bool(value) + return sum_ diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index bfe9a14353..b239b269b0 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -8,7 +8,7 @@ This is a base library used by a number of VMware services such as VMware ESX, ESXi, and vCenter servers. :codeauthor: Nitin Madhok -:codeauthor: Alexandru Bleotu +:codeauthor: Alexandru Bleotu Dependencies ~~~~~~~~~~~~ @@ -479,6 +479,28 @@ def is_connection_to_a_vcenter(service_instance): '\'VirtualCenter/HostAgent\''.format(api_type)) +def get_service_info(service_instance): + ''' + Returns information of the vCenter or ESXi host + + service_instance + The Service Instance from which to obtain managed object references. + ''' + try: + return service_instance.content.about + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + + def _get_dvs(service_instance, dvs_name): ''' Return a reference to a Distributed Virtual Switch object. @@ -983,6 +1005,287 @@ def list_objects(service_instance, vim_object, properties=None): return items +def get_license_manager(service_instance): + ''' + Returns the license manager. + + service_instance + The Service Instance Object from which to obrain the license manager. + ''' + + log.debug('Retrieving license manager') + try: + lic_manager = service_instance.content.licenseManager + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + return lic_manager + + +def get_license_assignment_manager(service_instance): + ''' + Returns the license assignment manager. + + service_instance + The Service Instance Object from which to obrain the license manager. + ''' + + log.debug('Retrieving license assignment manager') + try: + lic_assignment_manager = \ + service_instance.content.licenseManager.licenseAssignmentManager + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + if not lic_assignment_manager: + raise salt.exceptions.VMwareObjectRetrievalError( + 'License assignment manager was not retrieved') + return lic_assignment_manager + + +def get_licenses(service_instance, license_manager=None): + ''' + Returns the licenses on a specific instance. + + service_instance + The Service Instance Object from which to obrain the licenses. + + license_manager + The License Manager object of the service instance. If not provided it + will be retrieved. + ''' + + if not license_manager: + license_manager = get_license_manager(service_instance) + log.debug('Retrieving licenses') + try: + return license_manager.licenses + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + + +def add_license(service_instance, key, description, license_manager=None): + ''' + Adds a license. + + service_instance + The Service Instance Object. + + key + The key of the license to add. + + description + The description of the license to add. + + license_manager + The License Manager object of the service instance. If not provided it + will be retrieved. + ''' + if not license_manager: + license_manager = get_license_manager(service_instance) + label = vim.KeyValue() + label.key = 'VpxClientLicenseLabel' + label.value = description + log.debug('Adding license \'{}\''.format(description)) + try: + license = license_manager.AddLicense(key, [label]) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + return license + + +def get_assigned_licenses(service_instance, entity_ref=None, entity_name=None, + license_assignment_manager=None): + ''' + Returns the licenses assigned to an entity. If entity ref is not provided, + then entity_name is assumed to be the vcenter. This is later checked if + the entity name is provided. + + service_instance + The Service Instance Object from which to obtain the licenses. + + entity_ref + VMware entity to get the assigned licenses for. + If None, the entity is the vCenter itself. + Default is None. + + entity_name + Entity name used in logging. + Default is None. + + license_assignment_manager + The LicenseAssignmentManager object of the service instance. + If not provided it will be retrieved. + Default is None. + ''' + if not license_assignment_manager: + license_assignment_manager = \ + get_license_assignment_manager(service_instance) + if not entity_name: + raise salt.exceptions.ArgumentValueError('No entity_name passed') + # If entity_ref is not defined, then interested in the vcenter + entity_id = None + entity_type = 'moid' + check_name = False + if not entity_ref: + if entity_name: + check_name = True + entity_type = 'uuid' + try: + entity_id = service_instance.content.about.instanceUuid + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + else: + entity_id = entity_ref._moId + + log.trace('Retrieving licenses assigned to \'{0}\''.format(entity_name)) + try: + assignments = \ + license_assignment_manager.QueryAssignedLicenses(entity_id) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + + if entity_type == 'uuid' and len(assignments) > 1: + log.trace('Unexpectectedly retrieved more than one' + ' VCenter license assignment.') + raise salt.exceptions.VMwareObjectRetrievalError( + 'Unexpected return. Expect only a single assignment') + + if check_name: + if entity_name != assignments[0].entityDisplayName: + log.trace('Getting license info for wrong vcenter: ' + '{0} != {1}'.format(entity_name, + assignments[0].entityDisplayName)) + raise salt.exceptions.VMwareObjectRetrievalError( + 'Got license assignment info for a different vcenter') + + return [a.assignedLicense for a in assignments] + + +def assign_license(service_instance, license_key, license_name, + entity_ref=None, entity_name=None, + license_assignment_manager=None): + ''' + Assigns a license to an entity. + + service_instance + The Service Instance Object from which to obrain the licenses. + + license_key + The key of the license to add. + + license_name + The description of the license to add. + + entity_ref + VMware entity to assign the license to. + If None, the entity is the vCenter itself. + Default is None. + + entity_name + Entity name used in logging. + Default is None. + + license_assignment_manager + The LicenseAssignmentManager object of the service instance. + If not provided it will be retrieved + Default is None. + ''' + if not license_assignment_manager: + license_assignment_manager = \ + get_license_assignment_manager(service_instance) + entity_id = None + + if not entity_ref: + # vcenter + try: + entity_id = service_instance.content.about.instanceUuid + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + raise salt.exceptions.VMwareRuntimeError(exc.msg) + if not entity_name: + entity_name = 'vCenter' + else: + # e.g. vsan cluster or host + entity_id = entity_ref._moId + + log.trace('Assigning license to \'{0}\''.format(entity_name)) + try: + license = license_assignment_manager.UpdateAssignedLicense( + entity_id, + license_key) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + return license + + def list_datacenters(service_instance): ''' Returns a list of datacenters associated with a given service instance. @@ -1193,6 +1496,185 @@ def list_datastores(service_instance): return list_objects(service_instance, vim.Datastore) +def get_datastores(service_instance, reference, datastore_names=None, + backing_disk_ids=None, get_all_datastores=False): + ''' + Returns a list of vim.Datastore objects representing the datastores visible + from a VMware object, filtered by their names, or the backing disk + cannonical name or scsi_addresses + + service_instance + The Service Instance Object from which to obtain datastores. + + reference + The VMware object from which the datastores are visible. + + datastore_names + The list of datastore names to be retrieved. Default value is None. + + backing_disk_ids + The list of canonical names of the disks backing the datastores + to be retrieved. Only supported if reference is a vim.HostSystem. + Default value is None + + get_all_datastores + Specifies whether to retrieve all disks in the host. + Default value is False. + ''' + obj_name = get_managed_object_name(reference) + if get_all_datastores: + log.trace('Retrieving all datastores visible to ' + '\'{0}\''.format(obj_name)) + else: + log.trace('Retrieving datastores visible to \'{0}\': names = ({1}); ' + 'backing disk ids = ({2})'.format(obj_name, datastore_names, + backing_disk_ids)) + if backing_disk_ids and not isinstance(reference, vim.HostSystem): + + raise salt.exceptions.ArgumentValueError( + 'Unsupported reference type \'{0}\' when backing disk filter ' + 'is set'.format(reference.__class__.__name__)) + if (not get_all_datastores) and backing_disk_ids: + # At this point we know the reference is a vim.HostSystem + log.debug('Filtering datastores with backing disk ids: {}' + ''.format(backing_disk_ids)) + storage_system = get_storage_system(service_instance, reference, + obj_name) + props = salt.utils.vmware.get_properties_of_managed_object( + storage_system, ['fileSystemVolumeInfo.mountInfo']) + mount_infos = props.get('fileSystemVolumeInfo.mountInfo', []) + disk_datastores = [] + # Non vmfs volumes aren't backed by a disk + for vol in [i.volume for i in mount_infos if + isinstance(i.volume, vim.HostVmfsVolume)]: + + if not [e for e in vol.extent if e.diskName in backing_disk_ids]: + # Skip volume if it doesn't contain an extent with a + # canonical name of interest + continue + log.debug('Found datastore \'{0}\' for disk id(s) \'{1}\'' + ''.format(vol.name, + [e.diskName for e in vol.extent])) + disk_datastores.append(vol.name) + log.debug('Datastore found for disk filter: {}' + ''.format(disk_datastores)) + if datastore_names: + datastore_names.extend(disk_datastores) + else: + datastore_names = disk_datastores + + if (not get_all_datastores) and (not datastore_names): + log.trace('No datastore to be filtered after retrieving the datastores ' + 'backed by the disk id(s) \'{0}\''.format(backing_disk_ids)) + return [] + + log.trace('datastore_names = {0}'.format(datastore_names)) + + # Use the default traversal spec + if isinstance(reference, vim.HostSystem): + # Create a different traversal spec for hosts because it looks like the + # default doesn't retrieve the datastores + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + name='host_datastore_traversal', + path='datastore', + skip=False, + type=vim.HostSystem) + elif isinstance(reference, vim.ClusterComputeResource): + # Traversal spec for clusters + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + name='cluster_datastore_traversal', + path='datastore', + skip=False, + type=vim.ClusterComputeResource) + elif isinstance(reference, vim.Datacenter): + # Traversal spec for clusters + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + name='datacenter_datastore_traversal', + path='datastore', + skip=False, + type=vim.Datacenter) + elif isinstance(reference, vim.Folder) and \ + get_managed_object_name(reference) == 'Datacenters': + # Traversal of root folder (doesn't support multiple levels of Folders) + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='childEntity', + selectSet=[ + vmodl.query.PropertyCollector.TraversalSpec( + path='datastore', + skip=False, + type=vim.Datacenter)], + skip=False, + type=vim.Folder) + else: + raise salt.exceptions.ArgumentValueError( + 'Unsupported reference type \'{0}\'' + ''.format(reference.__class__.__name__)) + + items = get_mors_with_properties(service_instance, + object_type=vim.Datastore, + property_list=['name'], + container_ref=reference, + traversal_spec=traversal_spec) + log.trace('Retrieved {0} datastores'.format(len(items))) + items = [i for i in items if get_all_datastores or i['name'] in + datastore_names] + log.trace('Filtered datastores: {0}'.format([i['name'] for i in items])) + return [i['object'] for i in items] + + +def rename_datastore(datastore_ref, new_datastore_name): + ''' + Renames a datastore + + datastore_ref + vim.Datastore reference to the datastore object to be changed + + new_datastore_name + New datastore name + ''' + ds_name = get_managed_object_name(datastore_ref) + log.debug('Renaming datastore \'{0}\' to ' + '\'{1}\''.format(ds_name, new_datastore_name)) + try: + datastore_ref.RenameDatastore(new_datastore_name) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + + +def get_storage_system(service_instance, host_ref, hostname=None): + ''' + Returns a host's storage system + ''' + + if not hostname: + hostname = get_managed_object_name(host_ref) + + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='configManager.storageSystem', + type=vim.HostSystem, + skip=False) + objs = get_mors_with_properties(service_instance, + vim.HostStorageSystem, + property_list=['systemFile'], + container_ref=host_ref, + traversal_spec=traversal_spec) + if not objs: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Host\'s \'{0}\' storage system was not retrieved' + ''.format(hostname)) + log.trace('[{0}] Retrieved storage system'.format(hostname)) + return objs[0]['object'] + + def get_hosts(service_instance, datacenter_name=None, host_names=None, cluster_name=None, get_all_hosts=False): ''' diff --git a/salt/utils/vsan.py b/salt/utils/vsan.py new file mode 100644 index 0000000000..8ad713cd3e --- /dev/null +++ b/salt/utils/vsan.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +''' +Connection library for VMware vSAN endpoint + +This library used the vSAN extension of the VMware SDK +used to manage vSAN related objects + +:codeauthor: Alexandru Bleotu + +Dependencies +~~~~~~~~~~~~ + +- pyVmomi Python Module + +pyVmomi +------- + +PyVmomi can be installed via pip: + +.. code-block:: bash + + pip install pyVmomi + +.. note:: + + versions of Python. If using version 6.0 of pyVmomi, Python 2.6, + Python 2.7.9, or newer must be present. This is due to an upstream dependency + in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the + version of Python is not in the supported range, you will need to install an + earlier version of pyVmomi. See `Issue #29537`_ for more information. + +.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 + +Based on the note above, to install an earlier version of pyVmomi than the +version currently listed in PyPi, run the following: + +.. code-block:: bash + + pip install pyVmomi==5.5.0.2014.1.1 + +The 5.5.0.2014.1.1 is a known stable version that this original VMware utils file +was developed against. +''' + +# Import Python Libs +from __future__ import absolute_import +import sys +import logging +import ssl + +# Import Salt Libs +from salt.exceptions import VMwareApiError, VMwareRuntimeError +import salt.utils.vmware + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +try: + from salt.ext.vsan import vsanapiutils + HAS_PYVSAN = True +except ImportError: + HAS_PYVSAN = False + +# Get Logging Started +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only load if PyVmomi is installed. + ''' + if HAS_PYVSAN and HAS_PYVMOMI: + return True + else: + return False, 'Missing dependency: The salt.utils.vsan module ' \ + 'requires pyvmomi and the pyvsan extension library' + + +def vsan_supported(service_instance): + ''' + Returns whether vsan is supported on the vCenter: + api version needs to be 6 or higher + + service_instance + Service instance to the host or vCenter + ''' + try: + api_version = service_instance.content.about.apiVersion + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + if int(api_version.split('.')[0]) < 6: + return False + return True + + +def get_vsan_cluster_config_system(service_instance): + ''' + Returns a vim.cluster.VsanVcClusterConfigSystem object + + service_instance + Service instance to the host or vCenter + ''' + + #TODO Replace when better connection mechanism is available + + #For python 2.7.9 and later, the defaul SSL conext has more strict + #connection handshaking rule. We may need turn of the hostname checking + #and client side cert verification + context = None + if sys.version_info[:3] > (2, 7, 8): + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + stub = service_instance._stub + vc_mos = vsanapiutils.GetVsanVcMos(stub, context=context) + return vc_mos['vsan-cluster-config-system'] + + +def get_cluster_vsan_info(cluster_ref): + ''' + Returns the extended cluster vsan configuration object + (vim.VsanConfigInfoEx). + + cluster_ref + Reference to the cluster + ''' + + cluster_name = salt.utils.vmware.get_managed_object_name(cluster_ref) + log.trace('Retrieving cluster vsan info of cluster ' + '\'{0}\''.format(cluster_name)) + si = salt.utils.vmware.get_service_instance_from_managed_object( + cluster_ref) + vsan_cl_conf_sys = get_vsan_cluster_config_system(si) + try: + return vsan_cl_conf_sys.VsanClusterGetConfig(cluster_ref) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + + +def reconfigure_cluster_vsan(cluster_ref, cluster_vsan_spec): + ''' + Reconfigures the VSAN system of a cluster. + + cluster_ref + Reference to the cluster + + cluster_vsan_spec + Cluster VSAN reconfigure spec (vim.vsan.ReconfigSpec). + ''' + cluster_name = salt.utils.vmware.get_managed_object_name(cluster_ref) + log.trace('Reconfiguring vsan on cluster \'{0}\': {1}' + ''.format(cluster_name, cluster_vsan_spec)) + si = salt.utils.vmware.get_service_instance_from_managed_object( + cluster_ref) + vsan_cl_conf_sys = salt.utils.vsan.get_vsan_cluster_config_system(si) + try: + task = vsan_cl_conf_sys.VsanClusterReconfig(cluster_ref, + cluster_vsan_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + _wait_for_tasks([task], si) + + +def _wait_for_tasks(tasks, service_instance): + ''' + Wait for tasks created via the VSAN API + ''' + log.trace('Waiting for vsan tasks: {0}' + ''.format(', '.join([str(t) for t in tasks]))) + try: + vsanapiutils.WaitForTasks(tasks, service_instance) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + log.trace('Tasks {0} finished successfully' + ''.format(', '.join([str(t) for t in tasks]))) diff --git a/salt/utils/xmlutil.py b/salt/utils/xmlutil.py index 559d2eeae8..bb07921aaa 100644 --- a/salt/utils/xmlutil.py +++ b/salt/utils/xmlutil.py @@ -14,7 +14,7 @@ def to_dict(xmltree): ''' # If this object has no children, the for..loop below will return nothing # for it, so just return a single dict representing it. - if len(xmltree.getchildren()) < 1: + if len(xmltree.getchildren()) < 1 and len(xmltree.attrib.items()) < 1: name = xmltree.tag if '}' in name: comps = name.split('}') @@ -33,6 +33,8 @@ def to_dict(xmltree): if name not in xmldict: if len(item.getchildren()) > 0: xmldict[name] = to_dict(item) + elif len(item.attrib.items()) > 0: + xmldict[name] = to_dict(item) else: xmldict[name] = item.text else: @@ -42,4 +44,12 @@ def to_dict(xmltree): if not isinstance(xmldict[name], list): xmldict[name] = [xmldict[name]] xmldict[name].append(to_dict(item)) + + for attrName, attrValue in xmltree.attrib.items(): + if attrName not in xmldict: + xmldict[attrName] = attrValue + else: + # Attempt to ensure that items are not overwritten by attributes. + xmldict["attr{0}".format(attrName)] = attrValue + return xmldict diff --git a/salt/wheel/file_roots.py b/salt/wheel/file_roots.py index 109c8ea79c..8df2eee4b8 100644 --- a/salt/wheel/file_roots.py +++ b/salt/wheel/file_roots.py @@ -8,7 +8,6 @@ from __future__ import absolute_import import os # Import salt libs -import salt.utils import salt.utils.files # Import 3rd-party libs @@ -28,7 +27,7 @@ def find(path, saltenv='base'): if os.path.isfile(full): # Add it to the dict with salt.utils.files.fopen(full, 'rb') as fp_: - if salt.utils.istextfile(fp_): + if salt.utils.files.is_text_file(fp_): ret.append({full: 'txt'}) else: ret.append({full: 'bin'}) diff --git a/salt/wheel/pillar_roots.py b/salt/wheel/pillar_roots.py index 9eab69344a..65790e17d9 100644 --- a/salt/wheel/pillar_roots.py +++ b/salt/wheel/pillar_roots.py @@ -9,7 +9,6 @@ from __future__ import absolute_import import os # Import salt libs -import salt.utils import salt.utils.files # Import 3rd-party libs @@ -29,7 +28,7 @@ def find(path, saltenv='base'): if os.path.isfile(full): # Add it to the dict with salt.utils.files.fopen(full, 'rb') as fp_: - if salt.utils.istextfile(fp_): + if salt.utils.files.is_text_file(fp_): ret.append({full: 'txt'}) else: ret.append({full: 'bin'}) diff --git a/setup.py b/setup.py index effdc2f230..aacebb95a1 100755 --- a/setup.py +++ b/setup.py @@ -234,6 +234,7 @@ class GenerateSaltSyspaths(Command): spm_formula_path=self.distribution.salt_spm_formula_dir, spm_pillar_path=self.distribution.salt_spm_pillar_dir, spm_reactor_path=self.distribution.salt_spm_reactor_dir, + home_dir=self.distribution.salt_home_dir, ) ) @@ -724,6 +725,7 @@ PIDFILE_DIR = {pidfile_dir!r} SPM_FORMULA_PATH = {spm_formula_path!r} SPM_PILLAR_PATH = {spm_pillar_path!r} SPM_REACTOR_PATH = {spm_reactor_path!r} +HOME_DIR = {home_dir!r} ''' @@ -868,6 +870,8 @@ class SaltDistribution(distutils.dist.Distribution): 'Salt\'s pre-configured SPM pillar directory'), ('salt-spm-reactor-dir=', None, 'Salt\'s pre-configured SPM reactor directory'), + ('salt-home-dir=', None, + 'Salt\'s pre-configured user home directory'), ] def __init__(self, attrs=None): @@ -892,6 +896,7 @@ class SaltDistribution(distutils.dist.Distribution): self.salt_spm_formula_dir = None self.salt_spm_pillar_dir = None self.salt_spm_reactor_dir = None + self.salt_home_dir = None self.name = 'salt-ssh' if PACKAGED_FOR_SALT_SSH else 'salt' self.salt_version = __version__ # pylint: disable=undefined-variable diff --git a/tests/consist.py b/tests/consist.py index fea84b59f5..ad4a0b6403 100644 --- a/tests/consist.py +++ b/tests/consist.py @@ -9,13 +9,13 @@ import pprint import optparse # Import Salt libs -import salt.utils +import salt.utils.color # Import 3rd-party libs import yaml from salt.ext import six -colors = salt.utils.get_colors() +colors = salt.utils.color.get_colors() def parse(): diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index f2651f9470..92dcd8fedf 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -49,7 +49,8 @@ import salt.minion import salt.runner import salt.output import salt.version -import salt.utils # Can be removed once get_colors and appendproctitle are moved +import salt.utils # Can be removed once appendproctitle is moved +import salt.utils.color import salt.utils.files import salt.utils.path import salt.utils.platform @@ -188,7 +189,7 @@ class TestDaemon(object): def __init__(self, parser): self.parser = parser - self.colors = salt.utils.get_colors(self.parser.options.no_colors is False) + self.colors = salt.utils.color.get_colors(self.parser.options.no_colors is False) if salt.utils.platform.is_windows(): # There's no shell color support on windows... for key in self.colors: diff --git a/tests/integration/client/test_standard.py b/tests/integration/client/test_standard.py index f58c7272cc..b0e8809da1 100644 --- a/tests/integration/client/test_standard.py +++ b/tests/integration/client/test_standard.py @@ -147,3 +147,33 @@ class StdTest(ModuleCase): finally: os.unlink(key_file) + + def test_missing_minion_list(self): + ''' + test cmd with missing minion in nodegroup + ''' + ret = self.client.cmd( + 'minion,ghostminion', + 'test.ping', + tgt_type='list' + ) + self.assertIn('minion', ret) + self.assertIn('ghostminion', ret) + self.assertEqual(True, ret['minion']) + self.assertEqual(u'Minion did not return. [No response]', + ret['ghostminion']) + + def test_missing_minion_nodegroup(self): + ''' + test cmd with missing minion in nodegroup + ''' + ret = self.client.cmd( + 'missing_minion', + 'test.ping', + tgt_type='nodegroup' + ) + self.assertIn('minion', ret) + self.assertIn('ghostminion', ret) + self.assertEqual(True, ret['minion']) + self.assertEqual(u'Minion did not return. [No response]', + ret['ghostminion']) diff --git a/tests/integration/cloud/providers/test_digital_ocean.py b/tests/integration/cloud/providers/test_digitalocean.py similarity index 98% rename from tests/integration/cloud/providers/test_digital_ocean.py rename to tests/integration/cloud/providers/test_digitalocean.py index 950b637988..adbf76f43e 100644 --- a/tests/integration/cloud/providers/test_digital_ocean.py +++ b/tests/integration/cloud/providers/test_digitalocean.py @@ -17,7 +17,7 @@ from salt.config import cloud_providers_config # Create the cloud instance name to be used throughout the tests INSTANCE_NAME = generate_random_name('CLOUD-TEST-') -PROVIDER_NAME = 'digital_ocean' +PROVIDER_NAME = 'digitalocean' class DigitalOceanTest(ShellCase): @@ -66,7 +66,7 @@ class DigitalOceanTest(ShellCase): def test_list_images(self): ''' - Tests the return of running the --list-images command for digital ocean + Tests the return of running the --list-images command for digitalocean ''' image_list = self.run_cloud('--list-images {0}'.format(PROVIDER_NAME)) self.assertIn( @@ -76,7 +76,7 @@ class DigitalOceanTest(ShellCase): def test_list_locations(self): ''' - Tests the return of running the --list-locations command for digital ocean + Tests the return of running the --list-locations command for digitalocean ''' _list_locations = self.run_cloud('--list-locations {0}'.format(PROVIDER_NAME)) self.assertIn( @@ -86,7 +86,7 @@ class DigitalOceanTest(ShellCase): def test_list_sizes(self): ''' - Tests the return of running the --list-sizes command for digital ocean + Tests the return of running the --list-sizes command for digitalocean ''' _list_sizes = self.run_cloud('--list-sizes {0}'.format(PROVIDER_NAME)) self.assertIn( diff --git a/tests/integration/cloud/test_cloud.py b/tests/integration/cloud/test_cloud.py index 3eb85125e1..983f34fded 100644 --- a/tests/integration/cloud/test_cloud.py +++ b/tests/integration/cloud/test_cloud.py @@ -40,11 +40,11 @@ class CloudClientTestCase(ShellCase): @expensiveTest def setUp(self): self.config_file = os.path.join(RUNTIME_VARS.TMP_CONF_CLOUD_PROVIDER_INCLUDES, - 'digital_ocean.conf') + 'digitalocean.conf') self.provider_name = 'digitalocean-config' self.image_name = '14.04.5 x64' - # Use a --list-images salt-cloud call to see if the Digital Ocean provider is + # Use a --list-images salt-cloud call to see if the DigitalOcean provider is # configured correctly before running any tests. images = self.run_cloud('--list-images {0}'.format(self.provider_name)) diff --git a/tests/integration/files/conf/cloud.providers.d/digital_ocean.conf b/tests/integration/files/conf/cloud.providers.d/digital_ocean.conf index 44f89b558f..b0248442a9 100644 --- a/tests/integration/files/conf/cloud.providers.d/digital_ocean.conf +++ b/tests/integration/files/conf/cloud.providers.d/digital_ocean.conf @@ -1,5 +1,5 @@ digitalocean-config: - driver: digital_ocean + driver: digitalocean personal_access_token: '' ssh_key_file: '' ssh_key_name: '' diff --git a/tests/integration/files/conf/master b/tests/integration/files/conf/master index b78ecd9180..10c2eec07b 100644 --- a/tests/integration/files/conf/master +++ b/tests/integration/files/conf/master @@ -78,6 +78,7 @@ nodegroups: redundant_minions: N@min or N@mins nodegroup_loop_a: N@nodegroup_loop_b nodegroup_loop_b: N@nodegroup_loop_a + missing_minion: L@minion,ghostminion mysql.host: localhost diff --git a/tests/integration/files/file/base/mysql/select_query.sql b/tests/integration/files/file/base/mysql/select_query.sql new file mode 100644 index 0000000000..10cf4850fd --- /dev/null +++ b/tests/integration/files/file/base/mysql/select_query.sql @@ -0,0 +1,7 @@ +CREATE TABLE test_select (a INT); +insert into test_select values (1); +insert into test_select values (3); +insert into test_select values (4); +insert into test_select values (5); +update test_select set a=2 where a=1; +select * from test_select; diff --git a/tests/integration/files/file/base/mysql/update_query.sql b/tests/integration/files/file/base/mysql/update_query.sql new file mode 100644 index 0000000000..34cee2dab1 --- /dev/null +++ b/tests/integration/files/file/base/mysql/update_query.sql @@ -0,0 +1,3 @@ +CREATE TABLE test_update (a INT); +insert into test_update values (1); +update test_update set a=2 where a=1; diff --git a/tests/integration/modules/test_groupadd.py b/tests/integration/modules/test_groupadd.py index ed75916ba3..9963793ca1 100644 --- a/tests/integration/modules/test_groupadd.py +++ b/tests/integration/modules/test_groupadd.py @@ -11,6 +11,9 @@ from tests.support.helpers import destructiveTest, skip_if_not_root # Import 3rd-party libs from salt.ext.six.moves import range +import os +import grp +from salt import utils @skip_if_not_root @@ -57,6 +60,43 @@ class GroupModuleTest(ModuleCase): for x in range(size) ) + def __get_system_group_gid_range(self): + ''' + Returns (SYS_GID_MIN, SYS_GID_MAX) + ''' + defs_file = '/etc/login.defs' + if os.path.exists(defs_file): + with utils.fopen(defs_file) as defs_fd: + login_defs = dict([x.split() + for x in defs_fd.readlines() + if x.strip() + and not x.strip().startswith('#')]) + else: + login_defs = {'SYS_GID_MIN': 101, + 'SYS_GID_MAX': 999} + + gid_min = login_defs.get('SYS_GID_MIN', 101) + gid_max = login_defs.get('SYS_GID_MAX', + int(login_defs.get('GID_MIN', 1000)) - 1) + + return gid_min, gid_max + + def __get_free_system_gid(self): + ''' + Find a free system gid + ''' + + gid_min, gid_max = self.__get_system_group_gid_range() + + busy_gids = [x.gr_gid + for x in grp.getgrall() + if gid_min <= x.gr_gid <= gid_max] + + # find free system gid + for gid in range(gid_min, gid_max + 1): + if gid not in busy_gids: + return gid + @destructiveTest def test_add(self): ''' @@ -70,6 +110,42 @@ class GroupModuleTest(ModuleCase): #try adding the group again self.assertFalse(self.run_function('group.add', [self._group, self._gid])) + @destructiveTest + def test_add_system_group(self): + ''' + Test the add group function with system=True + ''' + + gid_min, gid_max = self.__get_system_group_gid_range() + + # add a new system group + self.assertTrue(self.run_function('group.add', + [self._group, None, True])) + group_info = self.run_function('group.info', [self._group]) + self.assertEqual(group_info['name'], self._group) + self.assertTrue(gid_min <= group_info['gid'] <= gid_max) + #try adding the group again + self.assertFalse(self.run_function('group.add', + [self._group])) + + @destructiveTest + def test_add_system_group_gid(self): + ''' + Test the add group function with system=True and a specific gid + ''' + + gid = self.__get_free_system_gid() + + # add a new system group + self.assertTrue(self.run_function('group.add', + [self._group, gid, True])) + group_info = self.run_function('group.info', [self._group]) + self.assertEqual(group_info['name'], self._group) + self.assertEqual(group_info['gid'], gid) + #try adding the group again + self.assertFalse(self.run_function('group.add', + [self._group, gid])) + @destructiveTest def test_delete(self): ''' diff --git a/tests/integration/modules/test_mysql.py b/tests/integration/modules/test_mysql.py index c6c82b7165..f94d01cf50 100644 --- a/tests/integration/modules/test_mysql.py +++ b/tests/integration/modules/test_mysql.py @@ -1280,6 +1280,7 @@ class MysqlModuleUserGrantTest(ModuleCase, SaltReturnAssertsMixin): testdb1 = 'tes.t\'"saltdb' testdb2 = 't_st `(:=salt%b)' testdb3 = 'test `(:=salteeb)' + test_file_query_db = 'test_query' table1 = 'foo' table2 = "foo `\'%_bar" users = { @@ -1391,13 +1392,19 @@ class MysqlModuleUserGrantTest(ModuleCase, SaltReturnAssertsMixin): name=self.testdb1, connection_user=self.user, connection_pass=self.password, - ) + ) self.run_function( 'mysql.db_remove', name=self.testdb2, connection_user=self.user, connection_pass=self.password, - ) + ) + self.run_function( + 'mysql.db_remove', + name=self.test_file_query_db, + connection_user=self.user, + connection_pass=self.password, + ) def _userCreation(self, uname, @@ -1627,3 +1634,123 @@ class MysqlModuleUserGrantTest(ModuleCase, SaltReturnAssertsMixin): "GRANT USAGE ON *.* TO ''@'localhost'", "GRANT DELETE ON `test ``(:=salteeb)`.* TO ''@'localhost'" ]) + + +@skipIf( + NO_MYSQL, + 'Please install MySQL bindings and a MySQL Server before running' + 'MySQL integration tests.' +) +class MysqlModuleFileQueryTest(ModuleCase, SaltReturnAssertsMixin): + ''' + Test file query module + ''' + + user = 'root' + password = 'poney' + testdb = 'test_file_query' + + @destructiveTest + def setUp(self): + ''' + Test presence of MySQL server, enforce a root password, create users + ''' + super(MysqlModuleFileQueryTest, self).setUp() + NO_MYSQL_SERVER = True + # now ensure we know the mysql root password + # one of theses two at least should work + ret1 = self.run_state( + 'cmd.run', + name='mysqladmin --host="localhost" -u ' + + self.user + + ' flush-privileges password "' + + self.password + + '"' + ) + ret2 = self.run_state( + 'cmd.run', + name='mysqladmin --host="localhost" -u ' + + self.user + + ' --password="' + + self.password + + '" flush-privileges password "' + + self.password + + '"' + ) + key, value = ret2.popitem() + if value['result']: + NO_MYSQL_SERVER = False + else: + self.skipTest('No MySQL Server running, or no root access on it.') + # Create some users and a test db + self.run_function( + 'mysql.db_create', + name=self.testdb, + connection_user=self.user, + connection_pass=self.password, + connection_db='mysql', + ) + + @destructiveTest + def tearDown(self): + ''' + Removes created users and db + ''' + self.run_function( + 'mysql.db_remove', + name=self.testdb, + connection_user=self.user, + connection_pass=self.password, + connection_db='mysql', + ) + + @destructiveTest + def test_update_file_query(self): + ''' + Test query without any output + ''' + ret = self.run_function( + 'mysql.file_query', + database=self.testdb, + file_name='salt://mysql/update_query.sql', + character_set='utf8', + collate='utf8_general_ci', + connection_user=self.user, + connection_pass=self.password + ) + self.assertTrue('query time' in ret) + ret.pop('query time') + self.assertEqual(ret, {'rows affected': 2}) + + @destructiveTest + def test_select_file_query(self): + ''' + Test query with table output + ''' + ret = self.run_function( + 'mysql.file_query', + database=self.testdb, + file_name='salt://mysql/select_query.sql', + character_set='utf8', + collate='utf8_general_ci', + connection_user=self.user, + connection_pass=self.password + ) + expected = { + 'rows affected': 5, + 'rows returned': 4, + 'results': [ + [ + ['2'], + ['3'], + ['4'], + ['5'] + ] + ], + 'columns': [ + ['a'] + ], + } + self.assertTrue('query time' in ret) + ret.pop('query time') + self.assertEqual(ret, expected) diff --git a/tests/integration/states/test_npm.py b/tests/integration/states/test_npm.py index 13b002d78d..d93825b593 100644 --- a/tests/integration/states/test_npm.py +++ b/tests/integration/states/test_npm.py @@ -54,7 +54,7 @@ class NpmStateTest(ModuleCase, SaltReturnAssertsMixin): Basic test to determine if NPM module successfully installs multiple packages. ''' - ret = self.run_state('npm.installed', name=None, pkgs=['pm2', 'grunt']) + ret = self.run_state('npm.installed', name='unused', pkgs=['pm2', 'grunt']) self.assertSaltTrueReturn(ret) @skipIf(salt.utils.path.which('npm') and LooseVersion(cmd.run('npm -v')) >= LooseVersion(MAX_NPM_VERSION), @@ -64,5 +64,5 @@ class NpmStateTest(ModuleCase, SaltReturnAssertsMixin): ''' Basic test to determine if NPM successfully cleans its cached packages. ''' - ret = self.run_state('npm.cache_cleaned', name=None, force=True) + ret = self.run_state('npm.cache_cleaned', name='unused', force=True) self.assertSaltTrueReturn(ret) diff --git a/tests/unit/beacons/test_btmp_beacon.py b/tests/unit/beacons/test_btmp_beacon.py new file mode 100644 index 0000000000..708dae9454 --- /dev/null +++ b/tests/unit/beacons/test_btmp_beacon.py @@ -0,0 +1,117 @@ +# coding: utf-8 + +# Python libs +from __future__ import absolute_import +import logging +import sys + +# Salt testing libs +from tests.support.unit import skipIf, TestCase +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, mock_open +from tests.support.mixins import LoaderModuleMockMixin + +# Salt libs +import salt.beacons.btmp as btmp + +if sys.version_info >= (3,): + raw = bytes('\x06\x00\x00\x00Nt\x00\x00ssh:notty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00garet\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xdd\xc7\xc2Y\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'utf-8') + pack = (6, 29774, b'ssh:notty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'\x00\x00\x00\x00', b'garet\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 0, 0, 0, 1505937373, 0, 0, 0, 0, 16777216) +else: + raw = b'\x06\x00\x00\x00Nt\x00\x00ssh:notty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00garet\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xdd\xc7\xc2Y\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + pack = (6, 29774, 'ssh:notty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', '\x00\x00\x00\x00', 'garet\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', '::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 0, 0, 0, 1505937373, 0, 0, 0, 0, 16777216) +log = logging.getLogger(__name__) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class BTMPBeaconTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test case for salt.beacons.[s] + ''' + + def setup_loader_modules(self): + return { + btmp: { + '__context__': {'btmp.loc': 2}, + '__salt__': {}, + } + } + + def test_non_list_config(self): + config = {} + ret = btmp.validate(config) + + self.assertEqual(ret, (False, 'Configuration for btmp beacon must' + ' be a list.')) + + def test_empty_config(self): + config = [{}] + + ret = btmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + def test_no_match(self): + config = [{'users': {'gareth': {'time': {'end': '5pm', + 'start': '3pm'}}}} + ] + + ret = btmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + ret = btmp.beacon(config) + self.assertEqual(ret, []) + + def test_match(self): + with patch('salt.utils.files.fopen', + mock_open(read_data=raw)): + with patch('struct.unpack', + MagicMock(return_value=pack)): + config = [{'users': {'garet': {}}}] + + ret = btmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + _expected = [{'addr': 1505937373, + 'exit_status': 0, + 'inittab': '', + 'hostname': '::1', + 'PID': 29774, + 'session': 0, + 'user': + 'garet', + 'time': 0, + 'line': 'ssh:notty', + 'type': 6}] + ret = btmp.beacon(config) + self.assertEqual(ret, _expected) + + def test_match_time(self): + with patch('salt.utils.files.fopen', + mock_open(read_data=raw)): + with patch('time.time', + MagicMock(return_value=1506121200)): + with patch('struct.unpack', + MagicMock(return_value=pack)): + config = [{'users': {'garet': {'time': {'end': '5pm', + 'start': '3pm'}}}} + ] + + ret = btmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + _expected = [{'addr': 1505937373, + 'exit_status': 0, + 'inittab': '', + 'hostname': '::1', + 'PID': 29774, + 'session': 0, + 'user': + 'garet', + 'time': 0, + 'line': 'ssh:notty', + 'type': 6}] + ret = btmp.beacon(config) + self.assertEqual(ret, _expected) diff --git a/tests/unit/beacons/test_wtmp_beacon.py b/tests/unit/beacons/test_wtmp_beacon.py new file mode 100644 index 0000000000..b1edd97096 --- /dev/null +++ b/tests/unit/beacons/test_wtmp_beacon.py @@ -0,0 +1,119 @@ +# coding: utf-8 + +# Python libs +from __future__ import absolute_import +import logging +import sys + +# Salt testing libs +from tests.support.unit import skipIf, TestCase +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, mock_open +from tests.support.mixins import LoaderModuleMockMixin + +# Salt libs +import salt.beacons.wtmp as wtmp + +if sys.version_info >= (3,): + raw = bytes('\x07\x00\x00\x00H\x18\x00\x00pts/14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00s/14gareth\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13I\xc5YZf\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'utf-8') + pack = (7, 6216, b'pts/14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b's/14', b'gareth\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 0, 0, 0, 1506101523, 353882, 0, 0, 0, 16777216) +else: + raw = b'\x07\x00\x00\x00H\x18\x00\x00pts/14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00s/14gareth\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13I\xc5YZf\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + pack = (7, 6216, 'pts/14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 's/14', 'gareth\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', '::1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 0, 0, 0, 1506101523, 353882, 0, 0, 0, 16777216) + +log = logging.getLogger(__name__) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class WTMPBeaconTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test case for salt.beacons.[s] + ''' + + def setup_loader_modules(self): + return { + wtmp: { + '__context__': {'wtmp.loc': 2}, + '__salt__': {}, + } + } + + def test_non_list_config(self): + config = {} + ret = wtmp.validate(config) + + self.assertEqual(ret, (False, 'Configuration for wtmp beacon must' + ' be a list.')) + + def test_empty_config(self): + config = [{}] + + ret = wtmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + def test_no_match(self): + config = [{'users': {'gareth': {'time': {'end': '5pm', + 'start': '3pm'}}}} + ] + + ret = wtmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + ret = wtmp.beacon(config) + self.assertEqual(ret, []) + + def test_match(self): + with patch('salt.utils.files.fopen', + mock_open(read_data=raw)): + with patch('struct.unpack', + MagicMock(return_value=pack)): + config = [{'users': {'gareth': {}}}] + + ret = wtmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + _expected = [{'PID': 6216, + 'line': 'pts/14', + 'session': 0, + 'time': 0, + 'exit_status': 0, + 'inittab': 's/14', + 'type': 7, + 'addr': 1506101523, + 'hostname': '::1', + 'user': 'gareth'}] + + ret = wtmp.beacon(config) + log.debug('{}'.format(ret)) + self.assertEqual(ret, _expected) + + def test_match_time(self): + with patch('salt.utils.files.fopen', + mock_open(read_data=raw)): + with patch('time.time', + MagicMock(return_value=1506121200)): + with patch('struct.unpack', + MagicMock(return_value=pack)): + config = [{'users': {'gareth': {'time': {'end': '5pm', + 'start': '3pm'}}}} + ] + + ret = wtmp.validate(config) + + self.assertEqual(ret, (True, 'Valid beacon configuration')) + + _expected = [{'PID': 6216, + 'line': 'pts/14', + 'session': 0, + 'time': 0, + 'exit_status': 0, + 'inittab': 's/14', + 'type': 7, + 'addr': 1506101523, + 'hostname': '::1', + 'user': 'gareth'}] + + ret = wtmp.beacon(config) + self.assertEqual(ret, _expected) diff --git a/tests/unit/cloud/clouds/test_ec2.py b/tests/unit/cloud/clouds/test_ec2.py index 9ffd74d47b..4f77b14a1b 100644 --- a/tests/unit/cloud/clouds/test_ec2.py +++ b/tests/unit/cloud/clouds/test_ec2.py @@ -2,42 +2,39 @@ # Import Python libs from __future__ import absolute_import -import os -import tempfile # Import Salt Libs from salt.cloud.clouds import ec2 from salt.exceptions import SaltCloudSystemExit # Import Salt Testing Libs -from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import TestCase, skipIf -from tests.support.mock import NO_MOCK, NO_MOCK_REASON +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, PropertyMock @skipIf(NO_MOCK, NO_MOCK_REASON) -class EC2TestCase(TestCase, LoaderModuleMockMixin): +class EC2TestCase(TestCase): ''' Unit TestCase for salt.cloud.clouds.ec2 module. ''' - def setup_loader_modules(self): - return {ec2: {}} - def test__validate_key_path_and_mode(self): - with tempfile.NamedTemporaryFile() as f: - key_file = f.name - os.chmod(key_file, 0o644) - self.assertRaises(SaltCloudSystemExit, - ec2._validate_key_path_and_mode, - key_file) - os.chmod(key_file, 0o600) - self.assertTrue(ec2._validate_key_path_and_mode(key_file)) - os.chmod(key_file, 0o400) - self.assertTrue(ec2._validate_key_path_and_mode(key_file)) + # Key file exists + with patch('os.path.exists', return_value=True): + with patch('os.stat') as patched_stat: - # tmp file removed - self.assertRaises(SaltCloudSystemExit, - ec2._validate_key_path_and_mode, - key_file) + type(patched_stat.return_value).st_mode = PropertyMock(return_value=0o644) + self.assertRaises( + SaltCloudSystemExit, ec2._validate_key_path_and_mode, 'key_file') + + type(patched_stat.return_value).st_mode = PropertyMock(return_value=0o600) + self.assertTrue(ec2._validate_key_path_and_mode('key_file')) + + type(patched_stat.return_value).st_mode = PropertyMock(return_value=0o400) + self.assertTrue(ec2._validate_key_path_and_mode('key_file')) + + # Key file does not exist + with patch('os.path.exists', return_value=False): + self.assertRaises( + SaltCloudSystemExit, ec2._validate_key_path_and_mode, 'key_file') diff --git a/tests/unit/config/test_api.py b/tests/unit/config/test_api.py index a7c55b18f5..b7e2d5d7a7 100644 --- a/tests/unit/config/test_api.py +++ b/tests/unit/config/test_api.py @@ -19,17 +19,20 @@ from tests.support.mock import ( # Import Salt libs import salt.config import salt.utils.platform +import salt.syspaths MOCK_MASTER_DEFAULT_OPTS = { - 'log_file': '/var/log/salt/master', - 'pidfile': '/var/run/salt-master.pid', - 'root_dir': '/' + 'log_file': '{0}/var/log/salt/master'.format(salt.syspaths.ROOT_DIR), + 'pidfile': '{0}/var/run/salt-master.pid'.format(salt.syspaths.ROOT_DIR), + 'root_dir': format(salt.syspaths.ROOT_DIR) } if salt.utils.platform.is_windows(): MOCK_MASTER_DEFAULT_OPTS = { - 'log_file': 'c:\\salt\\var\\log\\salt\\master', - 'pidfile': 'c:\\salt\\var\\run\\salt-master.pid', - 'root_dir': 'c:\\salt' + 'log_file': '{0}\\var\\log\\salt\\master'.format( + salt.syspaths.ROOT_DIR), + 'pidfile': '{0}\\var\\run\\salt-master.pid'.format( + salt.syspaths.ROOT_DIR), + 'root_dir': format(salt.syspaths.ROOT_DIR) } @@ -54,9 +57,11 @@ class APIConfigTestCase(TestCase): ''' with patch('salt.config.client_config', MagicMock(return_value=MOCK_MASTER_DEFAULT_OPTS)): - expected = '/var/log/salt/api' + expected = '{0}/var/log/salt/api'.format( + salt.syspaths.ROOT_DIR if salt.syspaths.ROOT_DIR != '/' else '') if salt.utils.platform.is_windows(): - expected = 'c:\\salt\\var\\log\\salt\\api' + expected = '{0}\\var\\log\\salt\\api'.format( + salt.syspaths.ROOT_DIR) ret = salt.config.api_config('/some/fake/path') self.assertEqual(ret['log_file'], expected) @@ -69,9 +74,11 @@ class APIConfigTestCase(TestCase): ''' with patch('salt.config.client_config', MagicMock(return_value=MOCK_MASTER_DEFAULT_OPTS)): - expected = '/var/run/salt-api.pid' + expected = '{0}/var/run/salt-api.pid'.format( + salt.syspaths.ROOT_DIR if salt.syspaths.ROOT_DIR != '/' else '') if salt.utils.platform.is_windows(): - expected = 'c:\\salt\\var\\run\\salt-api.pid' + expected = '{0}\\var\\run\\salt-api.pid'.format( + salt.syspaths.ROOT_DIR) ret = salt.config.api_config('/some/fake/path') self.assertEqual(ret['pidfile'], expected) diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 878d3db4c9..948d2ee35e 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -448,6 +448,30 @@ class ConfigTestCase(TestCase, AdaptedConfigurationTestCaseMixin): if os.path.isdir(tempdir): shutil.rmtree(tempdir) + def test_master_id_function(self): + try: + tempdir = tempfile.mkdtemp(dir=TMP) + master_config = os.path.join(tempdir, 'master') + + with salt.utils.fopen(master_config, 'w') as fp_: + fp_.write( + 'id_function:\n' + ' test.echo:\n' + ' text: hello_world\n' + 'root_dir: {0}\n' + 'log_file: {1}\n'.format(tempdir, master_config) + ) + + # Let's load the configuration + config = sconfig.master_config(master_config) + + self.assertEqual(config['log_file'], master_config) + # 'master_config' appends '_master' to the ID + self.assertEqual(config['id'], 'hello_world_master') + finally: + if os.path.isdir(tempdir): + shutil.rmtree(tempdir) + def test_minion_file_roots_glob(self): # Config file and stub file_roots. fpath = tempfile.mktemp() @@ -508,6 +532,29 @@ class ConfigTestCase(TestCase, AdaptedConfigurationTestCaseMixin): if os.path.isdir(tempdir): shutil.rmtree(tempdir) + def test_minion_id_function(self): + try: + tempdir = tempfile.mkdtemp(dir=TMP) + minion_config = os.path.join(tempdir, 'minion') + + with salt.utils.fopen(minion_config, 'w') as fp_: + fp_.write( + 'id_function:\n' + ' test.echo:\n' + ' text: hello_world\n' + 'root_dir: {0}\n' + 'log_file: {1}\n'.format(tempdir, minion_config) + ) + + # Let's load the configuration + config = sconfig.minion_config(minion_config) + + self.assertEqual(config['log_file'], minion_config) + self.assertEqual(config['id'], 'hello_world') + finally: + if os.path.isdir(tempdir): + shutil.rmtree(tempdir) + def test_syndic_config(self): syndic_conf_path = self.get_config_file_path('syndic') minion_conf_path = self.get_config_file_path('minion') @@ -680,8 +727,8 @@ class ConfigTestCase(TestCase, AdaptedConfigurationTestCaseMixin): Tests passing in valid provider and profile config files successfully ''' providers = {'test-provider': - {'digital_ocean': - {'driver': 'digital_ocean', 'profiles': {}}}} + {'digitalocean': + {'driver': 'digitalocean', 'profiles': {}}}} overrides = {'test-profile': {'provider': 'test-provider', 'image': 'Ubuntu 12.10 x64', @@ -689,7 +736,7 @@ class ConfigTestCase(TestCase, AdaptedConfigurationTestCaseMixin): 'conf_file': PATH} ret = {'test-profile': {'profile': 'test-profile', - 'provider': 'test-provider:digital_ocean', + 'provider': 'test-provider:digitalocean', 'image': 'Ubuntu 12.10 x64', 'size': '512MB'}} self.assertEqual(sconfig.apply_vm_profiles_config(providers, diff --git a/tests/unit/daemons/__init__.py b/tests/unit/daemons/__init__.py new file mode 100644 index 0000000000..40a96afc6f --- /dev/null +++ b/tests/unit/daemons/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/unit/daemons/test_masterapi.py b/tests/unit/daemons/test_masterapi.py new file mode 100644 index 0000000000..29ea37ecd4 --- /dev/null +++ b/tests/unit/daemons/test_masterapi.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + +# Import Python libs +from __future__ import absolute_import + +# Import Salt libs +import salt.config +import salt.daemons.masterapi as masterapi + +# Import Salt Testing Libs +from tests.support.unit import TestCase +from tests.support.mock import ( + patch, + MagicMock, +) + + +class LocalFuncsTestCase(TestCase): + ''' + TestCase for salt.daemons.masterapi.LocalFuncs class + ''' + + def setUp(self): + opts = salt.config.master_config(None) + self.local_funcs = masterapi.LocalFuncs(opts, 'test-key') + + def test_runner_token_not_authenticated(self): + ''' + Asserts that a TokenAuthenticationError is returned when the token can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'TokenAuthenticationError', + u'message': u'Authentication failure of type "token" occurred.'}} + ret = self.local_funcs.runner({u'token': u'asdfasdfasdfasdf'}) + self.assertDictEqual(mock_ret, ret) + + def test_runner_token_authorization_error(self): + ''' + Asserts that a TokenAuthenticationError is returned when the token authenticates, but is + not authorized. + ''' + token = u'asdfasdfasdfasdf' + load = {u'token': token, u'fun': u'test.arg', u'kwarg': {}} + mock_token = {u'token': token, u'eauth': u'foo', u'name': u'test'} + mock_ret = {u'error': {u'name': u'TokenAuthenticationError', + u'message': u'Authentication failure of type "token" occurred ' + u'for user test.'}} + + with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.local_funcs.runner(load) + + self.assertDictEqual(mock_ret, ret) + + def test_runner_token_salt_invocation_error(self): + ''' + Asserts that a SaltInvocationError is returned when the token authenticates, but the + command is malformed. + ''' + token = u'asdfasdfasdfasdf' + load = {u'token': token, u'fun': u'badtestarg', u'kwarg': {}} + mock_token = {u'token': token, u'eauth': u'foo', u'name': u'test'} + mock_ret = {u'error': {u'name': u'SaltInvocationError', + u'message': u'A command invocation error occurred: Check syntax.'}} + + with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.local_funcs.runner(load) + + self.assertDictEqual(mock_ret, ret) + + def test_runner_eauth_not_authenticated(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'EauthAuthenticationError', + u'message': u'Authentication failure of type "eauth" occurred for ' + u'user UNKNOWN.'}} + ret = self.local_funcs.runner({u'eauth': u'foo'}) + self.assertDictEqual(mock_ret, ret) + + def test_runner_eauth_authorization_error(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user authenticates, but is + not authorized. + ''' + load = {u'eauth': u'foo', u'username': u'test', u'fun': u'test.arg', u'kwarg': {}} + mock_ret = {u'error': {u'name': u'EauthAuthenticationError', + u'message': u'Authentication failure of type "eauth" occurred for ' + u'user test.'}} + with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.local_funcs.runner(load) + + self.assertDictEqual(mock_ret, ret) + + def test_runner_eauth_salt_invocation_errpr(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user authenticates, but the + command is malformed. + ''' + load = {u'eauth': u'foo', u'username': u'test', u'fun': u'bad.test.arg.func', u'kwarg': {}} + mock_ret = {u'error': {u'name': u'SaltInvocationError', + u'message': u'A command invocation error occurred: Check syntax.'}} + with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.local_funcs.runner(load) + + self.assertDictEqual(mock_ret, ret) + + def test_wheel_token_not_authenticated(self): + ''' + Asserts that a TokenAuthenticationError is returned when the token can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'TokenAuthenticationError', + u'message': u'Authentication failure of type "token" occurred.'}} + ret = self.local_funcs.wheel({u'token': u'asdfasdfasdfasdf'}) + self.assertDictEqual(mock_ret, ret) + + def test_wheel_token_authorization_error(self): + ''' + Asserts that a TokenAuthenticationError is returned when the token authenticates, but is + not authorized. + ''' + token = u'asdfasdfasdfasdf' + load = {u'token': token, u'fun': u'test.arg', u'kwarg': {}} + mock_token = {u'token': token, u'eauth': u'foo', u'name': u'test'} + mock_ret = {u'error': {u'name': u'TokenAuthenticationError', + u'message': u'Authentication failure of type "token" occurred ' + u'for user test.'}} + + with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.local_funcs.wheel(load) + + self.assertDictEqual(mock_ret, ret) + + def test_wheel_token_salt_invocation_error(self): + ''' + Asserts that a SaltInvocationError is returned when the token authenticates, but the + command is malformed. + ''' + token = u'asdfasdfasdfasdf' + load = {u'token': token, u'fun': u'badtestarg', u'kwarg': {}} + mock_token = {u'token': token, u'eauth': u'foo', u'name': u'test'} + mock_ret = {u'error': {u'name': u'SaltInvocationError', + u'message': u'A command invocation error occurred: Check syntax.'}} + + with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.local_funcs.wheel(load) + + self.assertDictEqual(mock_ret, ret) + + def test_wheel_eauth_not_authenticated(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'EauthAuthenticationError', + u'message': u'Authentication failure of type "eauth" occurred for ' + u'user UNKNOWN.'}} + ret = self.local_funcs.wheel({u'eauth': u'foo'}) + self.assertDictEqual(mock_ret, ret) + + def test_wheel_eauth_authorization_error(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user authenticates, but is + not authorized. + ''' + load = {u'eauth': u'foo', u'username': u'test', u'fun': u'test.arg', u'kwarg': {}} + mock_ret = {u'error': {u'name': u'EauthAuthenticationError', + u'message': u'Authentication failure of type "eauth" occurred for ' + u'user test.'}} + with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.local_funcs.wheel(load) + + self.assertDictEqual(mock_ret, ret) + + def test_wheel_eauth_salt_invocation_errpr(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user authenticates, but the + command is malformed. + ''' + load = {u'eauth': u'foo', u'username': u'test', u'fun': u'bad.test.arg.func', u'kwarg': {}} + mock_ret = {u'error': {u'name': u'SaltInvocationError', + u'message': u'A command invocation error occurred: Check syntax.'}} + with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.local_funcs.wheel(load) + + self.assertDictEqual(mock_ret, ret) + + def test_wheel_user_not_authenticated(self): + ''' + Asserts that an UserAuthenticationError is returned when the user can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'UserAuthenticationError', + u'message': u'Authentication failure of type "user" occurred for ' + u'user UNKNOWN.'}} + ret = self.local_funcs.wheel({}) + self.assertDictEqual(mock_ret, ret) diff --git a/tests/unit/fileserver/test_gitfs.py b/tests/unit/fileserver/test_gitfs.py index affa22eb05..64d8ca5284 100644 --- a/tests/unit/fileserver/test_gitfs.py +++ b/tests/unit/fileserver/test_gitfs.py @@ -9,8 +9,12 @@ import os import shutil import tempfile import textwrap -import pwd import logging +import stat +try: + import pwd +except ImportError: + pass # Import 3rd-party libs import yaml @@ -29,8 +33,10 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch from tests.support.paths import TMP, FILES # Import salt libs -import salt.utils.gitfs import salt.fileserver.gitfs as gitfs +import salt.utils.gitfs +import salt.utils.platform +import salt.utils.win_functions log = logging.getLogger(__name__) @@ -210,7 +216,6 @@ class GitFSTest(TestCase, LoaderModuleMockMixin): self.integration_base_files = os.path.join(FILES, 'file', 'base') # Create the dir if it doesn't already exist - try: shutil.copytree(self.integration_base_files, self.tmp_repo_dir + '/') except OSError: @@ -224,7 +229,10 @@ class GitFSTest(TestCase, LoaderModuleMockMixin): if 'USERNAME' not in os.environ: try: - os.environ['USERNAME'] = pwd.getpwuid(os.geteuid()).pw_name + if salt.utils.platform.is_windows(): + os.environ['USERNAME'] = salt.utils.win_functions.get_current_user() + else: + os.environ['USERNAME'] = pwd.getpwuid(os.geteuid()).pw_name except AttributeError: log.error('Unable to get effective username, falling back to ' '\'root\'.') @@ -240,14 +248,18 @@ class GitFSTest(TestCase, LoaderModuleMockMixin): Remove the temporary git repository and gitfs cache directory to ensure a clean environment for each test. ''' - shutil.rmtree(self.tmp_repo_dir) - shutil.rmtree(self.tmp_cachedir) - shutil.rmtree(self.tmp_sock_dir) + shutil.rmtree(self.tmp_repo_dir, onerror=self._rmtree_error) + shutil.rmtree(self.tmp_cachedir, onerror=self._rmtree_error) + shutil.rmtree(self.tmp_sock_dir, onerror=self._rmtree_error) del self.tmp_repo_dir del self.tmp_cachedir del self.tmp_sock_dir del self.integration_base_files + def _rmtree_error(self, func, path, excinfo): + os.chmod(path, stat.S_IWRITE) + func(path) + def test_file_list(self): ret = gitfs.file_list(LOAD) self.assertIn('testfile', ret) diff --git a/tests/unit/modules/test_alternatives.py b/tests/unit/modules/test_alternatives.py index b018eafa80..8dd0cde28d 100644 --- a/tests/unit/modules/test_alternatives.py +++ b/tests/unit/modules/test_alternatives.py @@ -66,30 +66,28 @@ class AlternativesTestCase(TestCase, LoaderModuleMockMixin): ) def test_show_current(self): - with patch('os.readlink') as os_readlink_mock: - os_readlink_mock.return_value = '/etc/alternatives/salt' + mock = MagicMock(return_value='/etc/alternatives/salt') + with patch('salt.utils.path.readlink', mock): ret = alternatives.show_current('better-world') self.assertEqual('/etc/alternatives/salt', ret) - os_readlink_mock.assert_called_once_with( - '/etc/alternatives/better-world' - ) + mock.assert_called_once_with('/etc/alternatives/better-world') with TestsLoggingHandler() as handler: - os_readlink_mock.side_effect = OSError('Hell was not found!!!') + mock.side_effect = OSError('Hell was not found!!!') self.assertFalse(alternatives.show_current('hell')) - os_readlink_mock.assert_called_with('/etc/alternatives/hell') + mock.assert_called_with('/etc/alternatives/hell') self.assertIn('ERROR:alternative: hell does not exist', handler.messages) def test_check_installed(self): - with patch('os.readlink') as os_readlink_mock: - os_readlink_mock.return_value = '/etc/alternatives/salt' + mock = MagicMock(return_value='/etc/alternatives/salt') + with patch('salt.utils.path.readlink', mock): self.assertTrue( alternatives.check_installed( 'better-world', '/etc/alternatives/salt' ) ) - os_readlink_mock.return_value = False + mock.return_value = False self.assertFalse( alternatives.check_installed( 'help', '/etc/alternatives/salt' diff --git a/tests/unit/modules/test_beacons.py b/tests/unit/modules/test_beacons.py index 708221d638..6706866bb9 100644 --- a/tests/unit/modules/test_beacons.py +++ b/tests/unit/modules/test_beacons.py @@ -59,6 +59,10 @@ class BeaconsTestCase(TestCase, LoaderModuleMockMixin): event_returns = [{'complete': True, 'tag': '/salt/minion/minion_beacons_list_complete', 'beacons': {}}, + {'complete': True, + 'valid': True, + 'vcomment': '', + 'tag': '/salt/minion/minion_beacons_list_complete'}, {'complete': True, 'tag': '/salt/minion/minion_beacon_add_complete', 'beacons': {'ps': [{'processes': {'salt-master': 'stopped', 'apache2': 'stopped'}}]}}] diff --git a/tests/unit/modules/test_chef.py b/tests/unit/modules/test_chef.py index 320093b7ce..46d0aae354 100644 --- a/tests/unit/modules/test_chef.py +++ b/tests/unit/modules/test_chef.py @@ -36,7 +36,8 @@ class ChefTestCase(TestCase, LoaderModuleMockMixin): ''' Test if it execute a chef client run and return a dict ''' - self.assertDictEqual(chef.client(), {}) + with patch.dict(chef.__opts__, {'cachedir': r'c:\salt\var\cache\salt\minion'}): + self.assertDictEqual(chef.client(), {}) # 'solo' function tests: 1 @@ -44,4 +45,5 @@ class ChefTestCase(TestCase, LoaderModuleMockMixin): ''' Test if it execute a chef solo run and return a dict ''' - self.assertDictEqual(chef.solo('/dev/sda1'), {}) + with patch.dict(chef.__opts__, {'cachedir': r'c:\salt\var\cache\salt\minion'}): + self.assertDictEqual(chef.solo('/dev/sda1'), {}) diff --git a/tests/unit/modules/test_dockermod.py b/tests/unit/modules/test_dockermod.py index 00fad0c3c5..e43d7011e4 100644 --- a/tests/unit/modules/test_dockermod.py +++ b/tests/unit/modules/test_dockermod.py @@ -18,6 +18,8 @@ from tests.support.mock import ( ) # Import Salt Libs +import salt.config +import salt.loader from salt.ext.six.moves import range from salt.exceptions import CommandExecutionError import salt.modules.dockermod as docker_mod @@ -39,7 +41,12 @@ class DockerTestCase(TestCase, LoaderModuleMockMixin): Validate docker module ''' def setup_loader_modules(self): - return {docker_mod: {'__context__': {'docker.docker_version': ''}}} + utils = salt.loader.utils( + salt.config.DEFAULT_MINION_OPTS, + whitelist=['state'] + ) + return {docker_mod: {'__context__': {'docker.docker_version': ''}, + '__utils__': utils}} try: docker_version = docker_mod.docker.version_info @@ -704,9 +711,9 @@ class DockerTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual({"retcode": 0, "comment": "container cmd"}, ret) def test_images_with_empty_tags(self): - """ + ''' docker 1.12 reports also images without tags with `null`. - """ + ''' client = Mock() client.api_version = '1.24' client.images = Mock( @@ -749,3 +756,24 @@ class DockerTestCase(TestCase, LoaderModuleMockMixin): with patch.object(docker_mod, 'inspect_image', inspect_image_mock): ret = docker_mod.compare_container('container1', 'container2') self.assertEqual(ret, {}) + + def test_resolve_tag(self): + ''' + Test the resolve_tag function + ''' + with_prefix = 'docker.io/foo:latest' + no_prefix = 'bar:latest' + with patch.object(docker_mod, + 'list_tags', + MagicMock(return_value=[with_prefix])): + self.assertEqual(docker_mod.resolve_tag('foo'), with_prefix) + self.assertEqual(docker_mod.resolve_tag('foo:latest'), with_prefix) + self.assertEqual(docker_mod.resolve_tag(with_prefix), with_prefix) + self.assertEqual(docker_mod.resolve_tag('foo:bar'), False) + + with patch.object(docker_mod, + 'list_tags', + MagicMock(return_value=[no_prefix])): + self.assertEqual(docker_mod.resolve_tag('bar'), no_prefix) + self.assertEqual(docker_mod.resolve_tag(no_prefix), no_prefix) + self.assertEqual(docker_mod.resolve_tag('bar:baz'), False) diff --git a/tests/unit/modules/test_esxcluster.py b/tests/unit/modules/test_esxcluster.py new file mode 100644 index 0000000000..5a32980d16 --- /dev/null +++ b/tests/unit/modules/test_esxcluster.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu ` + + Tests for functions in salt.modules.esxcluster +''' + +# Import Python Libs +from __future__ import absolute_import + +# Import Salt Libs +import salt.modules.esxcluster as esxcluster + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class GetDetailsTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.modules.esxcluster.get_details''' + def setup_loader_modules(self): + return {esxcluster: {'__virtual__': + MagicMock(return_value='esxcluster'), + '__proxy__': {}}} + + def test_get_details(self): + mock_get_details = MagicMock() + with patch.dict(esxcluster.__proxy__, + {'esxcluster.get_details': mock_get_details}): + esxcluster.get_details() + mock_get_details.assert_called_once_with() diff --git a/tests/unit/modules/test_file.py b/tests/unit/modules/test_file.py index eb440350c1..e4d74f1266 100644 --- a/tests/unit/modules/test_file.py +++ b/tests/unit/modules/test_file.py @@ -14,6 +14,8 @@ from tests.support.unit import TestCase from tests.support.mock import MagicMock, patch # Import Salt libs +import salt.config +import salt.loader import salt.utils.files import salt.modules.file as filemod import salt.modules.config as configmod @@ -45,7 +47,8 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): 'cachedir': 'tmp', 'grains': {}, }, - '__grains__': {'kernel': 'Linux'} + '__grains__': {'kernel': 'Linux'}, + '__utils__': {'files.is_text_file': MagicMock(return_value=True)}, } } @@ -203,7 +206,8 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): 'cachedir': 'tmp', 'grains': {}, }, - '__grains__': {'kernel': 'Linux'} + '__grains__': {'kernel': 'Linux'}, + '__utils__': {'files.is_text_file': MagicMock(return_value=True)}, } } diff --git a/tests/unit/modules/test_gem.py b/tests/unit/modules/test_gem.py index 14e38da893..23221ba646 100644 --- a/tests/unit/modules/test_gem.py +++ b/tests/unit/modules/test_gem.py @@ -65,7 +65,8 @@ class TestGemModule(TestCase, LoaderModuleMockMixin): with patch.dict(gem.__salt__, {'rvm.is_installed': MagicMock(return_value=False), 'rbenv.is_installed': MagicMock(return_value=True), - 'rbenv.do': mock}): + 'rbenv.do': mock}),\ + patch('salt.utils.platform.is_windows', return_value=False): gem._gem(['install', 'rails']) mock.assert_called_once_with( ['gem', 'install', 'rails'], diff --git a/tests/unit/modules/test_genesis.py b/tests/unit/modules/test_genesis.py index 3c7bd7abfe..ae9f3e7b16 100644 --- a/tests/unit/modules/test_genesis.py +++ b/tests/unit/modules/test_genesis.py @@ -46,12 +46,59 @@ class GenesisTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(genesis.__salt__, {'disk.blkid': MagicMock(return_value={})}): self.assertEqual(genesis.bootstrap('rpm', 'root', 'dir'), None) - with patch.object(genesis, '_bootstrap_deb', return_value='A'): + common_parms = {'platform': 'deb', + 'root': 'root', + 'img_format': 'dir', + 'arch': 'amd64', + 'flavor': 'stable', + 'static_qemu': 'qemu'} + + param_sets = [ + + {'params': {}, + 'cmd': ['debootstrap', '--foreign', '--arch', 'amd64', + 'stable', 'root', 'http://ftp.debian.org/debian/'] + }, + + {'params': {'pkgs': 'vim'}, + 'cmd': ['debootstrap', '--foreign', '--arch', 'amd64', + '--include', 'vim', + 'stable', 'root', 'http://ftp.debian.org/debian/'] + }, + + {'params': {'pkgs': 'vim,emacs'}, + 'cmd': ['debootstrap', '--foreign', '--arch', 'amd64', + '--include', 'vim,emacs', + 'stable', 'root', 'http://ftp.debian.org/debian/'] + }, + + {'params': {'pkgs': ['vim', 'emacs']}, + 'cmd': ['debootstrap', '--foreign', '--arch', 'amd64', + '--include', 'vim,emacs', + 'stable', 'root', 'http://ftp.debian.org/debian/'] + }, + + {'params': {'pkgs': ['vim', 'emacs'], 'exclude_pkgs': ['vim', 'foo']}, + 'cmd': ['debootstrap', '--foreign', '--arch', 'amd64', + '--include', 'vim,emacs', '--exclude', 'vim,foo', + 'stable', 'root', 'http://ftp.debian.org/debian/'] + }, + + ] + + for param_set in param_sets: + with patch.dict(genesis.__salt__, {'mount.umount': MagicMock(), 'file.rmdir': MagicMock(), - 'file.directory_exists': MagicMock()}): - with patch.dict(genesis.__salt__, {'disk.blkid': MagicMock(return_value={})}): - self.assertEqual(genesis.bootstrap('deb', 'root', 'dir'), None) + 'file.directory_exists': MagicMock(), + 'cmd.run': MagicMock(), + 'disk.blkid': MagicMock(return_value={})}): + with patch('salt.modules.genesis.salt.utils.path.which', return_value=True): + with patch('salt.modules.genesis.salt.utils.validate.path.is_executable', + return_value=True): + param_set['params'].update(common_parms) + self.assertEqual(genesis.bootstrap(**param_set['params']), None) + genesis.__salt__['cmd.run'].assert_any_call(param_set['cmd'], python_shell=False) with patch.object(genesis, '_bootstrap_pacman', return_value='A') as pacman_patch: with patch.dict(genesis.__salt__, {'mount.umount': MagicMock(), diff --git a/tests/unit/modules/test_groupadd.py b/tests/unit/modules/test_groupadd.py index b836bd8805..a0646556ea 100644 --- a/tests/unit/modules/test_groupadd.py +++ b/tests/unit/modules/test_groupadd.py @@ -5,7 +5,10 @@ # Import Python libs from __future__ import absolute_import -import grp +try: + import grp +except ImportError: + pass # Import Salt Testing Libs from tests.support.mixins import LoaderModuleMockMixin @@ -14,9 +17,11 @@ from tests.support.mock import MagicMock, patch, NO_MOCK, NO_MOCK_REASON # Import Salt Libs import salt.modules.groupadd as groupadd +import salt.utils.platform @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(salt.utils.platform.is_windows(), "Module not available on Windows") class GroupAddTestCase(TestCase, LoaderModuleMockMixin): ''' TestCase for salt.modules.groupadd @@ -113,16 +118,16 @@ class GroupAddTestCase(TestCase, LoaderModuleMockMixin): ''' os_version_list = [ {'grains': {'kernel': 'Linux', 'os_family': 'RedHat', 'osmajorrelease': '5'}, - 'cmd': ('gpasswd', '-a', 'root', 'test')}, + 'cmd': ['gpasswd', '-a', 'root', 'test']}, {'grains': {'kernel': 'Linux', 'os_family': 'Suse', 'osmajorrelease': '11'}, - 'cmd': ('usermod', '-A', 'test', 'root')}, + 'cmd': ['usermod', '-A', 'test', 'root']}, {'grains': {'kernel': 'Linux'}, - 'cmd': ('gpasswd', '--add', 'root', 'test')}, + 'cmd': ['gpasswd', '--add', 'root', 'test']}, {'grains': {'kernel': 'OTHERKERNEL'}, - 'cmd': ('usermod', '-G', 'test', 'root')}, + 'cmd': ['usermod', '-G', 'test', 'root']}, ] for os_version in os_version_list: @@ -140,16 +145,16 @@ class GroupAddTestCase(TestCase, LoaderModuleMockMixin): ''' os_version_list = [ {'grains': {'kernel': 'Linux', 'os_family': 'RedHat', 'osmajorrelease': '5'}, - 'cmd': ('gpasswd', '-d', 'root', 'test')}, + 'cmd': ['gpasswd', '-d', 'root', 'test']}, {'grains': {'kernel': 'Linux', 'os_family': 'Suse', 'osmajorrelease': '11'}, - 'cmd': ('usermod', '-R', 'test', 'root')}, + 'cmd': ['usermod', '-R', 'test', 'root']}, {'grains': {'kernel': 'Linux'}, - 'cmd': ('gpasswd', '--del', 'root', 'test')}, + 'cmd': ['gpasswd', '--del', 'root', 'test']}, {'grains': {'kernel': 'OpenBSD'}, - 'cmd': 'usermod -S foo root'}, + 'cmd': ['usermod', '-S', 'foo', 'root']}, ] for os_version in os_version_list: @@ -175,16 +180,16 @@ class GroupAddTestCase(TestCase, LoaderModuleMockMixin): ''' os_version_list = [ {'grains': {'kernel': 'Linux', 'os_family': 'RedHat', 'osmajorrelease': '5'}, - 'cmd': ('gpasswd', '-M', 'foo', 'test')}, + 'cmd': ['gpasswd', '-M', 'foo', 'test']}, {'grains': {'kernel': 'Linux', 'os_family': 'Suse', 'osmajorrelease': '11'}, - 'cmd': ('groupmod', '-A', 'foo', 'test')}, + 'cmd': ['groupmod', '-A', 'foo', 'test']}, {'grains': {'kernel': 'Linux'}, - 'cmd': ('gpasswd', '--members', 'foo', 'test')}, + 'cmd': ['gpasswd', '--members', 'foo', 'test']}, {'grains': {'kernel': 'OpenBSD'}, - 'cmd': 'usermod -G test foo'}, + 'cmd': ['usermod', '-G', 'test', 'foo']}, ] for os_version in os_version_list: diff --git a/tests/unit/modules/test_hosts.py b/tests/unit/modules/test_hosts.py index c7b1b4e988..56f01f56ab 100644 --- a/tests/unit/modules/test_hosts.py +++ b/tests/unit/modules/test_hosts.py @@ -16,6 +16,7 @@ from tests.support.mock import ( ) # Import Salt Libs import salt.modules.hosts as hosts +import salt.utils from salt.ext.six.moves import StringIO @@ -92,8 +93,12 @@ class HostsTestCase(TestCase, LoaderModuleMockMixin): ''' Tests true if the alias is set ''' + hosts_file = '/etc/hosts' + if salt.utils.is_windows(): + hosts_file = r'C:\Windows\System32\Drivers\etc\hosts' + with patch('salt.modules.hosts.__get_hosts_filename', - MagicMock(return_value='/etc/hosts')), \ + MagicMock(return_value=hosts_file)), \ patch('os.path.isfile', MagicMock(return_value=False)), \ patch.dict(hosts.__salt__, {'config.option': MagicMock(return_value=None)}): @@ -139,7 +144,16 @@ class HostsTestCase(TestCase, LoaderModuleMockMixin): self.close() def close(self): - data[0] = self.getvalue() + # Don't save unless there's something there. In Windows + # the class gets initialized the first time with mode = w + # which sets the initial value to ''. When the class closes + # it clears out data and causes the test to fail. + # I don't know why it get's initialized with a mode of 'w' + # For the purposes of this test data shouldn't be empty + # This is a problem with this class and not with the hosts + # module + if self.getvalue(): + data[0] = self.getvalue() StringIO.close(self) expected = '\n'.join(( @@ -151,6 +165,7 @@ class HostsTestCase(TestCase, LoaderModuleMockMixin): mock_opt = MagicMock(return_value=None) with patch.dict(hosts.__salt__, {'config.option': mock_opt}): self.assertTrue(hosts.set_host('1.1.1.1', ' ')) + self.assertEqual(data[0], expected) # 'rm_host' function tests: 2 @@ -182,9 +197,13 @@ class HostsTestCase(TestCase, LoaderModuleMockMixin): ''' Tests if specified host entry gets added from the hosts file ''' + hosts_file = '/etc/hosts' + if salt.utils.is_windows(): + hosts_file = r'C:\Windows\System32\Drivers\etc\hosts' + with patch('salt.utils.files.fopen', mock_open()), \ patch('salt.modules.hosts.__get_hosts_filename', - MagicMock(return_value='/etc/hosts')): + MagicMock(return_value=hosts_file)): mock_opt = MagicMock(return_value=None) with patch.dict(hosts.__salt__, {'config.option': mock_opt}): self.assertTrue(hosts.add_host('10.10.10.10', 'Salt1')) diff --git a/tests/unit/modules/test_ini_manage.py b/tests/unit/modules/test_ini_manage.py index 9e41ecae5a..f3550262a9 100644 --- a/tests/unit/modules/test_ini_manage.py +++ b/tests/unit/modules/test_ini_manage.py @@ -15,38 +15,38 @@ import salt.modules.ini_manage as ini class IniManageTestCase(TestCase): - TEST_FILE_CONTENT = '''\ -# Comment on the first line - -# First main option -option1=main1 - -# Second main option -option2=main2 - - -[main] -# Another comment -test1=value 1 - -test2=value 2 - -[SectionB] -test1=value 1B - -# Blank line should be above -test3 = value 3B - -[SectionC] -# The following option is empty -empty_option= -''' + TEST_FILE_CONTENT = os.linesep.join([ + '# Comment on the first line', + '', + '# First main option', + 'option1=main1', + '', + '# Second main option', + 'option2=main2', + '', + '', + '[main]', + '# Another comment', + 'test1=value 1', + '', + 'test2=value 2', + '', + '[SectionB]', + 'test1=value 1B', + '', + '# Blank line should be above', + 'test3 = value 3B', + '', + '[SectionC]', + '# The following option is empty', + 'empty_option=' + ]) maxDiff = None def setUp(self): - self.tfile = tempfile.NamedTemporaryFile(delete=False, mode='w+') - self.tfile.write(self.TEST_FILE_CONTENT) + self.tfile = tempfile.NamedTemporaryFile(delete=False, mode='w+b') + self.tfile.write(salt.utils.to_bytes(self.TEST_FILE_CONTENT)) self.tfile.close() def tearDown(self): @@ -121,40 +121,42 @@ empty_option= }) with salt.utils.files.fopen(self.tfile.name, 'r') as fp: file_content = fp.read() - self.assertIn('\nempty_option = \n', file_content, - 'empty_option was not preserved') + expected = '{0}{1}{0}'.format(os.linesep, 'empty_option = ') + self.assertIn(expected, file_content, 'empty_option was not preserved') def test_empty_lines_preserved_after_edit(self): ini.set_option(self.tfile.name, { 'SectionB': {'test3': 'new value 3B'}, }) + expected = os.linesep.join([ + '# Comment on the first line', + '', + '# First main option', + 'option1 = main1', + '', + '# Second main option', + 'option2 = main2', + '', + '[main]', + '# Another comment', + 'test1 = value 1', + '', + 'test2 = value 2', + '', + '[SectionB]', + 'test1 = value 1B', + '', + '# Blank line should be above', + 'test3 = new value 3B', + '', + '[SectionC]', + '# The following option is empty', + 'empty_option = ', + '' + ]) with salt.utils.files.fopen(self.tfile.name, 'r') as fp: file_content = fp.read() - self.assertEqual('''\ -# Comment on the first line - -# First main option -option1 = main1 - -# Second main option -option2 = main2 - -[main] -# Another comment -test1 = value 1 - -test2 = value 2 - -[SectionB] -test1 = value 1B - -# Blank line should be above -test3 = new value 3B - -[SectionC] -# The following option is empty -empty_option = -''', file_content) + self.assertEqual(expected, file_content) def test_empty_lines_preserved_after_multiple_edits(self): ini.set_option(self.tfile.name, { diff --git a/tests/unit/modules/test_inspect_collector.py b/tests/unit/modules/test_inspect_collector.py index cdcb689eb7..0d37519a9e 100644 --- a/tests/unit/modules/test_inspect_collector.py +++ b/tests/unit/modules/test_inspect_collector.py @@ -49,9 +49,15 @@ class InspectorCollectorTestCase(TestCase): :return: ''' - inspector = Inspector(cachedir='/foo/cache', piddir='/foo/pid', pidfilename='bar.pid') - self.assertEqual(inspector.dbfile, '/foo/cache/_minion_collector.db') - self.assertEqual(inspector.pidfile, '/foo/pid/bar.pid') + cachedir = os.sep + os.sep.join(['foo', 'cache']) + piddir = os.sep + os.sep.join(['foo', 'pid']) + inspector = Inspector(cachedir=cachedir, piddir=piddir, pidfilename='bar.pid') + self.assertEqual( + inspector.dbfile, + os.sep + os.sep.join(['foo', 'cache', '_minion_collector.db'])) + self.assertEqual( + inspector.pidfile, + os.sep + os.sep.join(['foo', 'pid', 'bar.pid'])) def test_file_tree(self): ''' @@ -60,12 +66,29 @@ class InspectorCollectorTestCase(TestCase): :return: ''' - inspector = Inspector(cachedir='/test', piddir='/test', pidfilename='bar.pid') + inspector = Inspector(cachedir=os.sep + 'test', + piddir=os.sep + 'test', + pidfilename='bar.pid') tree_root = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'inspectlib', 'tree_test') - expected_tree = (['/a/a/dummy.a', '/a/b/dummy.b', '/b/b.1', '/b/b.2', '/b/b.3'], - ['/a', '/a/a', '/a/b', '/a/c', '/b', '/c'], - ['/a/a/dummy.ln.a', '/a/b/dummy.ln.b', '/a/c/b.1', '/b/b.4', - '/b/b.5', '/c/b.1', '/c/b.2', '/c/b.3']) + expected_tree = ([os.sep + os.sep.join(['a', 'a', 'dummy.a']), + os.sep + os.sep.join(['a', 'b', 'dummy.b']), + os.sep + os.sep.join(['b', 'b.1']), + os.sep + os.sep.join(['b', 'b.2']), + os.sep + os.sep.join(['b', 'b.3'])], + [os.sep + 'a', + os.sep + os.sep.join(['a', 'a']), + os.sep + os.sep.join(['a', 'b']), + os.sep + os.sep.join(['a', 'c']), + os.sep + 'b', + os.sep + 'c'], + [os.sep + os.sep.join(['a', 'a', 'dummy.ln.a']), + os.sep + os.sep.join(['a', 'b', 'dummy.ln.b']), + os.sep + os.sep.join(['a', 'c', 'b.1']), + os.sep + os.sep.join(['b', 'b.4']), + os.sep + os.sep.join(['b', 'b.5']), + os.sep + os.sep.join(['c', 'b.1']), + os.sep + os.sep.join(['c', 'b.2']), + os.sep + os.sep.join(['c', 'b.3'])]) tree_result = [] for chunk in inspector._get_all_files(tree_root): buff = [] diff --git a/tests/unit/modules/test_kubernetes.py b/tests/unit/modules/test_kubernetes.py new file mode 100644 index 0000000000..e3d0ef73d8 --- /dev/null +++ b/tests/unit/modules/test_kubernetes.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Jochen Breuer ` +''' + +# Import Python Libs +from __future__ import absolute_import + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + Mock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +try: + from salt.modules import kubernetes +except ImportError: + kubernetes = False +if not kubernetes.HAS_LIBS: + kubernetes = False + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(kubernetes is False, "Probably Kubernetes client lib is not installed. \ + Skipping test_kubernetes.py") +class KubernetesTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.modules.kubernetes + ''' + + def setup_loader_modules(self): + return { + kubernetes: { + '__salt__': {}, + } + } + + def test_nodes(self): + ''' + Test node listing. + :return: + ''' + with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib: + with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}): + mock_kubernetes_lib.client.CoreV1Api.return_value = Mock( + **{"list_node.return_value.to_dict.return_value": + {'items': [{'metadata': {'name': 'mock_node_name'}}]}} + ) + self.assertEqual(kubernetes.nodes(), ['mock_node_name']) + self.assertTrue(kubernetes.kubernetes.client.CoreV1Api().list_node().to_dict.called) + + def test_deployments(self): + ''' + Tests deployment listing. + :return: + ''' + with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib: + with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}): + mock_kubernetes_lib.client.ExtensionsV1beta1Api.return_value = Mock( + **{"list_namespaced_deployment.return_value.to_dict.return_value": + {'items': [{'metadata': {'name': 'mock_deployment_name'}}]}} + ) + self.assertEqual(kubernetes.deployments(), ['mock_deployment_name']) + self.assertTrue( + kubernetes.kubernetes.client.ExtensionsV1beta1Api().list_namespaced_deployment().to_dict.called) + + def test_services(self): + ''' + Tests services listing. + :return: + ''' + with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib: + with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}): + mock_kubernetes_lib.client.CoreV1Api.return_value = Mock( + **{"list_namespaced_service.return_value.to_dict.return_value": + {'items': [{'metadata': {'name': 'mock_service_name'}}]}} + ) + self.assertEqual(kubernetes.services(), ['mock_service_name']) + self.assertTrue(kubernetes.kubernetes.client.CoreV1Api().list_namespaced_service().to_dict.called) + + def test_pods(self): + ''' + Tests pods listing. + :return: + ''' + with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib: + with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}): + mock_kubernetes_lib.client.CoreV1Api.return_value = Mock( + **{"list_namespaced_pod.return_value.to_dict.return_value": + {'items': [{'metadata': {'name': 'mock_pod_name'}}]}} + ) + self.assertEqual(kubernetes.pods(), ['mock_pod_name']) + self.assertTrue(kubernetes.kubernetes.client.CoreV1Api(). + list_namespaced_pod().to_dict.called) + + def test_delete_deployments(self): + ''' + Tests deployment creation. + :return: + ''' + with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib: + with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}): + mock_kubernetes_lib.client.V1DeleteOptions = Mock(return_value="") + mock_kubernetes_lib.client.ExtensionsV1beta1Api.return_value = Mock( + **{"delete_namespaced_deployment.return_value.to_dict.return_value": {'code': 200}} + ) + self.assertEqual(kubernetes.delete_deployment("test"), {'code': 200}) + self.assertTrue( + kubernetes.kubernetes.client.ExtensionsV1beta1Api(). + delete_namespaced_deployment().to_dict.called) + + def test_create_deployments(self): + ''' + Tests deployment creation. + :return: + ''' + with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib: + with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}): + mock_kubernetes_lib.client.ExtensionsV1beta1Api.return_value = Mock( + **{"create_namespaced_deployment.return_value.to_dict.return_value": {}} + ) + self.assertEqual(kubernetes.create_deployment("test", "default", {}, {}, + None, None, None), {}) + self.assertTrue( + kubernetes.kubernetes.client.ExtensionsV1beta1Api(). + create_namespaced_deployment().to_dict.called) diff --git a/tests/unit/modules/test_mac_group.py b/tests/unit/modules/test_mac_group.py index 2c03deb357..d69288ccb9 100644 --- a/tests/unit/modules/test_mac_group.py +++ b/tests/unit/modules/test_mac_group.py @@ -5,11 +5,15 @@ # Import python libs from __future__ import absolute_import -import grp +HAS_GRP = True +try: + import grp +except ImportError: + HAS_GRP = False # Import Salt Testing Libs from tests.support.mixins import LoaderModuleMockMixin -from tests.support.unit import TestCase +from tests.support.unit import TestCase, skipIf from tests.support.mock import MagicMock, patch # Import Salt Libs @@ -17,6 +21,7 @@ import salt.modules.mac_group as mac_group from salt.exceptions import SaltInvocationError, CommandExecutionError +@skipIf(not HAS_GRP, "Missing required library 'grp'") class MacGroupTestCase(TestCase, LoaderModuleMockMixin): ''' TestCase for the salt.modules.mac_group module diff --git a/tests/unit/modules/test_mac_user.py b/tests/unit/modules/test_mac_user.py index 51402e6cd0..c639f022da 100644 --- a/tests/unit/modules/test_mac_user.py +++ b/tests/unit/modules/test_mac_user.py @@ -2,10 +2,13 @@ ''' :codeauthor: :email:`Nicole Thomas ` ''' - # Import python libs from __future__ import absolute_import -import pwd +HAS_PWD = True +try: + import pwd +except ImportError: + HAS_PWD = False # Import Salt Testing Libs from tests.support.mixins import LoaderModuleMockMixin @@ -17,6 +20,7 @@ import salt.modules.mac_user as mac_user from salt.exceptions import SaltInvocationError, CommandExecutionError +@skipIf(not HAS_PWD, "Missing required library 'pwd'") @skipIf(NO_MOCK, NO_MOCK_REASON) class MacUserTestCase(TestCase, LoaderModuleMockMixin): ''' @@ -26,14 +30,15 @@ class MacUserTestCase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): return {mac_user: {}} - mock_pwall = [pwd.struct_passwd(('_amavisd', '*', 83, 83, 'AMaViS Daemon', - '/var/virusmails', '/usr/bin/false')), - pwd.struct_passwd(('_appleevents', '*', 55, 55, - 'AppleEvents Daemon', - '/var/empty', '/usr/bin/false')), - pwd.struct_passwd(('_appowner', '*', 87, 87, - 'Application Owner', - '/var/empty', '/usr/bin/false'))] + if HAS_PWD: + mock_pwall = [pwd.struct_passwd(('_amavisd', '*', 83, 83, 'AMaViS Daemon', + '/var/virusmails', '/usr/bin/false')), + pwd.struct_passwd(('_appleevents', '*', 55, 55, + 'AppleEvents Daemon', + '/var/empty', '/usr/bin/false')), + pwd.struct_passwd(('_appowner', '*', 87, 87, + 'Application Owner', + '/var/empty', '/usr/bin/false'))] mock_info_ret = {'shell': '/bin/bash', 'name': 'test', 'gid': 4376, 'groups': ['TEST_GROUP'], 'home': '/Users/foo', 'fullname': 'TEST USER', 'uid': 4376} diff --git a/tests/unit/modules/test_mount.py b/tests/unit/modules/test_mount.py index ef7e6765e2..8d8dcb6067 100644 --- a/tests/unit/modules/test_mount.py +++ b/tests/unit/modules/test_mount.py @@ -21,8 +21,8 @@ from tests.support.mock import ( # Import Salt Libs import salt.utils.files import salt.utils.path -from salt.exceptions import CommandExecutionError import salt.modules.mount as mount +from salt.exceptions import CommandExecutionError MOCK_SHELL_FILE = 'A B C D F G\n' diff --git a/tests/unit/modules/test_pam.py b/tests/unit/modules/test_pam.py index 21d37b0634..05dcfdb2cc 100644 --- a/tests/unit/modules/test_pam.py +++ b/tests/unit/modules/test_pam.py @@ -34,7 +34,8 @@ class PamTestCase(TestCase): ''' Test if the parsing function works ''' - with patch('salt.utils.files.fopen', mock_open(read_data=MOCK_FILE)): + with patch('os.path.exists', return_value=True), \ + patch('salt.utils.files.fopen', mock_open(read_data=MOCK_FILE)): self.assertListEqual(pam.read_file('/etc/pam.d/login'), [{'arguments': [], 'control_flag': 'ok', 'interface': 'ok', 'module': 'ignore'}]) diff --git a/tests/unit/modules/test_parted.py b/tests/unit/modules/test_parted.py index 1e7f89cc88..6accff745a 100644 --- a/tests/unit/modules/test_parted.py +++ b/tests/unit/modules/test_parted.py @@ -41,36 +41,48 @@ class PartedTestCase(TestCase, LoaderModuleMockMixin): # Test __virtual__ function for module registration def test_virtual_bails_on_windows(self): - '''If running windows, __virtual__ shouldn't register module''' + ''' + If running windows, __virtual__ shouldn't register module + ''' with patch('salt.utils.platform.is_windows', lambda: True): ret = parted.__virtual__() err = (False, 'The parted execution module failed to load Windows systems are not supported.') self.assertEqual(err, ret) def test_virtual_bails_without_parted(self): - '''If parted not in PATH, __virtual__ shouldn't register module''' - with patch('salt.utils.path.which', lambda exe: not exe == "parted"): + ''' + If parted not in PATH, __virtual__ shouldn't register module + ''' + with patch('salt.utils.path.which', lambda exe: not exe == "parted"),\ + patch('salt.utils.platform.is_windows', return_value=False): ret = parted.__virtual__() err = (False, 'The parted execution module failed to load parted binary is not in the path.') self.assertEqual(err, ret) def test_virtual_bails_without_lsblk(self): - '''If lsblk not in PATH, __virtual__ shouldn't register module''' - with patch('salt.utils.path.which', lambda exe: not exe == "lsblk"): + ''' + If lsblk not in PATH, __virtual__ shouldn't register module + ''' + with patch('salt.utils.path.which', lambda exe: not exe == "lsblk"),\ + patch('salt.utils.platform.is_windows', return_value=False): ret = parted.__virtual__() err = (False, 'The parted execution module failed to load lsblk binary is not in the path.') self.assertEqual(err, ret) def test_virtual_bails_without_partprobe(self): - '''If partprobe not in PATH, __virtual__ shouldn't register module''' - with patch('salt.utils.path.which', lambda exe: not exe == "partprobe"): + ''' + If partprobe not in PATH, __virtual__ shouldn't register module + ''' + with patch('salt.utils.path.which', lambda exe: not exe == "partprobe"),\ + patch('salt.utils.platform.is_windows', return_value=False): ret = parted.__virtual__() err = (False, 'The parted execution module failed to load partprobe binary is not in the path.') self.assertEqual(err, ret) def test_virtual(self): - '''On expected platform with correct utils in PATH, register - "partition" module''' + ''' + On expected platform with correct utils in PATH, register "partition" module + ''' with patch('salt.utils.platform.is_windows', lambda: False), \ patch('salt.utils.path.which', lambda exe: exe in ('parted', 'lsblk', 'partprobe')): ret = parted.__virtual__() diff --git a/tests/unit/modules/test_portage_config.py b/tests/unit/modules/test_portage_config.py index a0d0119954..02a76b63d2 100644 --- a/tests/unit/modules/test_portage_config.py +++ b/tests/unit/modules/test_portage_config.py @@ -7,11 +7,14 @@ ''' # Import Python libs from __future__ import absolute_import +import re # Import Salt Testing libs from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import skipIf, TestCase from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch +from tests.support.paths import TMP +import salt.utils.files # Import salt libs import salt.modules.portage_config as portage_config @@ -19,27 +22,101 @@ import salt.modules.portage_config as portage_config @skipIf(NO_MOCK, NO_MOCK_REASON) class PortageConfigTestCase(TestCase, LoaderModuleMockMixin): - class DummyAtom(object): - def __init__(self, atom): - self.cp, self.repo = atom.split("::") if "::" in atom else (atom, None) + def __init__(self): + self.cp = None + self.repo = None + + def __call__(self, atom, *_, **__): + if atom == '#' or isinstance(atom, MagicMock): + self.repo = None + self.cp = None + return self + + # extract (and remove) repo + atom, self.repo = atom.split('::') if '::' in atom else (atom, None) + + # remove '>, >=, <=, =, ~' etc. + atom = re.sub(r'[<>~+=]', '', atom) + # remove slots + atom = re.sub(r':[0-9][^:]*', '', atom) + # remove version + atom = re.sub(r'-[0-9][\.0-9]*', '', atom) + + self.cp = atom + return self def setup_loader_modules(self): - self.portage = MagicMock() - self.addCleanup(delattr, self, 'portage') - return {portage_config: {'portage': self.portage}} + try: + import portage + return {} + except ImportError: + dummy_atom = self.DummyAtom() + self.portage = MagicMock() + self.portage.dep.Atom = MagicMock(side_effect=dummy_atom) + self.portage.dep_getkey = MagicMock(side_effect=lambda x: dummy_atom(x).cp) + self.portage.exception.InvalidAtom = Exception + self.addCleanup(delattr, self, 'portage') + return {portage_config: {'portage': self.portage}} def test_get_config_file_wildcards(self): pairs = [ ('*/*::repo', '/etc/portage/package.mask/repo'), ('*/pkg::repo', '/etc/portage/package.mask/pkg'), - ('cat/*', '/etc/portage/package.mask/cat'), + ('cat/*', '/etc/portage/package.mask/cat_'), ('cat/pkg', '/etc/portage/package.mask/cat/pkg'), ('cat/pkg::repo', '/etc/portage/package.mask/cat/pkg'), ] for (atom, expected) in pairs: - dummy_atom = self.DummyAtom(atom) - self.portage.dep.Atom = MagicMock(return_value=dummy_atom) - with patch.object(portage_config, '_p_to_cp', MagicMock(return_value=dummy_atom.cp)): - self.assertEqual(portage_config._get_config_file('mask', atom), expected) + self.assertEqual(portage_config._get_config_file('mask', atom), expected) + + def test_enforce_nice_config(self): + atoms = [ + ('*/*::repo', 'repo'), + ('*/pkg1::repo', 'pkg1'), + ('cat/*', 'cat_'), + ('cat/pkg2', 'cat/pkg2'), + ('cat/pkg3::repo', 'cat/pkg3'), + ('cat/pkg5-0.0.0.0:0', 'cat/pkg5'), + ('>cat/pkg6-0.0.0.0:0::repo', 'cat/pkg6'), + ('<=cat/pkg7-0.0.0.0', 'cat/pkg7'), + ('=cat/pkg8-0.0.0.0', 'cat/pkg8'), + ] + + supported = [ + ('accept_keywords', ['~amd64']), + ('env', ['glibc.conf']), + ('license', ['LICENCE1', 'LICENCE2']), + ('mask', ['']), + ('properties', ['* -interactive']), + ('unmask', ['']), + ('use', ['apple', '-banana', 'ananas', 'orange']), + ] + + base_path = TMP + '/package.{0}' + + def make_line(atom, addition): + return atom + (' ' + addition if addition != '' else '') + '\n' + + for typ, additions in supported: + path = base_path.format(typ) + with salt.utils.files.fopen(path, 'a') as fh: + for atom, _ in atoms: + for addition in additions: + line = make_line(atom, addition) + fh.write('# comment for: ' + line) + fh.write(line) + + with patch.object(portage_config, 'BASE_PATH', base_path): + with patch.object(portage_config, '_merge_flags', lambda l1, l2, _: list(set(l1 + l2))): + portage_config.enforce_nice_config() + + for typ, additions in supported: + for atom, file_name in atoms: + with salt.utils.files.fopen(base_path.format(typ) + "/" + file_name, 'r') as fh: + for line in fh: + self.assertTrue(atom in line, msg="'{}' not in '{}'".format(addition, line)) + for addition in additions: + self.assertTrue(addition in line, msg="'{}' not in '{}'".format(addition, line)) diff --git a/tests/unit/modules/test_pw_group.py b/tests/unit/modules/test_pw_group.py index 3d21bbd43c..2cfc5f32d2 100644 --- a/tests/unit/modules/test_pw_group.py +++ b/tests/unit/modules/test_pw_group.py @@ -18,6 +18,7 @@ from tests.support.mock import ( # Import Salt Libs import salt.modules.pw_group as pw_group +import salt.utils.platform @skipIf(NO_MOCK, NO_MOCK_REASON) @@ -44,6 +45,7 @@ class PwGroupTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(pw_group.__salt__, {'cmd.run_all': mock}): self.assertTrue(pw_group.delete('a')) + @skipIf(salt.utils.platform.is_windows(), 'grp not available on Windows') def test_info(self): ''' Tests to return information about a group @@ -57,6 +59,7 @@ class PwGroupTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(pw_group.grinfo, mock): self.assertDictEqual(pw_group.info('name'), {}) + @skipIf(salt.utils.platform.is_windows(), 'grp not available on Windows') def test_getent(self): ''' Tests for return info on all groups diff --git a/tests/unit/modules/test_qemu_nbd.py b/tests/unit/modules/test_qemu_nbd.py index ec6ec84587..59361c0050 100644 --- a/tests/unit/modules/test_qemu_nbd.py +++ b/tests/unit/modules/test_qemu_nbd.py @@ -80,15 +80,14 @@ class QemuNbdTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(qemu_nbd.__salt__, {'cmd.run': mock}): self.assertEqual(qemu_nbd.init('/srv/image.qcow2'), '') - with patch.object(os.path, 'isfile', mock): - with patch.object(glob, 'glob', - MagicMock(return_value=['/dev/nbd0'])): - with patch.dict(qemu_nbd.__salt__, - {'cmd.run': mock, - 'mount.mount': mock, - 'cmd.retcode': MagicMock(side_effect=[1, 0])}): - self.assertDictEqual(qemu_nbd.init('/srv/image.qcow2'), - {'{0}/nbd/nbd0/nbd0'.format(tempfile.gettempdir()): '/dev/nbd0'}) + with patch.object(os.path, 'isfile', mock),\ + patch.object(glob, 'glob', MagicMock(return_value=['/dev/nbd0'])),\ + patch.dict(qemu_nbd.__salt__, + {'cmd.run': mock, + 'mount.mount': mock, + 'cmd.retcode': MagicMock(side_effect=[1, 0])}): + expected = {os.sep.join([tempfile.gettempdir(), 'nbd', 'nbd0', 'nbd0']): '/dev/nbd0'} + self.assertDictEqual(qemu_nbd.init('/srv/image.qcow2'), expected) # 'clear' function tests: 1 diff --git a/tests/unit/modules/test_rh_ip.py b/tests/unit/modules/test_rh_ip.py index 08b3367a73..f115ccf5ba 100644 --- a/tests/unit/modules/test_rh_ip.py +++ b/tests/unit/modules/test_rh_ip.py @@ -58,7 +58,7 @@ class RhipTestCase(TestCase, LoaderModuleMockMixin): ''' Test to build an interface script for a network interface. ''' - with patch.dict(rh_ip.__grains__, {'os': 'Fedora'}): + with patch.dict(rh_ip.__grains__, {'os': 'Fedora', 'osmajorrelease': 26}): with patch.object(rh_ip, '_raise_error_iface', return_value=None): self.assertRaises(AttributeError, diff --git a/tests/unit/modules/test_saltcheck.py b/tests/unit/modules/test_saltcheck.py new file mode 100644 index 0000000000..5907af17ef --- /dev/null +++ b/tests/unit/modules/test_saltcheck.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +'''Unit test for saltcheck execution module''' + +# Import Python libs +from __future__ import absolute_import +import os.path + +try: + import salt.modules.saltcheck as saltcheck + import salt.config + import salt.syspaths as syspaths +except: + raise + +# Import Salt Testing Libs +try: + from tests.support.mixins import LoaderModuleMockMixin + from tests.support.unit import skipIf, TestCase + from tests.support.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON + ) +except: + raise + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class LinuxSysctlTestCase(TestCase, LoaderModuleMockMixin): + ''' + TestCase for salt.modules.saltcheck module + ''' + + def setup_loader_modules(self): + # Setting the environment to be local + local_opts = salt.config.minion_config( + os.path.join(syspaths.CONFIG_DIR, u'minion')) + local_opts['file_client'] = 'local' + patcher = patch('salt.config.minion_config', + MagicMock(return_value=local_opts)) + patcher.start() + self.addCleanup(patcher.stop) + return {saltcheck: {}} + + def test_call_salt_command(self): + '''test simple test.echo module''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'sys.list_modules': MagicMock(return_value=['module1']), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + returned = sc_instance.call_salt_command(fun="test.echo", args=['hello'], kwargs=None) + self.assertEqual(returned, 'hello') + + def test_update_master_cache(self): + '''test master cache''' + self.assertTrue(saltcheck.update_master_cache) + + def test_call_salt_command2(self): + '''test simple test.echo module again''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'sys.list_modules': MagicMock(return_value=['module1']), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + returned = sc_instance.call_salt_command(fun="test.echo", args=['hello'], kwargs=None) + self.assertNotEqual(returned, 'not-hello') + + def test__assert_equal1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = {'a': 1, 'b': 2} + bbb = {'a': 1, 'b': 2} + mybool = sc_instance._SaltCheck__assert_equal(aaa, bbb) + self.assertTrue(mybool) + + def test__assert_equal2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + mybool = sc_instance._SaltCheck__assert_equal(False, True) + self.assertNotEqual(mybool, True) + + def test__assert_not_equal1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = {'a': 1, 'b': 2} + bbb = {'a': 1, 'b': 2, 'c': 3} + mybool = sc_instance._SaltCheck__assert_not_equal(aaa, bbb) + self.assertTrue(mybool) + + def test__assert_not_equal2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = {'a': 1, 'b': 2} + bbb = {'a': 1, 'b': 2} + mybool = sc_instance._SaltCheck__assert_not_equal(aaa, bbb) + self.assertNotEqual(mybool, True) + + def test__assert_true1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + mybool = sc_instance._SaltCheck__assert_equal(True, True) + self.assertTrue(mybool) + + def test__assert_true2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + mybool = sc_instance._SaltCheck__assert_equal(False, True) + self.assertNotEqual(mybool, True) + + def test__assert_false1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + mybool = sc_instance._SaltCheck__assert_false(False) + self.assertTrue(mybool) + + def test__assert_false2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + mybool = sc_instance._SaltCheck__assert_false(True) + self.assertNotEqual(mybool, True) + + def test__assert_in1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = "bob" + mylist = ['alice', 'bob', 'charles', 'dana'] + mybool = sc_instance._SaltCheck__assert_in(aaa, mylist) + self.assertTrue(mybool, True) + + def test__assert_in2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = "elaine" + mylist = ['alice', 'bob', 'charles', 'dana'] + mybool = sc_instance._SaltCheck__assert_in(aaa, mylist) + self.assertNotEqual(mybool, True) + + def test__assert_not_in1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = "elaine" + mylist = ['alice', 'bob', 'charles', 'dana'] + mybool = sc_instance._SaltCheck__assert_not_in(aaa, mylist) + self.assertTrue(mybool, True) + + def test__assert_not_in2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = "bob" + mylist = ['alice', 'bob', 'charles', 'dana'] + mybool = sc_instance._SaltCheck__assert_not_in(aaa, mylist) + self.assertNotEqual(mybool, True) + + def test__assert_greater1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 110 + bbb = 100 + mybool = sc_instance._SaltCheck__assert_greater(aaa, bbb) + self.assertTrue(mybool, True) + + def test__assert_greater2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 100 + bbb = 110 + mybool = sc_instance._SaltCheck__assert_greater(aaa, bbb) + self.assertNotEqual(mybool, True) + + def test__assert_greater3(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 100 + bbb = 100 + mybool = sc_instance._SaltCheck__assert_greater(aaa, bbb) + self.assertNotEqual(mybool, True) + + def test__assert_greater_equal1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 110 + bbb = 100 + mybool = sc_instance._SaltCheck__assert_greater_equal(aaa, bbb) + self.assertTrue(mybool, True) + + def test__assert_greater_equal2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 100 + bbb = 110 + mybool = sc_instance._SaltCheck__assert_greater_equal(aaa, bbb) + self.assertNotEqual(mybool, True) + + def test__assert_greater_equal3(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 100 + bbb = 100 + mybool = sc_instance._SaltCheck__assert_greater_equal(aaa, bbb) + self.assertEqual(mybool, 'Pass') + + def test__assert_less1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 99 + bbb = 100 + mybool = sc_instance._SaltCheck__assert_less(aaa, bbb) + self.assertTrue(mybool, True) + + def test__assert_less2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 110 + bbb = 99 + mybool = sc_instance._SaltCheck__assert_less(aaa, bbb) + self.assertNotEqual(mybool, True) + + def test__assert_less3(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 100 + bbb = 100 + mybool = sc_instance._SaltCheck__assert_less(aaa, bbb) + self.assertNotEqual(mybool, True) + + def test__assert_less_equal1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 99 + bbb = 100 + mybool = sc_instance._SaltCheck__assert_less_equal(aaa, bbb) + self.assertTrue(mybool, True) + + def test__assert_less_equal2(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 110 + bbb = 99 + mybool = sc_instance._SaltCheck__assert_less_equal(aaa, bbb) + self.assertNotEqual(mybool, True) + + def test__assert_less_equal3(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'cp.cache_master': MagicMock(return_value=[True]) + }): + sc_instance = saltcheck.SaltCheck() + aaa = 100 + bbb = 100 + mybool = sc_instance._SaltCheck__assert_less_equal(aaa, bbb) + self.assertEqual(mybool, 'Pass') + + def test_run_test_1(self): + '''test''' + with patch.dict(saltcheck.__salt__, {'config.get': MagicMock(return_value=True), + 'sys.list_modules': MagicMock(return_value=['test']), + 'sys.list_functions': MagicMock(return_value=['test.echo']), + 'cp.cache_master': MagicMock(return_value=[True])}): + returned = saltcheck.run_test(test={"module_and_function": "test.echo", + "assertion": "assertEqual", + "expected-return": "This works!", + "args": ["This works!"] + }) + self.assertEqual(returned, 'Pass') diff --git a/tests/unit/modules/test_seed.py b/tests/unit/modules/test_seed.py index d0dc7e912a..8a795889ba 100644 --- a/tests/unit/modules/test_seed.py +++ b/tests/unit/modules/test_seed.py @@ -47,14 +47,19 @@ class SeedTestCase(TestCase, LoaderModuleMockMixin): ''' Test to update and get the random script to a random place ''' - with patch.dict(seed.__salt__, - {'config.gather_bootstrap_script': MagicMock(return_value='BS_PATH/BS')}): - with patch.object(uuid, 'uuid4', return_value='UUID'): - with patch.object(os.path, 'exists', return_value=True): - with patch.object(os, 'chmod', return_value=None): - with patch.object(shutil, 'copy', return_value=None): - self.assertEqual(seed.prep_bootstrap('MPT'), ('MPT/tmp/UUID/BS', '/tmp/UUID')) - self.assertEqual(seed.prep_bootstrap('/MPT'), ('/MPT/tmp/UUID/BS', '/tmp/UUID')) + with patch.dict(seed.__salt__, {'config.gather_bootstrap_script': MagicMock(return_value=os.path.join('BS_PATH', 'BS'))}),\ + patch.object(uuid, 'uuid4', return_value='UUID'),\ + patch.object(os.path, 'exists', return_value=True),\ + patch.object(os, 'chmod', return_value=None),\ + patch.object(shutil, 'copy', return_value=None): + + expect = (os.path.join('MPT', 'tmp', 'UUID', 'BS'), + os.sep + os.path.join('tmp', 'UUID')) + self.assertEqual(seed.prep_bootstrap('MPT'), expect) + + expect = (os.sep + os.path.join('MPT', 'tmp', 'UUID', 'BS'), + os.sep + os.path.join('tmp', 'UUID')) + self.assertEqual(seed.prep_bootstrap(os.sep + 'MPT'), expect) def test_apply_(self): ''' diff --git a/tests/unit/modules/test_state.py b/tests/unit/modules/test_state.py index 26162a8705..b6dc97af05 100644 --- a/tests/unit/modules/test_state.py +++ b/tests/unit/modules/test_state.py @@ -19,11 +19,14 @@ from tests.support.mock import ( ) # Import Salt Libs +import salt.config +import salt.loader import salt.utils import salt.utils.odict import salt.utils.platform import salt.modules.state as state from salt.exceptions import SaltInvocationError +from salt.ext import six class MockState(object): @@ -345,6 +348,10 @@ class StateTestCase(TestCase, LoaderModuleMockMixin): ''' def setup_loader_modules(self): + utils = salt.loader.utils( + salt.config.DEFAULT_MINION_OPTS, + whitelist=['state'] + ) patcher = patch('salt.modules.state.salt.state', MockState()) patcher.start() self.addCleanup(patcher.stop) @@ -355,6 +362,7 @@ class StateTestCase(TestCase, LoaderModuleMockMixin): 'environment': None, '__cli': 'salt', }, + '__utils__': utils, }, } @@ -977,6 +985,12 @@ class StateTestCase(TestCase, LoaderModuleMockMixin): MockTarFile.path = "" MockJson.flag = False - with patch('salt.utils.files.fopen', mock_open()): - self.assertTrue(state.pkg("/tmp/state_pkg.tgz", - 0, "md5")) + if six.PY2: + with patch('salt.utils.files.fopen', mock_open()), \ + patch.dict(state.__utils__, {'state.check_result': MagicMock(return_value=True)}): + self.assertTrue(state.pkg("/tmp/state_pkg.tgz", + 0, "md5")) + else: + with patch('salt.utils.files.fopen', mock_open()): + self.assertTrue(state.pkg("/tmp/state_pkg.tgz", + 0, "md5")) diff --git a/tests/unit/modules/test_virtualenv.py b/tests/unit/modules/test_virtualenv.py index 25f511ef1b..7e86f2bcad 100644 --- a/tests/unit/modules/test_virtualenv.py +++ b/tests/unit/modules/test_virtualenv.py @@ -109,10 +109,9 @@ class VirtualenvTestCase(TestCase, LoaderModuleMockMixin): # Are we logging the deprecation information? self.assertIn( - 'INFO:The virtualenv \'--never-download\' option has been ' - 'deprecated in virtualenv(>=1.10), as such, the ' - '\'never_download\' option to `virtualenv.create()` has ' - 'also been deprecated and it\'s not necessary anymore.', + 'INFO:--never-download was deprecated in 1.10.0, ' + 'but reimplemented in 14.0.0. If this feature is needed, ' + 'please install a supported virtualenv version.', handler.messages ) diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index e7ac51dfa0..56669b900e 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -11,7 +11,8 @@ from __future__ import absolute_import # Import Salt Libs import salt.modules.vsphere as vsphere -from salt.exceptions import CommandExecutionError, VMwareSaltError +from salt.exceptions import CommandExecutionError, VMwareSaltError, \ + ArgumentValueError, VMwareObjectRetrievalError # Import Salt Testing Libs from tests.support.mixins import LoaderModuleMockMixin @@ -620,6 +621,7 @@ class _GetProxyConnectionDetailsTestCase(TestCase, LoaderModuleMockMixin): 'principal': 'fake_principal', 'domain': 'fake_domain'} self.esxdatacenter_details = {'vcenter': 'fake_vcenter', + 'datacenter': 'fake_dc', 'username': 'fake_username', 'password': 'fake_password', 'protocol': 'fake_protocol', @@ -627,9 +629,20 @@ class _GetProxyConnectionDetailsTestCase(TestCase, LoaderModuleMockMixin): 'mechanism': 'fake_mechanism', 'principal': 'fake_principal', 'domain': 'fake_domain'} + self.esxcluster_details = {'vcenter': 'fake_vcenter', + 'datacenter': 'fake_dc', + 'cluster': 'fake_cluster', + 'username': 'fake_username', + 'password': 'fake_password', + 'protocol': 'fake_protocol', + 'port': 'fake_port', + 'mechanism': 'fake_mechanism', + 'principal': 'fake_principal', + 'domain': 'fake_domain'} def tearDown(self): - for attrname in ('esxi_host_details', 'esxi_vcenter_details'): + for attrname in ('esxi_host_details', 'esxi_vcenter_details', + 'esxdatacenter_details', 'esxcluster_details'): try: delattr(self, attrname) except AttributeError: @@ -651,8 +664,22 @@ class _GetProxyConnectionDetailsTestCase(TestCase, LoaderModuleMockMixin): MagicMock(return_value='esxdatacenter')): with patch.dict(vsphere.__salt__, {'esxdatacenter.get_details': MagicMock( - return_value=self.esxdatacenter_details)}): + return_value=self.esxdatacenter_details)}): ret = vsphere._get_proxy_connection_details() + self.assertEqual(('fake_vcenter', 'fake_username', 'fake_password', + 'fake_protocol', 'fake_port', 'fake_mechanism', + 'fake_principal', 'fake_domain'), ret) + + def test_esxcluster_proxy_details(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxcluster')): + with patch.dict(vsphere.__salt__, + {'esxcluster.get_details': MagicMock( + return_value=self.esxcluster_details)}): + ret = vsphere._get_proxy_connection_details() + self.assertEqual(('fake_vcenter', 'fake_username', 'fake_password', + 'fake_protocol', 'fake_port', 'fake_mechanism', + 'fake_principal', 'fake_domain'), ret) def test_esxi_proxy_vcenter_details(self): with patch('salt.modules.vsphere.get_proxy_type', @@ -862,8 +889,8 @@ class GetServiceInstanceViaProxyTestCase(TestCase, LoaderModuleMockMixin): } } - def test_supported_proxes(self): - supported_proxies = ['esxi', 'esxdatacenter'] + def test_supported_proxies(self): + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -905,8 +932,8 @@ class DisconnectTestCase(TestCase, LoaderModuleMockMixin): } } - def test_supported_proxes(self): - supported_proxies = ['esxi', 'esxdatacenter'] + def test_supported_proxies(self): + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -946,8 +973,8 @@ class TestVcenterConnectionTestCase(TestCase, LoaderModuleMockMixin): } } - def test_supported_proxes(self): - supported_proxies = ['esxi', 'esxdatacenter'] + def test_supported_proxies(self): + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -1022,7 +1049,7 @@ class ListDatacentersViaProxyTestCase(TestCase, LoaderModuleMockMixin): } def test_supported_proxies(self): - supported_proxies = ['esxdatacenter'] + supported_proxies = ['esxcluster', 'esxdatacenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -1099,7 +1126,7 @@ class CreateDatacenterTestCase(TestCase, LoaderModuleMockMixin): } } - def test_supported_proxes(self): + def test_supported_proxies(self): supported_proxies = ['esxdatacenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', @@ -1125,3 +1152,260 @@ class CreateDatacenterTestCase(TestCase, LoaderModuleMockMixin): def test_returned_value(self): res = vsphere.create_datacenter('fake_dc1') self.assertEqual(res, {'create_datacenter': True}) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class ListClusterTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.modules.vsphere.list_cluster''' + def setup_loader_modules(self): + return { + vsphere: { + '__virtual__': MagicMock(return_value='vsphere'), + '_get_proxy_connection_details': MagicMock(), + '__salt__': {} + } + } + + def setUp(self): + attrs = (('mock_si', MagicMock()), + ('mock_dc', MagicMock()), + ('mock_cl', MagicMock()), + ('mock__get_cluster_dict', MagicMock())) + for attr, mock_obj in attrs: + setattr(self, attr, mock_obj) + self.addCleanup(delattr, self, attr) + attrs = (('mock_get_cluster', MagicMock(return_value=self.mock_cl)),) + for attr, mock_obj in attrs: + setattr(self, attr, mock_obj) + self.addCleanup(delattr, self, attr) + patches = ( + ('salt.utils.vmware.get_service_instance', + MagicMock(return_value=self.mock_si)), + ('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxcluster')), + ('salt.modules.vsphere._get_proxy_target', + MagicMock(return_value=self.mock_cl)), + ('salt.utils.vmware.get_cluster', self.mock_get_cluster), + ('salt.modules.vsphere._get_cluster_dict', + self.mock__get_cluster_dict)) + for module, mock_obj in patches: + patcher = patch(module, mock_obj) + patcher.start() + self.addCleanup(patcher.stop) + # Patch __salt__ dunder + patcher = patch.dict(vsphere.__salt__, + {'esxcluster.get_details': + MagicMock(return_value={'cluster': 'cl'})}) + patcher.start() + self.addCleanup(patcher.stop) + + def test_supported_proxies(self): + supported_proxies = ['esxcluster', 'esxdatacenter'] + for proxy_type in supported_proxies: + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value=proxy_type)): + vsphere.list_cluster(cluster='cl') + + def test_default_service_instance(self): + mock__get_proxy_target = MagicMock() + with patch('salt.modules.vsphere._get_proxy_target', + mock__get_proxy_target): + vsphere.list_cluster() + mock__get_proxy_target.assert_called_once_with(self.mock_si) + + def test_defined_service_instance(self): + mock_si = MagicMock() + mock__get_proxy_target = MagicMock() + with patch('salt.modules.vsphere._get_proxy_target', + mock__get_proxy_target): + vsphere.list_cluster(service_instance=mock_si) + mock__get_proxy_target.assert_called_once_with(mock_si) + + def test_no_cluster_raises_argument_value_error(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxdatacenter')): + with patch('salt.modules.vsphere._get_proxy_target', MagicMock()): + with self.assertRaises(ArgumentValueError) as excinfo: + vsphere.list_cluster() + self.assertEqual(excinfo.exception.strerror, + '\'cluster\' needs to be specified') + + def test_get_cluster_call(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxdatacenter')): + with patch('salt.modules.vsphere._get_proxy_target', + MagicMock(return_value=self.mock_dc)): + vsphere.list_cluster(cluster='cl') + self.mock_get_cluster.assert_called_once_with(self.mock_dc, 'cl') + + def test__get_cluster_dict_call(self): + vsphere.list_cluster() + self.mock__get_cluster_dict.assert_called_once_with('cl', self.mock_cl) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class RenameDatastoreTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.modules.vsphere.rename_datastore''' + def setup_loader_modules(self): + return { + vsphere: { + '__virtual__': MagicMock(return_value='vsphere'), + '_get_proxy_connection_details': MagicMock(), + 'get_proxy_type': MagicMock(return_value='esxdatacenter') + } + } + + def setUp(self): + self.mock_si = MagicMock() + self.mock_target = MagicMock() + self.mock_ds_ref = MagicMock() + self.mock_get_datastores = MagicMock(return_value=[self.mock_ds_ref]) + self.mock_rename_datastore = MagicMock() + patches = ( + ('salt.utils.vmware.get_service_instance', + MagicMock(return_value=self.mock_si)), + ('salt.modules.vsphere._get_proxy_target', + MagicMock(return_value=self.mock_target)), + ('salt.utils.vmware.get_datastores', + self.mock_get_datastores), + ('salt.utils.vmware.rename_datastore', + self.mock_rename_datastore)) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_target', 'mock_ds_ref', + 'mock_get_datastores', 'mock_rename_datastore'): + delattr(self, attr) + + def test_supported_proxes(self): + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] + for proxy_type in supported_proxies: + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value=proxy_type)): + vsphere.rename_datastore('current_ds_name', 'new_ds_name') + + def test_default_service_instance(self): + mock__get_proxy_target = MagicMock() + with patch('salt.modules.vsphere._get_proxy_target', + mock__get_proxy_target): + vsphere.rename_datastore('current_ds_name', 'new_ds_name') + mock__get_proxy_target.assert_called_once_with(self.mock_si) + + def test_defined_service_instance(self): + mock_si = MagicMock() + mock__get_proxy_target = MagicMock() + with patch('salt.modules.vsphere._get_proxy_target', + mock__get_proxy_target): + vsphere.rename_datastore('current_ds_name', 'new_ds_name', + service_instance=mock_si) + + mock__get_proxy_target.assert_called_once_with(mock_si) + + def test_get_datastore_call(self): + vsphere.rename_datastore('current_ds_name', 'new_ds_name') + self.mock_get_datastores.assert_called_once_with( + self.mock_si, self.mock_target, + datastore_names=['current_ds_name']) + + def test_get_no_datastores(self): + with patch('salt.utils.vmware.get_datastores', + MagicMock(return_value=[])): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vsphere.rename_datastore('current_ds_name', 'new_ds_name') + self.assertEqual(excinfo.exception.strerror, + 'Datastore \'current_ds_name\' was not found') + + def test_rename_datastore_call(self): + vsphere.rename_datastore('current_ds_name', 'new_ds_name') + self.mock_rename_datastore.assert_called_once_with( + self.mock_ds_ref, 'new_ds_name') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class _GetProxyTargetTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.modules.vsphere._get_proxy_target''' + def setup_loader_modules(self): + return { + vsphere: { + '__virtual__': MagicMock(return_value='vsphere'), + '_get_proxy_connection_details': MagicMock(), + 'get_proxy_type': MagicMock(return_value='esxdatacenter') + } + } + + def setUp(self): + attrs = (('mock_si', MagicMock()), + ('mock_dc', MagicMock()), + ('mock_cl', MagicMock())) + for attr, mock_obj in attrs: + setattr(self, attr, mock_obj) + self.addCleanup(delattr, self, attr) + attrs = (('mock_get_datacenter', MagicMock(return_value=self.mock_dc)), + ('mock_get_cluster', MagicMock(return_value=self.mock_cl))) + for attr, mock_obj in attrs: + setattr(self, attr, mock_obj) + self.addCleanup(delattr, self, attr) + patches = ( + ('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxcluster')), + ('salt.utils.vmware.is_connection_to_a_vcenter', + MagicMock(return_value=True)), + ('salt.modules.vsphere._get_esxcluster_proxy_details', + MagicMock(return_value=(None, None, None, None, None, None, None, + None, 'datacenter', 'cluster'))), + ('salt.modules.vsphere._get_esxdatacenter_proxy_details', + MagicMock(return_value=(None, None, None, None, None, None, None, + None, 'datacenter'))), + ('salt.utils.vmware.get_datacenter', self.mock_get_datacenter), + ('salt.utils.vmware.get_cluster', self.mock_get_cluster)) + for module, mock_obj in patches: + patcher = patch(module, mock_obj) + patcher.start() + self.addCleanup(patcher.stop) + + def test_supported_proxies(self): + supported_proxies = ['esxcluster', 'esxdatacenter'] + for proxy_type in supported_proxies: + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value=proxy_type)): + vsphere._get_proxy_target(self.mock_si) + + def test_connected_to_esxi(self): + with patch('salt.utils.vmware.is_connection_to_a_vcenter', + MagicMock(return_value=False)): + with self.assertRaises(CommandExecutionError) as excinfo: + vsphere._get_proxy_target(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + '\'_get_proxy_target\' not supported when ' + 'connected via the ESXi host') + + def test_get_cluster_call(self): + #with patch('salt.modules.vsphere.get_proxy_type', + # MagicMock(return_value='esxcluster')): + vsphere._get_proxy_target(self.mock_si) + self.mock_get_datacenter.assert_called_once_with(self.mock_si, + 'datacenter') + self.mock_get_cluster.assert_called_once_with(self.mock_dc, 'cluster') + + def test_esxcluster_proxy_return(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxcluster')): + ret = vsphere._get_proxy_target(self.mock_si) + self.assertEqual(ret, self.mock_cl) + + def test_get_datacenter_call(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxdatacenter')): + vsphere._get_proxy_target(self.mock_si) + self.mock_get_datacenter.assert_called_once_with(self.mock_si, + 'datacenter') + self.assertEqual(self.mock_get_cluster.call_count, 0) + + def test_esxdatacenter_proxy_return(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxdatacenter')): + ret = vsphere._get_proxy_target(self.mock_si) + self.assertEqual(ret, self.mock_dc) diff --git a/tests/unit/modules/test_yumpkg.py b/tests/unit/modules/test_yumpkg.py index cf754d6289..84e0a0ac52 100644 --- a/tests/unit/modules/test_yumpkg.py +++ b/tests/unit/modules/test_yumpkg.py @@ -103,72 +103,86 @@ class YumTestCase(TestCase, LoaderModuleMockMixin): patch.dict(yumpkg.__salt__, {'pkg_resource.add_pkg': _add_data}), \ patch.dict(yumpkg.__salt__, {'pkg_resource.format_pkg_list': pkg_resource.format_pkg_list}), \ patch.dict(yumpkg.__salt__, {'pkg_resource.stringify': MagicMock()}): - pkgs = yumpkg.list_pkgs(attr=['arch', 'install_date_time_t']) + pkgs = yumpkg.list_pkgs(attr=['epoch', 'release', 'arch', 'install_date_time_t']) for pkg_name, pkg_attr in { 'python-urlgrabber': { - 'version': '3.10-8.el7', + 'version': '3.10', + 'release': '8.el7', 'arch': 'noarch', 'install_date_time_t': 1487838471, }, 'alsa-lib': { - 'version': '1.1.1-1.el7', + 'version': '1.1.1', + 'release': '1.el7', 'arch': 'x86_64', 'install_date_time_t': 1487838475, }, 'gnupg2': { - 'version': '2.0.22-4.el7', + 'version': '2.0.22', + 'release': '4.el7', 'arch': 'x86_64', 'install_date_time_t': 1487838477, }, 'rpm-python': { - 'version': '4.11.3-21.el7', + 'version': '4.11.3', + 'release': '21.el7', 'arch': 'x86_64', 'install_date_time_t': 1487838477, }, 'pygpgme': { - 'version': '0.3-9.el7', + 'version': '0.3', + 'release': '9.el7', 'arch': 'x86_64', 'install_date_time_t': 1487838478, }, 'yum': { - 'version': '3.4.3-150.el7.centos', + 'version': '3.4.3', + 'release': '150.el7.centos', 'arch': 'noarch', 'install_date_time_t': 1487838479, }, 'lzo': { - 'version': '2.06-8.el7', + 'version': '2.06', + 'release': '8.el7', 'arch': 'x86_64', 'install_date_time_t': 1487838479, }, 'qrencode-libs': { - 'version': '3.4.1-3.el7', + 'version': '3.4.1', + 'release': '3.el7', 'arch': 'x86_64', 'install_date_time_t': 1487838480, }, 'ustr': { - 'version': '1.0.4-16.el7', + 'version': '1.0.4', + 'release': '16.el7', 'arch': 'x86_64', 'install_date_time_t': 1487838480, }, 'shadow-utils': { - 'version': '2:4.1.5.1-24.el7', + 'epoch': '2', + 'version': '4.1.5.1', + 'release': '24.el7', 'arch': 'x86_64', 'install_date_time_t': 1487838481, }, 'util-linux': { - 'version': '2.23.2-33.el7', + 'version': '2.23.2', + 'release': '33.el7', 'arch': 'x86_64', 'install_date_time_t': 1487838484, }, 'openssh': { - 'version': '6.6.1p1-33.el7_3', + 'version': '6.6.1p1', + 'release': '33.el7_3', 'arch': 'x86_64', 'install_date_time_t': 1487838485, }, 'virt-what': { - 'version': '1.13-8.el7', - 'arch': 'x86_64', + 'version': '1.13', + 'release': '8.el7', 'install_date_time_t': 1487838486, + 'arch': 'x86_64', }}.items(): self.assertTrue(pkgs.get(pkg_name)) self.assertEqual(pkgs[pkg_name], [pkg_attr]) diff --git a/tests/unit/modules/test_zypper.py b/tests/unit/modules/test_zypper.py index ae343ee7d6..a6a0c88794 100644 --- a/tests/unit/modules/test_zypper.py +++ b/tests/unit/modules/test_zypper.py @@ -533,36 +533,42 @@ Repository 'DUMMY' not found by its alias, number, or URI. patch.dict(zypper.__salt__, {'pkg_resource.add_pkg': _add_data}), \ patch.dict(zypper.__salt__, {'pkg_resource.format_pkg_list': pkg_resource.format_pkg_list}), \ patch.dict(zypper.__salt__, {'pkg_resource.stringify': MagicMock()}): - pkgs = zypper.list_pkgs(attr=['arch', 'install_date_time_t']) + pkgs = zypper.list_pkgs(attr=['epoch', 'release', 'arch', 'install_date_time_t']) for pkg_name, pkg_attr in { 'jakarta-commons-discovery': { - 'version': '0.4-129.686', + 'version': '0.4', + 'release': '129.686', 'arch': 'noarch', 'install_date_time_t': 1498636511, }, 'yast2-ftp-server': { - 'version': '3.1.8-8.1', + 'version': '3.1.8', + 'release': '8.1', 'arch': 'x86_64', 'install_date_time_t': 1499257798, }, 'protobuf-java': { - 'version': '2.6.1-3.1.develHead', - 'arch': 'noarch', + 'version': '2.6.1', + 'release': '3.1.develHead', 'install_date_time_t': 1499257756, + 'arch': 'noarch', }, 'susemanager-build-keys-web': { - 'version': '12.0-5.1.develHead', + 'version': '12.0', + 'release': '5.1.develHead', 'arch': 'noarch', 'install_date_time_t': 1498636510, }, 'apache-commons-cli': { - 'version': '1.2-1.233', + 'version': '1.2', + 'release': '1.233', 'arch': 'noarch', 'install_date_time_t': 1498636510, }, 'jose4j': { - 'version': '0.4.4-2.1.develHead', 'arch': 'noarch', + 'version': '0.4.4', + 'release': '2.1.develHead', 'install_date_time_t': 1499257756, }}.items(): self.assertTrue(pkgs.get(pkg_name)) diff --git a/tests/unit/pillar/test_nodegroups.py b/tests/unit/pillar/test_nodegroups.py index 03b4730d95..4b872d79cb 100644 --- a/tests/unit/pillar/test_nodegroups.py +++ b/tests/unit/pillar/test_nodegroups.py @@ -27,8 +27,10 @@ fake_pillar_name = 'fake_pillar_name' def side_effect(group_sel, t): if group_sel.find(fake_minion_id) != -1: - return [fake_minion_id, ] - return ['another_minion_id', ] + return {'minions': [fake_minion_id, ], + 'missing': []} + return {'minions': ['another_minion_id', ], + 'missing': []} class NodegroupsPillarTestCase(TestCase, LoaderModuleMockMixin): diff --git a/tests/unit/proxy/test_esxcluster.py b/tests/unit/proxy/test_esxcluster.py new file mode 100644 index 0000000000..e13faab69e --- /dev/null +++ b/tests/unit/proxy/test_esxcluster.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu ` + + Tests for esxcluster proxy +''' + +# Import Python Libs +from __future__ import absolute_import + +# Import external libs +try: + import jsonschema + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False + +# Import Salt Libs +import salt.proxy.esxcluster as esxcluster +import salt.exceptions +from salt.config.schemas.esxcluster import EsxclusterProxySchema + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_JSONSCHEMA, 'jsonschema is required') +class InitTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.proxy.esxcluster.init''' + def setup_loader_modules(self): + return {esxcluster: {'__virtual__': + MagicMock(return_value='esxcluster'), + 'DETAILS': {}, '__pillar__': {}}} + + def setUp(self): + self.opts_userpass = {'proxy': {'proxytype': 'esxcluster', + 'vcenter': 'fake_vcenter', + 'datacenter': 'fake_dc', + 'cluster': 'fake_cluster', + 'mechanism': 'userpass', + 'username': 'fake_username', + 'passwords': ['fake_password'], + 'protocol': 'fake_protocol', + 'port': 100}} + self.opts_sspi = {'proxy': {'proxytype': 'esxcluster', + 'vcenter': 'fake_vcenter', + 'datacenter': 'fake_dc', + 'cluster': 'fake_cluster', + 'mechanism': 'sspi', + 'domain': 'fake_domain', + 'principal': 'fake_principal', + 'protocol': 'fake_protocol', + 'port': 100}} + patches = (('salt.proxy.esxcluster.merge', + MagicMock(return_value=self.opts_sspi['proxy'])),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def test_merge(self): + mock_pillar_proxy = MagicMock() + mock_opts_proxy = MagicMock() + mock_merge = MagicMock(return_value=self.opts_sspi['proxy']) + with patch.dict(esxcluster.__pillar__, + {'proxy': mock_pillar_proxy}): + with patch('salt.proxy.esxcluster.merge', mock_merge): + esxcluster.init(opts={'proxy': mock_opts_proxy}) + mock_merge.assert_called_once_with(mock_opts_proxy, mock_pillar_proxy) + + def test_esxcluster_schema(self): + mock_json_validate = MagicMock() + serialized_schema = EsxclusterProxySchema().serialize() + with patch('salt.proxy.esxcluster.jsonschema.validate', + mock_json_validate): + esxcluster.init(self.opts_sspi) + mock_json_validate.assert_called_once_with( + self.opts_sspi['proxy'], serialized_schema) + + def test_invalid_proxy_input_error(self): + with patch('salt.proxy.esxcluster.jsonschema.validate', + MagicMock(side_effect=jsonschema.exceptions.ValidationError( + 'Validation Error'))): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxcluster.init(self.opts_userpass) + self.assertEqual(excinfo.exception.strerror.message, + 'Validation Error') + + def test_no_username(self): + opts = self.opts_userpass.copy() + del opts['proxy']['username'] + with patch('salt.proxy.esxcluster.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxcluster.init(opts) + self.assertEqual(excinfo.exception.strerror, + 'Mechanism is set to \'userpass\', but no ' + '\'username\' key found in proxy config.') + + def test_no_passwords(self): + opts = self.opts_userpass.copy() + del opts['proxy']['passwords'] + with patch('salt.proxy.esxcluster.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxcluster.init(opts) + self.assertEqual(excinfo.exception.strerror, + 'Mechanism is set to \'userpass\', but no ' + '\'passwords\' key found in proxy config.') + + def test_no_domain(self): + opts = self.opts_sspi.copy() + del opts['proxy']['domain'] + with patch('salt.proxy.esxcluster.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxcluster.init(opts) + self.assertEqual(excinfo.exception.strerror, + 'Mechanism is set to \'sspi\', but no ' + '\'domain\' key found in proxy config.') + + def test_no_principal(self): + opts = self.opts_sspi.copy() + del opts['proxy']['principal'] + with patch('salt.proxy.esxcluster.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxcluster.init(opts) + self.assertEqual(excinfo.exception.strerror, + 'Mechanism is set to \'sspi\', but no ' + '\'principal\' key found in proxy config.') + + def test_find_credentials(self): + mock_find_credentials = MagicMock(return_value=('fake_username', + 'fake_password')) + with patch('salt.proxy.esxcluster.merge', + MagicMock(return_value=self.opts_userpass['proxy'])): + with patch('salt.proxy.esxcluster.find_credentials', + mock_find_credentials): + esxcluster.init(self.opts_userpass) + mock_find_credentials.assert_called_once_with() + + def test_details_userpass(self): + mock_find_credentials = MagicMock(return_value=('fake_username', + 'fake_password')) + with patch('salt.proxy.esxcluster.merge', + MagicMock(return_value=self.opts_userpass['proxy'])): + with patch('salt.proxy.esxcluster.find_credentials', + mock_find_credentials): + esxcluster.init(self.opts_userpass) + self.assertDictEqual(esxcluster.DETAILS, + {'vcenter': 'fake_vcenter', + 'datacenter': 'fake_dc', + 'cluster': 'fake_cluster', + 'mechanism': 'userpass', + 'username': 'fake_username', + 'password': 'fake_password', + 'passwords': ['fake_password'], + 'protocol': 'fake_protocol', + 'port': 100}) + + def test_details_sspi(self): + esxcluster.init(self.opts_sspi) + self.assertDictEqual(esxcluster.DETAILS, + {'vcenter': 'fake_vcenter', + 'datacenter': 'fake_dc', + 'cluster': 'fake_cluster', + 'mechanism': 'sspi', + 'domain': 'fake_domain', + 'principal': 'fake_principal', + 'protocol': 'fake_protocol', + 'port': 100}) diff --git a/tests/unit/proxy/test_esxdatacenter.py b/tests/unit/proxy/test_esxdatacenter.py index fb44851a9f..bda93182af 100644 --- a/tests/unit/proxy/test_esxdatacenter.py +++ b/tests/unit/proxy/test_esxdatacenter.py @@ -38,7 +38,7 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): return {esxdatacenter: {'__virtual__': MagicMock(return_value='esxdatacenter'), - 'DETAILS': {}}} + 'DETAILS': {}, '__pillar__': {}}} def setUp(self): self.opts_userpass = {'proxy': {'proxytype': 'esxdatacenter', @@ -57,6 +57,22 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): 'principal': 'fake_principal', 'protocol': 'fake_protocol', 'port': 100}} + patches = (('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=self.opts_sspi['proxy'])),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def test_merge(self): + mock_pillar_proxy = MagicMock() + mock_opts_proxy = MagicMock() + mock_merge = MagicMock(return_value=self.opts_sspi['proxy']) + with patch.dict(esxdatacenter.__pillar__, + {'proxy': mock_pillar_proxy}): + with patch('salt.proxy.esxdatacenter.merge', mock_merge): + esxdatacenter.init(opts={'proxy': mock_opts_proxy}) + mock_merge.assert_called_once_with(mock_opts_proxy, mock_pillar_proxy) def test_esxdatacenter_schema(self): mock_json_validate = MagicMock() @@ -80,9 +96,11 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_no_username(self): opts = self.opts_userpass.copy() del opts['proxy']['username'] - with self.assertRaises(salt.exceptions.InvalidConfigError) as \ - excinfo: - esxdatacenter.init(opts) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxdatacenter.init(opts) self.assertEqual(excinfo.exception.strerror, 'Mechanism is set to \'userpass\', but no ' '\'username\' key found in proxy config.') @@ -90,9 +108,11 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_no_passwords(self): opts = self.opts_userpass.copy() del opts['proxy']['passwords'] - with self.assertRaises(salt.exceptions.InvalidConfigError) as \ - excinfo: - esxdatacenter.init(opts) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxdatacenter.init(opts) self.assertEqual(excinfo.exception.strerror, 'Mechanism is set to \'userpass\', but no ' '\'passwords\' key found in proxy config.') @@ -100,9 +120,11 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_no_domain(self): opts = self.opts_sspi.copy() del opts['proxy']['domain'] - with self.assertRaises(salt.exceptions.InvalidConfigError) as \ - excinfo: - esxdatacenter.init(opts) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxdatacenter.init(opts) self.assertEqual(excinfo.exception.strerror, 'Mechanism is set to \'sspi\', but no ' '\'domain\' key found in proxy config.') @@ -110,9 +132,11 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_no_principal(self): opts = self.opts_sspi.copy() del opts['proxy']['principal'] - with self.assertRaises(salt.exceptions.InvalidConfigError) as \ - excinfo: - esxdatacenter.init(opts) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=opts['proxy'])): + with self.assertRaises(salt.exceptions.InvalidConfigError) as \ + excinfo: + esxdatacenter.init(opts) self.assertEqual(excinfo.exception.strerror, 'Mechanism is set to \'sspi\', but no ' '\'principal\' key found in proxy config.') @@ -120,17 +144,21 @@ class InitTestCase(TestCase, LoaderModuleMockMixin): def test_find_credentials(self): mock_find_credentials = MagicMock(return_value=('fake_username', 'fake_password')) - with patch('salt.proxy.esxdatacenter.find_credentials', - mock_find_credentials): - esxdatacenter.init(self.opts_userpass) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=self.opts_userpass['proxy'])): + with patch('salt.proxy.esxdatacenter.find_credentials', + mock_find_credentials): + esxdatacenter.init(self.opts_userpass) mock_find_credentials.assert_called_once_with() def test_details_userpass(self): mock_find_credentials = MagicMock(return_value=('fake_username', 'fake_password')) - with patch('salt.proxy.esxdatacenter.find_credentials', - mock_find_credentials): - esxdatacenter.init(self.opts_userpass) + with patch('salt.proxy.esxdatacenter.merge', + MagicMock(return_value=self.opts_userpass['proxy'])): + with patch('salt.proxy.esxdatacenter.find_credentials', + mock_find_credentials): + esxdatacenter.init(self.opts_userpass) self.assertDictEqual(esxdatacenter.DETAILS, {'vcenter': 'fake_vcenter', 'datacenter': 'fake_dc', diff --git a/tests/unit/renderers/test_nacl.py b/tests/unit/renderers/test_nacl.py index bc55dd4e39..04aa31dd88 100644 --- a/tests/unit/renderers/test_nacl.py +++ b/tests/unit/renderers/test_nacl.py @@ -38,7 +38,7 @@ class NaclTestCase(TestCase, LoaderModuleMockMixin): secret_list = [secret] crypted_list = [crypted] - with patch.dict(nacl.__salt__, {'nacl.dec_pub': MagicMock(return_value=secret)}): + with patch.dict(nacl.__salt__, {'nacl.dec': MagicMock(return_value=secret)}): self.assertEqual(nacl._decrypt_object(secret), secret) self.assertEqual(nacl._decrypt_object(crypted), secret) self.assertEqual(nacl._decrypt_object(crypted_map), secret_map) @@ -51,5 +51,5 @@ class NaclTestCase(TestCase, LoaderModuleMockMixin): ''' secret = 'Use more salt.' crypted = 'NACL[MRN3cc+fmdxyQbz6WMF+jq1hKdU5X5BBI7OjK+atvHo1ll+w1gZ7XyWtZVfq9gK9rQaMfkDxmidJKwE0Mw==]' - with patch.dict(nacl.__salt__, {'nacl.dec_pub': MagicMock(return_value=secret)}): + with patch.dict(nacl.__salt__, {'nacl.dec': MagicMock(return_value=secret)}): self.assertEqual(nacl.render(crypted), secret) diff --git a/tests/unit/returners/test_local_cache.py b/tests/unit/returners/test_local_cache.py index ad9ff53e07..741957ffd8 100644 --- a/tests/unit/returners/test_local_cache.py +++ b/tests/unit/returners/test_local_cache.py @@ -97,7 +97,10 @@ class LocalCacheCleanOldJobsTestCase(TestCase, LoaderModuleMockMixin): local_cache.clean_old_jobs() # Get the name of the JID directory that was created to test against - jid_dir_name = jid_dir.rpartition('/')[2] + if salt.utils.is_windows(): + jid_dir_name = jid_dir.rpartition('\\')[2] + else: + jid_dir_name = jid_dir.rpartition('/')[2] # Assert the JID directory is still present to be cleaned after keep_jobs interval self.assertEqual([jid_dir_name], os.listdir(TMP_JID_DIR)) diff --git a/tests/unit/states/test_boto_cloudfront.py b/tests/unit/states/test_boto_cloudfront.py new file mode 100644 index 0000000000..dddb78d384 --- /dev/null +++ b/tests/unit/states/test_boto_cloudfront.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +''' +Unit tests for the boto_cloudfront state module. +''' +# Import Python libs +from __future__ import absolute_import +import copy +import textwrap + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import skipIf, TestCase +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch + +# Import Salt Libs +import salt.config +import salt.loader +import salt.states.boto_cloudfront as boto_cloudfront + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class BotoCloudfrontTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.states.boto_cloudfront + ''' + def setup_loader_modules(self): + utils = salt.loader.utils( + self.opts, + whitelist=['boto3', 'dictdiffer', 'yamldumper'], + context={}, + ) + return { + boto_cloudfront: { + '__utils__': utils, + } + } + + @classmethod + def setUpClass(cls): + cls.opts = salt.config.DEFAULT_MINION_OPTS + + cls.name = 'my_distribution' + cls.base_ret = {'name': cls.name, 'changes': {}} + + # Most attributes elided since there are so many required ones + cls.config = {'Enabled': True, 'HttpVersion': 'http2'} + cls.tags = {'test_tag1': 'value1'} + + @classmethod + def tearDownClass(cls): + del cls.opts + + del cls.name + del cls.base_ret + + del cls.config + del cls.tags + + def base_ret_with(self, extra_ret): + new_ret = copy.deepcopy(self.base_ret) + new_ret.update(extra_ret) + return new_ret + + def test_present_distribution_retrieval_error(self): + ''' + Test for boto_cloudfront.present when we cannot get the distribution. + ''' + mock_get = MagicMock(return_value={'error': 'get_distribution error'}) + with patch.multiple(boto_cloudfront, + __salt__={'boto_cloudfront.get_distribution': mock_get}, + __opts__={'test': False}, + ): + comment = 'Error checking distribution {0}: get_distribution error' + self.assertDictEqual( + boto_cloudfront.present(self.name, self.config, self.tags), + self.base_ret_with({ + 'result': False, + 'comment': comment.format(self.name), + }), + ) + + def test_present_from_scratch(self): + mock_get = MagicMock(return_value={'result': None}) + + with patch.multiple(boto_cloudfront, + __salt__={'boto_cloudfront.get_distribution': mock_get}, + __opts__={'test': True}, + ): + comment = 'Distribution {0} set for creation.'.format(self.name) + self.assertDictEqual( + boto_cloudfront.present(self.name, self.config, self.tags), + self.base_ret_with({ + 'result': None, + 'comment': comment, + 'pchanges': {'old': None, 'new': self.name}, + }), + ) + + mock_create_failure = MagicMock(return_value={'error': 'create error'}) + with patch.multiple(boto_cloudfront, + __salt__={ + 'boto_cloudfront.get_distribution': mock_get, + 'boto_cloudfront.create_distribution': mock_create_failure, + }, + __opts__={'test': False}, + ): + comment = 'Error creating distribution {0}: create error' + self.assertDictEqual( + boto_cloudfront.present(self.name, self.config, self.tags), + self.base_ret_with({ + 'result': False, + 'comment': comment.format(self.name), + }), + ) + + mock_create_success = MagicMock(return_value={'result': True}) + with patch.multiple(boto_cloudfront, + __salt__={ + 'boto_cloudfront.get_distribution': mock_get, + 'boto_cloudfront.create_distribution': mock_create_success, + }, + __opts__={'test': False}, + ): + comment = 'Created distribution {0}.' + self.assertDictEqual( + boto_cloudfront.present(self.name, self.config, self.tags), + self.base_ret_with({ + 'result': True, + 'comment': comment.format(self.name), + 'changes': {'old': None, 'new': self.name}, + }), + ) + + def test_present_correct_state(self): + mock_get = MagicMock(return_value={'result': { + 'distribution': {'DistributionConfig': self.config}, + 'tags': self.tags, + 'etag': 'test etag', + }}) + with patch.multiple(boto_cloudfront, + __salt__={'boto_cloudfront.get_distribution': mock_get}, + __opts__={'test': False}, + ): + comment = 'Distribution {0} has correct config.' + self.assertDictEqual( + boto_cloudfront.present(self.name, self.config, self.tags), + self.base_ret_with({ + 'result': True, + 'comment': comment.format(self.name), + }), + ) + + def test_present_update_config_and_tags(self): + mock_get = MagicMock(return_value={'result': { + 'distribution': {'DistributionConfig': { + 'Enabled': False, + 'Comment': 'to be removed', + }}, + 'tags': {'bad existing tag': 'also to be removed'}, + 'etag': 'test etag', + }}) + + diff = textwrap.dedent('''\ + --- + +++ + @@ -1,5 +1,5 @@ + config: + - Comment: to be removed + - Enabled: false + + Enabled: true + + HttpVersion: http2 + tags: + - bad existing tag: also to be removed + + test_tag1: value1 + ''') + + with patch.multiple(boto_cloudfront, + __salt__={'boto_cloudfront.get_distribution': mock_get}, + __opts__={'test': True}, + ): + header = 'Distribution {0} set for new config:'.format(self.name) + self.assertDictEqual( + boto_cloudfront.present(self.name, self.config, self.tags), + self.base_ret_with({ + 'result': None, + 'comment': '\n'.join([header, diff]), + 'pchanges': {'diff': diff}, + }), + ) + + mock_update_failure = MagicMock(return_value={'error': 'update error'}) + with patch.multiple(boto_cloudfront, + __salt__={ + 'boto_cloudfront.get_distribution': mock_get, + 'boto_cloudfront.update_distribution': mock_update_failure, + }, + __opts__={'test': False}, + ): + comment = 'Error updating distribution {0}: update error' + self.assertDictEqual( + boto_cloudfront.present(self.name, self.config, self.tags), + self.base_ret_with({ + 'result': False, + 'comment': comment.format(self.name), + }), + ) + + mock_update_success = MagicMock(return_value={'result': True}) + with patch.multiple(boto_cloudfront, + __salt__={ + 'boto_cloudfront.get_distribution': mock_get, + 'boto_cloudfront.update_distribution': mock_update_success, + }, + __opts__={'test': False}, + ): + self.assertDictEqual( + boto_cloudfront.present(self.name, self.config, self.tags), + self.base_ret_with({ + 'result': True, + 'comment': 'Updated distribution {0}.'.format(self.name), + 'changes': {'diff': diff}, + }), + ) diff --git a/tests/unit/states/test_boto_sqs.py b/tests/unit/states/test_boto_sqs.py index 4c50a5449f..80672e87f4 100644 --- a/tests/unit/states/test_boto_sqs.py +++ b/tests/unit/states/test_boto_sqs.py @@ -62,15 +62,15 @@ class BotoSqsTestCase(TestCase, LoaderModuleMockMixin): 'boto_sqs.create': mock_bool, 'boto_sqs.get_attributes': mock_attr}): with patch.dict(boto_sqs.__opts__, {'test': False}): - comt = 'Failed to create SQS queue {0}: create error'.format( + comt = ['Failed to create SQS queue {0}: create error'.format( name, - ) + )] ret = base_ret.copy() ret.update({'result': False, 'comment': comt}) self.assertDictEqual(boto_sqs.present(name), ret) with patch.dict(boto_sqs.__opts__, {'test': True}): - comt = 'SQS queue {0} is set to be created.'.format(name) + comt = ['SQS queue {0} is set to be created.'.format(name)] ret = base_ret.copy() ret.update({ 'result': None, @@ -85,17 +85,19 @@ class BotoSqsTestCase(TestCase, LoaderModuleMockMixin): -{} +DelaySeconds: 20 ''') - comt = textwrap.dedent('''\ - SQS queue mysqs present. - Attribute(s) DelaySeconds set to be updated: - ''') + diff + comt = [ + 'SQS queue mysqs present.', + 'Attribute(s) DelaySeconds set to be updated:\n{0}'.format( + diff, + ), + ] ret.update({ 'comment': comt, 'pchanges': {'attributes': {'diff': diff}}, }) self.assertDictEqual(boto_sqs.present(name, attributes), ret) - comt = ('SQS queue mysqs present.') + comt = ['SQS queue mysqs present.'] ret = base_ret.copy() ret.update({'result': True, 'comment': comt}) self.assertDictEqual(boto_sqs.present(name), ret) diff --git a/tests/unit/states/test_docker_image.py b/tests/unit/states/test_docker_image.py index 4d94c2e239..e96dbc9f9d 100644 --- a/tests/unit/states/test_docker_image.py +++ b/tests/unit/states/test_docker_image.py @@ -10,14 +10,13 @@ from __future__ import absolute_import from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import skipIf, TestCase from tests.support.mock import ( - Mock, + MagicMock, NO_MOCK, NO_MOCK_REASON, patch ) # Import Salt Libs -from salt.exceptions import CommandExecutionError import salt.modules.dockermod as docker_mod import salt.states.docker_image as docker_state @@ -50,21 +49,19 @@ class DockerImageTestCase(TestCase, LoaderModuleMockMixin): if ``image:latest`` is already downloaded locally the state should not report changes. ''' - docker_inspect_image = Mock( - return_value={'Id': 'abcdefghijk'}) - docker_pull = Mock( + docker_inspect_image = MagicMock(return_value={'Id': 'abcdefghijkl'}) + docker_pull = MagicMock( return_value={'Layers': - {'Already_Pulled': ['abcdefghijk'], + {'Already_Pulled': ['abcdefghijkl'], 'Pulled': []}, 'Status': 'Image is up to date for image:latest', 'Time_Elapsed': 1.1}) - docker_list_tags = Mock( - return_value=['image:latest'] - ) + docker_list_tags = MagicMock(return_value=['image:latest']) + docker_resolve_tag = MagicMock(return_value='image:latest') __salt__ = {'docker.list_tags': docker_list_tags, 'docker.pull': docker_pull, 'docker.inspect_image': docker_inspect_image, - } + 'docker.resolve_tag': docker_resolve_tag} with patch.dict(docker_state.__dict__, {'__salt__': __salt__}): ret = docker_state.present('image:latest', force=True) @@ -89,29 +86,24 @@ class DockerImageTestCase(TestCase, LoaderModuleMockMixin): if ``image:latest`` is not downloaded and force is true should pull a new image successfuly. ''' - docker_inspect_image = Mock( - side_effect=CommandExecutionError( - 'Error 404: No such image/container: image:latest')) - docker_pull = Mock( + docker_inspect_image = MagicMock(return_value={'Id': '1234567890ab'}) + docker_pull = MagicMock( return_value={'Layers': - {'Already_Pulled': ['abcdefghijk'], - 'Pulled': ['abcdefghijk']}, - 'Status': "Image 'image:latest' was pulled", - 'Time_Elapsed': 1.1}) - docker_list_tags = Mock( - side_effect=[[], ['image:latest']] - ) + {'Pulled': ['abcdefghijkl']}, + 'Status': "Image 'image:latest' was pulled", + 'Time_Elapsed': 1.1}) + docker_list_tags = MagicMock(side_effect=[[], ['image:latest']]) + docker_resolve_tag = MagicMock(return_value='image:latest') __salt__ = {'docker.list_tags': docker_list_tags, 'docker.pull': docker_pull, 'docker.inspect_image': docker_inspect_image, - } + 'docker.resolve_tag': docker_resolve_tag} with patch.dict(docker_state.__dict__, {'__salt__': __salt__}): ret = docker_state.present('image:latest', force=True) self.assertEqual(ret, {'changes': { - 'Layers': {'Already_Pulled': ['abcdefghijk'], - 'Pulled': ['abcdefghijk']}, + 'Layers': {'Pulled': ['abcdefghijkl']}, 'Status': "Image 'image:latest' was pulled", 'Time_Elapsed': 1.1}, 'result': True, diff --git a/tests/unit/states/test_file.py b/tests/unit/states/test_file.py index bac6594652..7aa2c0d0f7 100644 --- a/tests/unit/states/test_file.py +++ b/tests/unit/states/test_file.py @@ -56,7 +56,8 @@ class TestFileState(TestCase, LoaderModuleMockMixin): }, '__opts__': {'test': False, 'cachedir': ''}, '__instance_id__': '', - '__low__': {} + '__low__': {}, + '__utils__': {}, } } @@ -1181,7 +1182,7 @@ class TestFileState(TestCase, LoaderModuleMockMixin): ret.update({'name': name}) with patch.object(salt.utils.files, 'fopen', MagicMock(mock_open(read_data=''))): - with patch.object(salt.utils, 'istextfile', mock_f): + with patch.dict(filestate.__utils__, {'files.is_text_file': mock_f}): with patch.dict(filestate.__opts__, {'test': True}): change = {'diff': 'Replace binary file'} comt = ('File {0} is set to be updated' diff --git a/tests/unit/states/test_mdadm.py b/tests/unit/states/test_mdadm.py index 9095a1926d..3e0cbdf7ea 100644 --- a/tests/unit/states/test_mdadm.py +++ b/tests/unit/states/test_mdadm.py @@ -32,41 +32,133 @@ class MdadmTestCase(TestCase, LoaderModuleMockMixin): ''' Test to verify that the raid is present ''' - ret = [{'changes': {}, 'comment': 'Raid salt already present', + ret = [{'changes': {}, 'comment': 'Raid salt already present.', 'name': 'salt', 'result': True}, {'changes': {}, - 'comment': "Devices are a mix of RAID constituents" - " (['dev0']) and non-RAID-constituents(['dev1']).", + 'comment': "Devices are a mix of RAID constituents with multiple MD_UUIDs:" + " ['6be5fc45:05802bba:1c2d6722:666f0e03', 'ffffffff:ffffffff:ffffffff:ffffffff'].", 'name': 'salt', 'result': False}, {'changes': {}, 'comment': 'Raid will be created with: True', 'name': 'salt', 'result': None}, {'changes': {}, 'comment': 'Raid salt failed to be created.', + 'name': 'salt', 'result': False}, + {'changes': {'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}, 'comment': 'Raid salt created.', + 'name': 'salt', 'result': True}, + {'changes': {'added': ['dev1'], 'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}, + 'comment': 'Raid salt assembled. Added new device dev1 to salt.\n', + 'name': 'salt', 'result': True}, + {'changes': {'added': ['dev1']}, + 'comment': 'Raid salt already present. Added new device dev1 to salt.\n', + 'name': 'salt', 'result': True}, + {'changes': {}, 'comment': 'Raid salt failed to be assembled.', 'name': 'salt', 'result': False}] - mock = MagicMock(side_effect=[{'salt': True}, {'salt': False}, - {'salt': False}, {'salt': False}, - {'salt': False}]) - with patch.dict(mdadm.__salt__, {'raid.list': mock}): - self.assertEqual(mdadm.present("salt", 5, "dev0"), ret[0]) + mock_raid_list_exists = MagicMock(return_value={'salt': {'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}}) + mock_raid_list_missing = MagicMock(return_value={}) - mock = MagicMock(side_effect=[0, 1]) - with patch.dict(mdadm.__salt__, {'cmd.retcode': mock}): - self.assertDictEqual(mdadm.present("salt", 5, - ["dev0", "dev1"]), - ret[1]) + mock_file_access_ok = MagicMock(return_value=True) - mock = MagicMock(return_value=True) - with patch.dict(mdadm.__salt__, {'cmd.retcode': mock}): - with patch.dict(mdadm.__opts__, {'test': True}): - with patch.dict(mdadm.__salt__, {'raid.create': mock}): - self.assertDictEqual(mdadm.present("salt", 5, "dev0"), - ret[2]) + mock_raid_examine_ok = MagicMock(return_value={'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}) + mock_raid_examine_missing = MagicMock(return_value={}) - with patch.dict(mdadm.__opts__, {'test': False}): - with patch.dict(mdadm.__salt__, {'raid.create': mock}): - self.assertDictEqual(mdadm.present("salt", 5, "dev0"), - ret[3]) + mock_raid_create_success = MagicMock(return_value=True) + mock_raid_create_fail = MagicMock(return_value=False) + + mock_raid_assemble_success = MagicMock(return_value=True) + mock_raid_assemble_fail = MagicMock(return_value=False) + + mock_raid_add_success = MagicMock(return_value=True) + + mock_raid_save_config = MagicMock(return_value=True) + + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_exists, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_ok + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertEqual(mdadm.present("salt", 5, "dev0"), ret[0]) + + mock_raid_examine_mixed = MagicMock(side_effect=[ + {'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}, {'MD_UUID': 'ffffffff:ffffffff:ffffffff:ffffffff'}, + ]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_missing, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_mixed + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertEqual(mdadm.present("salt", 5, ["dev0", "dev1"]), ret[1]) + + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_missing, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_missing, + 'raid.create': mock_raid_create_success + }): + with patch.dict(mdadm.__opts__, {'test': True}): + self.assertDictEqual(mdadm.present("salt", 5, "dev0"), ret[2]) + + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_missing, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_missing, + 'raid.create': mock_raid_create_fail + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, "dev0"), ret[3]) + + mock_raid_list_create = MagicMock(side_effect=[{}, {'salt': {'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}}]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_create, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_missing, + 'raid.create': mock_raid_create_success, + 'raid.save_config': mock_raid_save_config + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, "dev0"), ret[4]) + + mock_raid_examine_replaced = MagicMock(side_effect=[ + {'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}, {}, + ]) + mock_raid_list_create = MagicMock(side_effect=[{}, {'salt': {'uuid': '6be5fc45:05802bba:1c2d6722:666f0e03'}}]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_create, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_replaced, + 'raid.assemble': mock_raid_assemble_success, + 'raid.add': mock_raid_add_success, + 'raid.save_config': mock_raid_save_config + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, ["dev0", "dev1"]), ret[5]) + + mock_raid_examine_replaced = MagicMock(side_effect=[ + {'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}, {}, + ]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_exists, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_replaced, + 'raid.add': mock_raid_add_success, + 'raid.save_config': mock_raid_save_config + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, ["dev0", "dev1"]), ret[6]) + + mock_raid_examine_replaced = MagicMock(side_effect=[ + {'MD_UUID': '6be5fc45:05802bba:1c2d6722:666f0e03'}, {}, + ]) + with patch.dict(mdadm.__salt__, { + 'raid.list': mock_raid_list_missing, + 'file.access': mock_file_access_ok, + 'raid.examine': mock_raid_examine_replaced, + 'raid.assemble': mock_raid_assemble_fail, + }): + with patch.dict(mdadm.__opts__, {'test': False}): + self.assertDictEqual(mdadm.present("salt", 5, ["dev0", "dev1"]), ret[7]) def test_absent(self): ''' diff --git a/tests/unit/states/test_mount.py b/tests/unit/states/test_mount.py index 65173ea698..1e1886001f 100644 --- a/tests/unit/states/test_mount.py +++ b/tests/unit/states/test_mount.py @@ -62,6 +62,8 @@ class MountTestCase(TestCase, LoaderModuleMockMixin): mock_str = MagicMock(return_value='salt') mock_user = MagicMock(return_value={'uid': 510}) mock_group = MagicMock(return_value={'gid': 100}) + mock_read_cache = MagicMock(return_value={}) + mock_write_cache = MagicMock(return_value=True) umount1 = ("Forced unmount because devices don't match. " "Wanted: /dev/sdb6, current: /dev/sdb5, /dev/sdb5") with patch.dict(mount.__grains__, {'os': 'Darwin'}): @@ -163,6 +165,8 @@ class MountTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(mount.__salt__, {'mount.active': mock_mnt, 'mount.mount': mock_str, 'mount.umount': mock_f, + 'mount.read_mount_cache': mock_read_cache, + 'mount.write_mount_cache': mock_write_cache, 'mount.set_fstab': mock, 'user.info': mock_user, 'group.info': mock_group}): diff --git a/tests/unit/states/test_saltmod.py b/tests/unit/states/test_saltmod.py index ecfd891476..a884e331d0 100644 --- a/tests/unit/states/test_saltmod.py +++ b/tests/unit/states/test_saltmod.py @@ -20,6 +20,8 @@ from tests.support.mock import ( ) # Import Salt Libs +import salt.config +import salt.loader import salt.utils.jid import salt.utils.event import salt.states.saltmod as saltmod @@ -31,6 +33,10 @@ class SaltmodTestCase(TestCase, LoaderModuleMockMixin): Test cases for salt.states.saltmod ''' def setup_loader_modules(self): + utils = salt.loader.utils( + salt.config.DEFAULT_MINION_OPTS, + whitelist=['state'] + ) return { saltmod: { '__env__': 'base', @@ -41,7 +47,8 @@ class SaltmodTestCase(TestCase, LoaderModuleMockMixin): 'transport': 'tcp' }, '__salt__': {'saltutil.cmd': MagicMock()}, - '__orchestration_jid__': salt.utils.jid.gen_jid() + '__orchestration_jid__': salt.utils.jid.gen_jid({}), + '__utils__': utils, } } @@ -251,8 +258,8 @@ class SaltmodTestCase(TestCase, LoaderModuleMockMixin): ''' name = 'state' - ret = {'changes': True, 'name': 'state', 'result': True, - 'comment': 'Runner function \'state\' executed.', + ret = {'changes': {}, 'name': 'state', 'result': True, + 'comment': 'Runner function \'state\' executed with return True.', '__orchestration__': True} runner_mock = MagicMock(return_value={'return': True}) @@ -267,8 +274,8 @@ class SaltmodTestCase(TestCase, LoaderModuleMockMixin): ''' name = 'state' - ret = {'changes': True, 'name': 'state', 'result': True, - 'comment': 'Wheel function \'state\' executed.', + ret = {'changes': {}, 'name': 'state', 'result': True, + 'comment': 'Wheel function \'state\' executed with return True.', '__orchestration__': True} wheel_mock = MagicMock(return_value={'return': True}) @@ -291,7 +298,7 @@ class StatemodTests(TestCase, LoaderModuleMockMixin): 'extension_modules': os.path.join(self.tmp_cachedir, 'extmods'), }, '__salt__': {'saltutil.cmd': MagicMock()}, - '__orchestration_jid__': salt.utils.jid.gen_jid() + '__orchestration_jid__': salt.utils.jid.gen_jid({}) } } diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py index 625d203eba..d08f9e71b1 100644 --- a/tests/unit/test_auth.py +++ b/tests/unit/test_auth.py @@ -49,7 +49,8 @@ class LoadAuthTestCase(TestCase): self.assertEqual(ret, '', "Did not bail when the auth loader didn't have the auth type.") # Test a case with valid params - with patch('salt.utils.arg_lookup', MagicMock(return_value={'args': ['username', 'password']})) as format_call_mock: + with patch('salt.utils.args.arg_lookup', + MagicMock(return_value={'args': ['username', 'password']})) as format_call_mock: expected_ret = call('fake_func_str') ret = self.lauth.load_name(valid_eauth_load) format_call_mock.assert_has_calls((expected_ret,), any_order=True) @@ -154,7 +155,8 @@ class MasterACLTestCase(ModuleCase): Test to ensure a simple name can auth against a given function. This tests to ensure test_user can access test.ping but *not* sys.doc ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='some_minions')): + _check_minions_return = {'minions': ['some_minions'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): # Can we access test.ping? self.clear.publish(self.valid_clear_load) self.assertEqual(self.fire_event_mock.call_args[0][0]['fun'], 'test.ping') @@ -169,7 +171,8 @@ class MasterACLTestCase(ModuleCase): ''' Tests to ensure test_group can access test.echo but *not* sys.doc ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='some_minions')): + _check_minions_return = {'minions': ['some_minions'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): self.valid_clear_load['kwargs']['user'] = 'new_user' self.valid_clear_load['fun'] = 'test.echo' self.valid_clear_load['arg'] = 'hello' @@ -235,7 +238,8 @@ class MasterACLTestCase(ModuleCase): requested_tgt = 'minion_glob1' self.valid_clear_load['tgt'] = requested_tgt self.valid_clear_load['fun'] = requested_function - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=['minion_glob1'])): # Assume that there is a listening minion match + _check_minions_return = {'minions': ['minion_glob1'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): # Assume that there is a listening minion match self.clear.publish(self.valid_clear_load) self.assertTrue(self.fire_event_mock.called, 'Did not fire {0} for minion tgt {1}'.format(requested_function, requested_tgt)) self.assertEqual(self.fire_event_mock.call_args[0][0]['fun'], requested_function, 'Did not fire {0} for minion glob'.format(requested_function)) @@ -261,7 +265,8 @@ class MasterACLTestCase(ModuleCase): minion1: - test.empty: ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='minion1')): + _check_minions_return = {'minions': ['minion1'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): self.valid_clear_load['kwargs'].update({'username': 'test_user_func'}) self.valid_clear_load.update({'user': 'test_user_func', 'tgt': 'minion1', @@ -281,7 +286,8 @@ class MasterACLTestCase(ModuleCase): - 'TEST' - 'TEST.*' ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='minion1')): + _check_minions_return = {'minions': ['minion1'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): self.valid_clear_load['kwargs'].update({'username': 'test_user_func'}) self.valid_clear_load.update({'user': 'test_user_func', 'tgt': 'minion1', @@ -301,7 +307,8 @@ class MasterACLTestCase(ModuleCase): - 'TEST' - 'TEST.*' ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='minion1')): + _check_minions_return = {'minions': ['minion1'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): self.valid_clear_load['kwargs'].update({'username': 'test_user_func'}) self.valid_clear_load.update({'user': 'test_user_func', 'tgt': 'minion1', @@ -326,7 +333,8 @@ class MasterACLTestCase(ModuleCase): - 'TEST' - 'TEST.*' ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='minion1')): + _check_minions_return = {'minions': ['minion1'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): self.valid_clear_load['kwargs'].update({'username': 'test_user_func'}) # Wrong last arg self.valid_clear_load.update({'user': 'test_user_func', @@ -358,7 +366,8 @@ class MasterACLTestCase(ModuleCase): kwargs: text: 'KWMSG:.*' ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='some_minions')): + _check_minions_return = {'minions': ['some_minions'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): self.valid_clear_load['kwargs'].update({'username': 'test_user_func'}) self.valid_clear_load.update({'user': 'test_user_func', 'tgt': '*', @@ -380,7 +389,8 @@ class MasterACLTestCase(ModuleCase): kwargs: text: 'KWMSG:.*' ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='some_minions')): + _check_minions_return = {'minions': ['some_minions'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): self.valid_clear_load['kwargs'].update({'username': 'test_user_func'}) self.valid_clear_load.update({'user': 'test_user_func', 'tgt': '*', @@ -433,7 +443,8 @@ class MasterACLTestCase(ModuleCase): 'kwa': 'kwa.*' 'kwb': 'kwb' ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='some_minions')): + _check_minions_return = {'minions': ['some_minions'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): self.valid_clear_load['kwargs'].update({'username': 'test_user_func'}) self.valid_clear_load.update({'user': 'test_user_func', 'tgt': '*', @@ -463,7 +474,8 @@ class MasterACLTestCase(ModuleCase): 'kwa': 'kwa.*' 'kwb': 'kwb' ''' - with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value='some_minions')): + _check_minions_return = {'minions': ['some_minions'], 'missing': []} + with patch('salt.utils.minions.CkMinions.check_minions', MagicMock(return_value=_check_minions_return)): self.valid_clear_load['kwargs'].update({'username': 'test_user_func'}) self.valid_clear_load.update({'user': 'test_user_func', 'tgt': '*', diff --git a/tests/unit/test_master.py b/tests/unit/test_master.py new file mode 100644 index 0000000000..c663d2c45c --- /dev/null +++ b/tests/unit/test_master.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- + +# Import Python libs +from __future__ import absolute_import + +# Import Salt libs +import salt.config +import salt.master + +# Import Salt Testing Libs +from tests.support.unit import TestCase +from tests.support.mock import ( + patch, + MagicMock, +) + + +class ClearFuncsTestCase(TestCase): + ''' + TestCase for salt.master.ClearFuncs class + ''' + + def setUp(self): + opts = salt.config.master_config(None) + self.clear_funcs = salt.master.ClearFuncs(opts, {}) + + def test_runner_token_not_authenticated(self): + ''' + Asserts that a TokenAuthenticationError is returned when the token can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'TokenAuthenticationError', + u'message': u'Authentication failure of type "token" occurred.'}} + ret = self.clear_funcs.runner({u'token': u'asdfasdfasdfasdf'}) + self.assertDictEqual(mock_ret, ret) + + def test_runner_token_authorization_error(self): + ''' + Asserts that a TokenAuthenticationError is returned when the token authenticates, but is + not authorized. + ''' + token = u'asdfasdfasdfasdf' + clear_load = {u'token': token, u'fun': u'test.arg'} + mock_token = {u'token': token, u'eauth': u'foo', u'name': u'test'} + mock_ret = {u'error': {u'name': u'TokenAuthenticationError', + u'message': u'Authentication failure of type "token" occurred ' + u'for user test.'}} + + with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.clear_funcs.runner(clear_load) + + self.assertDictEqual(mock_ret, ret) + + def test_runner_token_salt_invocation_error(self): + ''' + Asserts that a SaltInvocationError is returned when the token authenticates, but the + command is malformed. + ''' + token = u'asdfasdfasdfasdf' + clear_load = {u'token': token, u'fun': u'badtestarg'} + mock_token = {u'token': token, u'eauth': u'foo', u'name': u'test'} + mock_ret = {u'error': {u'name': u'SaltInvocationError', + u'message': u'A command invocation error occurred: Check syntax.'}} + + with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.clear_funcs.runner(clear_load) + + self.assertDictEqual(mock_ret, ret) + + def test_runner_eauth_not_authenticated(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'EauthAuthenticationError', + u'message': u'Authentication failure of type "eauth" occurred for ' + u'user UNKNOWN.'}} + ret = self.clear_funcs.runner({u'eauth': u'foo'}) + self.assertDictEqual(mock_ret, ret) + + def test_runner_eauth_authorization_error(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user authenticates, but is + not authorized. + ''' + clear_load = {u'eauth': u'foo', u'username': u'test', u'fun': u'test.arg'} + mock_ret = {u'error': {u'name': u'EauthAuthenticationError', + u'message': u'Authentication failure of type "eauth" occurred for ' + u'user test.'}} + with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.clear_funcs.runner(clear_load) + + self.assertDictEqual(mock_ret, ret) + + def test_runner_eauth_salt_invocation_errpr(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user authenticates, but the + command is malformed. + ''' + clear_load = {u'eauth': u'foo', u'username': u'test', u'fun': u'bad.test.arg.func'} + mock_ret = {u'error': {u'name': u'SaltInvocationError', + u'message': u'A command invocation error occurred: Check syntax.'}} + with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.clear_funcs.runner(clear_load) + + self.assertDictEqual(mock_ret, ret) + + def test_runner_user_not_authenticated(self): + ''' + Asserts that an UserAuthenticationError is returned when the user can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'UserAuthenticationError', + u'message': u'Authentication failure of type "user" occurred'}} + ret = self.clear_funcs.runner({}) + self.assertDictEqual(mock_ret, ret) + + def test_wheel_token_not_authenticated(self): + ''' + Asserts that a TokenAuthenticationError is returned when the token can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'TokenAuthenticationError', + u'message': u'Authentication failure of type "token" occurred.'}} + ret = self.clear_funcs.wheel({u'token': u'asdfasdfasdfasdf'}) + self.assertDictEqual(mock_ret, ret) + + def test_wheel_token_authorization_error(self): + ''' + Asserts that a TokenAuthenticationError is returned when the token authenticates, but is + not authorized. + ''' + token = u'asdfasdfasdfasdf' + clear_load = {u'token': token, u'fun': u'test.arg'} + mock_token = {u'token': token, u'eauth': u'foo', u'name': u'test'} + mock_ret = {u'error': {u'name': u'TokenAuthenticationError', + u'message': u'Authentication failure of type "token" occurred ' + u'for user test.'}} + + with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.clear_funcs.wheel(clear_load) + + self.assertDictEqual(mock_ret, ret) + + def test_wheel_token_salt_invocation_error(self): + ''' + Asserts that a SaltInvocationError is returned when the token authenticates, but the + command is malformed. + ''' + token = u'asdfasdfasdfasdf' + clear_load = {u'token': token, u'fun': u'badtestarg'} + mock_token = {u'token': token, u'eauth': u'foo', u'name': u'test'} + mock_ret = {u'error': {u'name': u'SaltInvocationError', + u'message': u'A command invocation error occurred: Check syntax.'}} + + with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.clear_funcs.wheel(clear_load) + + self.assertDictEqual(mock_ret, ret) + + def test_wheel_eauth_not_authenticated(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'EauthAuthenticationError', + u'message': u'Authentication failure of type "eauth" occurred for ' + u'user UNKNOWN.'}} + ret = self.clear_funcs.wheel({u'eauth': u'foo'}) + self.assertDictEqual(mock_ret, ret) + + def test_wheel_eauth_authorization_error(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user authenticates, but is + not authorized. + ''' + clear_load = {u'eauth': u'foo', u'username': u'test', u'fun': u'test.arg'} + mock_ret = {u'error': {u'name': u'EauthAuthenticationError', + u'message': u'Authentication failure of type "eauth" occurred for ' + u'user test.'}} + with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.clear_funcs.wheel(clear_load) + + self.assertDictEqual(mock_ret, ret) + + def test_wheel_eauth_salt_invocation_errpr(self): + ''' + Asserts that an EauthAuthenticationError is returned when the user authenticates, but the + command is malformed. + ''' + clear_load = {u'eauth': u'foo', u'username': u'test', u'fun': u'bad.test.arg.func'} + mock_ret = {u'error': {u'name': u'SaltInvocationError', + u'message': u'A command invocation error occurred: Check syntax.'}} + with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + ret = self.clear_funcs.wheel(clear_load) + + self.assertDictEqual(mock_ret, ret) + + def test_wheel_user_not_authenticated(self): + ''' + Asserts that an UserAuthenticationError is returned when the user can't authenticate. + ''' + mock_ret = {u'error': {u'name': u'UserAuthenticationError', + u'message': u'Authentication failure of type "user" occurred'}} + ret = self.clear_funcs.wheel({}) + self.assertDictEqual(mock_ret, ret) diff --git a/tests/unit/test_pillar.py b/tests/unit/test_pillar.py index 41a5f4efdc..4479164232 100644 --- a/tests/unit/test_pillar.py +++ b/tests/unit/test_pillar.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ''' :codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)` + :codeauthor: :email:`Alexandru Bleotu (alexandru.bleotu@morganstanley.com)` tests.unit.pillar_test @@ -19,6 +20,7 @@ from tests.support.paths import TMP # Import salt libs import salt.pillar import salt.utils.stringutils +import salt.exceptions @skipIf(NO_MOCK, NO_MOCK_REASON) @@ -56,6 +58,244 @@ class PillarTestCase(TestCase): self.assertEqual(pillar.opts['environment'], 'dev') self.assertEqual(pillar.opts['pillarenv'], 'dev') + def test_ext_pillar_no_extra_minion_data_val_dict(self): + opts = { + 'renderer': 'json', + 'renderer_blacklist': [], + 'renderer_whitelist': [], + 'state_top': '', + 'pillar_roots': { + 'dev': [], + 'base': [] + }, + 'file_roots': { + 'dev': [], + 'base': [] + }, + 'extension_modules': '', + 'pillarenv_from_saltenv': True + } + mock_ext_pillar_func = MagicMock() + with patch('salt.loader.pillars', + MagicMock(return_value={'fake_ext_pillar': + mock_ext_pillar_func})): + pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'dev') + # ext pillar function doesn't have the extra_minion_data arg + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=[]))): + pillar._external_pillar_data('fake_pillar', {'arg': 'foo'}, + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with('mocked-minion', + 'fake_pillar', + arg='foo') + # ext pillar function has the extra_minion_data arg + mock_ext_pillar_func.reset_mock() + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=['extra_minion_data']))): + pillar._external_pillar_data('fake_pillar', {'arg': 'foo'}, + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with('mocked-minion', + 'fake_pillar', + arg='foo') + + def test_ext_pillar_no_extra_minion_data_val_list(self): + opts = { + 'renderer': 'json', + 'renderer_blacklist': [], + 'renderer_whitelist': [], + 'state_top': '', + 'pillar_roots': { + 'dev': [], + 'base': [] + }, + 'file_roots': { + 'dev': [], + 'base': [] + }, + 'extension_modules': '', + 'pillarenv_from_saltenv': True + } + mock_ext_pillar_func = MagicMock() + with patch('salt.loader.pillars', + MagicMock(return_value={'fake_ext_pillar': + mock_ext_pillar_func})): + pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'dev') + # ext pillar function doesn't have the extra_minion_data arg + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=[]))): + pillar._external_pillar_data('fake_pillar', ['foo'], + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with('mocked-minion', + 'fake_pillar', + 'foo') + # ext pillar function has the extra_minion_data arg + mock_ext_pillar_func.reset_mock() + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=['extra_minion_data']))): + pillar._external_pillar_data('fake_pillar', ['foo'], + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with('mocked-minion', + 'fake_pillar', + 'foo') + + def test_ext_pillar_no_extra_minion_data_val_elem(self): + opts = { + 'renderer': 'json', + 'renderer_blacklist': [], + 'renderer_whitelist': [], + 'state_top': '', + 'pillar_roots': { + 'dev': [], + 'base': [] + }, + 'file_roots': { + 'dev': [], + 'base': [] + }, + 'extension_modules': '', + 'pillarenv_from_saltenv': True + } + mock_ext_pillar_func = MagicMock() + with patch('salt.loader.pillars', + MagicMock(return_value={'fake_ext_pillar': + mock_ext_pillar_func})): + pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'dev') + # ext pillar function doesn't have the extra_minion_data arg + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=[]))): + pillar._external_pillar_data('fake_pillar', 'fake_val', + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with('mocked-minion', + 'fake_pillar', 'fake_val') + # ext pillar function has the extra_minion_data arg + mock_ext_pillar_func.reset_mock() + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=['extra_minion_data']))): + pillar._external_pillar_data('fake_pillar', 'fake_val', + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with('mocked-minion', + 'fake_pillar', 'fake_val') + + def test_ext_pillar_with_extra_minion_data_val_dict(self): + opts = { + 'renderer': 'json', + 'renderer_blacklist': [], + 'renderer_whitelist': [], + 'state_top': '', + 'pillar_roots': { + 'dev': [], + 'base': [] + }, + 'file_roots': { + 'dev': [], + 'base': [] + }, + 'extension_modules': '', + 'pillarenv_from_saltenv': True + } + mock_ext_pillar_func = MagicMock() + with patch('salt.loader.pillars', + MagicMock(return_value={'fake_ext_pillar': + mock_ext_pillar_func})): + pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'dev', + extra_minion_data={'fake_key': 'foo'}) + # ext pillar function doesn't have the extra_minion_data arg + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=[]))): + pillar._external_pillar_data('fake_pillar', {'arg': 'foo'}, + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with( + 'mocked-minion', 'fake_pillar', arg='foo') + # ext pillar function has the extra_minion_data arg + mock_ext_pillar_func.reset_mock() + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=['extra_minion_data']))): + pillar._external_pillar_data('fake_pillar', {'arg': 'foo'}, + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with( + 'mocked-minion', 'fake_pillar', arg='foo', + extra_minion_data={'fake_key': 'foo'}) + + def test_ext_pillar_with_extra_minion_data_val_list(self): + opts = { + 'renderer': 'json', + 'renderer_blacklist': [], + 'renderer_whitelist': [], + 'state_top': '', + 'pillar_roots': { + 'dev': [], + 'base': [] + }, + 'file_roots': { + 'dev': [], + 'base': [] + }, + 'extension_modules': '', + 'pillarenv_from_saltenv': True + } + mock_ext_pillar_func = MagicMock() + with patch('salt.loader.pillars', + MagicMock(return_value={'fake_ext_pillar': + mock_ext_pillar_func})): + pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'dev', + extra_minion_data={'fake_key': 'foo'}) + # ext pillar function doesn't have the extra_minion_data arg + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=[]))): + pillar._external_pillar_data('fake_pillar', ['bar'], + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with( + 'mocked-minion', 'fake_pillar', 'bar') + # ext pillar function has the extra_minion_data arg + mock_ext_pillar_func.reset_mock() + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=['extra_minion_data']))): + pillar._external_pillar_data('fake_pillar', ['bar'], + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with( + 'mocked-minion', 'fake_pillar', 'bar', + extra_minion_data={'fake_key': 'foo'}) + + def test_ext_pillar_with_extra_minion_data_val_elem(self): + opts = { + 'renderer': 'json', + 'renderer_blacklist': [], + 'renderer_whitelist': [], + 'state_top': '', + 'pillar_roots': { + 'dev': [], + 'base': [] + }, + 'file_roots': { + 'dev': [], + 'base': [] + }, + 'extension_modules': '', + 'pillarenv_from_saltenv': True + } + mock_ext_pillar_func = MagicMock() + with patch('salt.loader.pillars', + MagicMock(return_value={'fake_ext_pillar': + mock_ext_pillar_func})): + pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'dev', + extra_minion_data={'fake_key': 'foo'}) + # ext pillar function doesn't have the extra_minion_data arg + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=[]))): + pillar._external_pillar_data('fake_pillar', 'bar', + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with( + 'mocked-minion', 'fake_pillar', 'bar') + # ext pillar function has the extra_minion_data arg + mock_ext_pillar_func.reset_mock() + with patch('salt.utils.args.get_function_argspec', + MagicMock(return_value=MagicMock(args=['extra_minion_data']))): + pillar._external_pillar_data('fake_pillar', 'bar', + 'fake_ext_pillar') + mock_ext_pillar_func.assert_called_once_with( + 'mocked-minion', 'fake_pillar', 'bar', + extra_minion_data={'fake_key': 'foo'}) + def test_malformed_pillar_sls(self): with patch('salt.pillar.compile_template') as compile_template: opts = { @@ -318,3 +558,174 @@ p2: }[sls] client.get_state.side_effect = get_state + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@patch('salt.transport.Channel.factory', MagicMock()) +class RemotePillarTestCase(TestCase): + ''' + Tests for instantiating a RemotePillar in salt.pillar + ''' + def setUp(self): + self.grains = {} + + def tearDown(self): + for attr in ('grains',): + try: + delattr(self, attr) + except AttributeError: + continue + + def test_get_opts_in_pillar_override_call(self): + mock_get_extra_minion_data = MagicMock(return_value={}) + with patch( + 'salt.pillar.RemotePillarMixin.get_ext_pillar_extra_minion_data', + mock_get_extra_minion_data): + + salt.pillar.RemotePillar({}, self.grains, 'mocked-minion', 'dev') + mock_get_extra_minion_data.assert_called_once_with( + {'environment': 'dev'}) + + def test_multiple_keys_in_opts_added_to_pillar(self): + opts = { + 'renderer': 'json', + 'path_to_add': 'fake_data', + 'path_to_add2': {'fake_data2': ['fake_data3', 'fake_data4']}, + 'pass_to_ext_pillars': ['path_to_add', 'path_to_add2'] + } + pillar = salt.pillar.RemotePillar(opts, self.grains, + 'mocked-minion', 'dev') + self.assertEqual(pillar.extra_minion_data, + {'path_to_add': 'fake_data', + 'path_to_add2': {'fake_data2': ['fake_data3', + 'fake_data4']}}) + + def test_subkey_in_opts_added_to_pillar(self): + opts = { + 'renderer': 'json', + 'path_to_add': 'fake_data', + 'path_to_add2': {'fake_data5': 'fake_data6', + 'fake_data2': ['fake_data3', 'fake_data4']}, + 'pass_to_ext_pillars': ['path_to_add2:fake_data5'] + } + pillar = salt.pillar.RemotePillar(opts, self.grains, + 'mocked-minion', 'dev') + self.assertEqual(pillar.extra_minion_data, + {'path_to_add2': {'fake_data5': 'fake_data6'}}) + + def test_non_existent_leaf_opt_in_add_to_pillar(self): + opts = { + 'renderer': 'json', + 'path_to_add': 'fake_data', + 'path_to_add2': {'fake_data5': 'fake_data6', + 'fake_data2': ['fake_data3', 'fake_data4']}, + 'pass_to_ext_pillars': ['path_to_add2:fake_data_non_exist'] + } + pillar = salt.pillar.RemotePillar(opts, self.grains, + 'mocked-minion', 'dev') + self.assertEqual(pillar.pillar_override, {}) + + def test_non_existent_intermediate_opt_in_add_to_pillar(self): + opts = { + 'renderer': 'json', + 'path_to_add': 'fake_data', + 'path_to_add2': {'fake_data5': 'fake_data6', + 'fake_data2': ['fake_data3', 'fake_data4']}, + 'pass_to_ext_pillars': ['path_to_add_no_exist'] + } + pillar = salt.pillar.RemotePillar(opts, self.grains, + 'mocked-minion', 'dev') + self.assertEqual(pillar.pillar_override, {}) + + def test_malformed_add_to_pillar(self): + opts = { + 'renderer': 'json', + 'path_to_add': 'fake_data', + 'path_to_add2': {'fake_data5': 'fake_data6', + 'fake_data2': ['fake_data3', 'fake_data4']}, + 'pass_to_ext_pillars': MagicMock() + } + with self.assertRaises(salt.exceptions.SaltClientError) as excinfo: + salt.pillar.RemotePillar(opts, self.grains, 'mocked-minion', 'dev') + self.assertEqual(excinfo.exception.strerror, + '\'pass_to_ext_pillars\' config is malformed.') + + def test_pillar_send_extra_minion_data_from_config(self): + opts = { + 'renderer': 'json', + 'pillarenv': 'fake_pillar_env', + 'path_to_add': 'fake_data', + 'path_to_add2': {'fake_data5': 'fake_data6', + 'fake_data2': ['fake_data3', 'fake_data4']}, + 'pass_to_ext_pillars': ['path_to_add']} + mock_channel = MagicMock( + crypted_transfer_decode_dictentry=MagicMock(return_value={})) + with patch('salt.transport.Channel.factory', + MagicMock(return_value=mock_channel)): + pillar = salt.pillar.RemotePillar(opts, self.grains, + 'mocked_minion', 'fake_env') + + ret = pillar.compile_pillar() + self.assertEqual(pillar.channel, mock_channel) + mock_channel.crypted_transfer_decode_dictentry.assert_called_once_with( + {'cmd': '_pillar', 'ver': '2', + 'id': 'mocked_minion', + 'grains': {}, + 'saltenv': 'fake_env', + 'pillarenv': 'fake_pillar_env', + 'pillar_override': {}, + 'extra_minion_data': {'path_to_add': 'fake_data'}}, + dictkey='pillar') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@patch('salt.transport.client.AsyncReqChannel.factory', MagicMock()) +class AsyncRemotePillarTestCase(TestCase): + ''' + Tests for instantiating a AsyncRemotePillar in salt.pillar + ''' + def setUp(self): + self.grains = {} + + def tearDown(self): + for attr in ('grains',): + try: + delattr(self, attr) + except AttributeError: + continue + + def test_get_opts_in_pillar_override_call(self): + mock_get_extra_minion_data = MagicMock(return_value={}) + with patch( + 'salt.pillar.RemotePillarMixin.get_ext_pillar_extra_minion_data', + mock_get_extra_minion_data): + + salt.pillar.RemotePillar({}, self.grains, 'mocked-minion', 'dev') + mock_get_extra_minion_data.assert_called_once_with( + {'environment': 'dev'}) + + def test_pillar_send_extra_minion_data_from_config(self): + opts = { + 'renderer': 'json', + 'pillarenv': 'fake_pillar_env', + 'path_to_add': 'fake_data', + 'path_to_add2': {'fake_data5': 'fake_data6', + 'fake_data2': ['fake_data3', 'fake_data4']}, + 'pass_to_ext_pillars': ['path_to_add']} + mock_channel = MagicMock( + crypted_transfer_decode_dictentry=MagicMock(return_value={})) + with patch('salt.transport.client.AsyncReqChannel.factory', + MagicMock(return_value=mock_channel)): + pillar = salt.pillar.RemotePillar(opts, self.grains, + 'mocked_minion', 'fake_env') + + ret = pillar.compile_pillar() + mock_channel.crypted_transfer_decode_dictentry.assert_called_once_with( + {'cmd': '_pillar', 'ver': '2', + 'id': 'mocked_minion', + 'grains': {}, + 'saltenv': 'fake_env', + 'pillarenv': 'fake_pillar_env', + 'pillar_override': {}, + 'extra_minion_data': {'path_to_add': 'fake_data'}}, + dictkey='pillar') diff --git a/tests/unit/test_pydsl.py b/tests/unit/test_pydsl.py index 20087ad17a..3500beb16b 100644 --- a/tests/unit/test_pydsl.py +++ b/tests/unit/test_pydsl.py @@ -91,12 +91,7 @@ class PyDSLRendererTestCase(CommonTestCaseBoilerplate): def render_sls(self, content, sls='', saltenv='base', **kws): if 'env' in kws: - salt.utils.versions.warn_until( - 'Oxygen', - 'Parameter \'env\' has been detected in the argument list. This ' - 'parameter is no longer used and has been replaced by \'saltenv\' ' - 'as of Salt 2016.11.0. This warning will be removed in Salt Oxygen.' - ) + # "env" is not supported; Use "saltenv". kws.pop('env') return self.HIGHSTATE.state.rend['pydsl']( diff --git a/tests/unit/test_ssh_config_roster.py b/tests/unit/test_ssh_config_roster.py new file mode 100644 index 0000000000..f3479caf89 --- /dev/null +++ b/tests/unit/test_ssh_config_roster.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +# Import Python libs +from __future__ import absolute_import +import collections + +# Import Salt Testing Libs +from tests.support import mock +from tests.support import mixins +from tests.support.unit import skipIf, TestCase + +# Import Salt Libs +import salt.roster.sshconfig as sshconfig + +_SAMPLE_SSH_CONFIG = """ +Host * + User user.mcuserface + +Host abc* + IdentityFile ~/.ssh/id_rsa_abc + +Host def* + IdentityFile ~/.ssh/id_rsa_def + +Host abc.asdfgfdhgjkl.com + HostName 123.123.123.123 + +Host abc123.asdfgfdhgjkl.com + HostName 123.123.123.124 + +Host def.asdfgfdhgjkl.com + HostName 234.234.234.234 +""" + +_TARGET_ABC = collections.OrderedDict([ + ('user', 'user.mcuserface'), + ('priv', '~/.ssh/id_rsa_abc'), + ('host', 'abc.asdfgfdhgjkl.com') +]) + +_TARGET_ABC123 = collections.OrderedDict([ + ('user', 'user.mcuserface'), + ('priv', '~/.ssh/id_rsa_abc'), + ('host', 'abc123.asdfgfdhgjkl.com') +]) + +_TARGET_DEF = collections.OrderedDict([ + ('user', 'user.mcuserface'), + ('priv', '~/.ssh/id_rsa_def'), + ('host', 'def.asdfgfdhgjkl.com') +]) + +_ALL = { + 'abc.asdfgfdhgjkl.com': _TARGET_ABC, + 'abc123.asdfgfdhgjkl.com': _TARGET_ABC123, + 'def.asdfgfdhgjkl.com': _TARGET_DEF +} + +_ABC_GLOB = { + 'abc.asdfgfdhgjkl.com': _TARGET_ABC, + 'abc123.asdfgfdhgjkl.com': _TARGET_ABC123 +} + + +@skipIf(mock.NO_MOCK, mock.NO_MOCK_REASON) +class SSHConfigRosterTestCase(TestCase, mixins.LoaderModuleMockMixin): + + @classmethod + def setUpClass(cls): + cls.mock_fp = mock_fp = mock.mock_open(read_data=_SAMPLE_SSH_CONFIG) + + def setup_loader_modules(self): + return {sshconfig: {}} + + def test_all(self): + with mock.patch('salt.utils.fopen', self.mock_fp): + with mock.patch('salt.roster.sshconfig._get_ssh_config_file'): + self.mock_fp.return_value.__iter__.return_value = _SAMPLE_SSH_CONFIG.splitlines() + targets = sshconfig.targets('*') + self.assertEqual(targets, _ALL) + + def test_abc_glob(self): + with mock.patch('salt.utils.fopen', self.mock_fp): + with mock.patch('salt.roster.sshconfig._get_ssh_config_file'): + self.mock_fp.return_value.__iter__.return_value = _SAMPLE_SSH_CONFIG.splitlines() + targets = sshconfig.targets('abc*') + self.assertEqual(targets, _ABC_GLOB) diff --git a/tests/unit/test_state.py b/tests/unit/test_state.py index 3d36e70d62..df2cd70259 100644 --- a/tests/unit/test_state.py +++ b/tests/unit/test_state.py @@ -16,8 +16,9 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch from tests.support.mixins import AdaptedConfigurationTestCaseMixin # Import Salt libs -import salt.state import salt.exceptions +from salt.ext import six +import salt.state from salt.utils.odict import OrderedDict, DefaultOrderedDict @@ -472,3 +473,28 @@ class TopFileMergeTestCase(TestCase, AdaptedConfigurationTestCaseMixin): expected_merge = DefaultOrderedDict(OrderedDict) self.assertEqual(merged_tops, expected_merge) + + +class StateReturnsTestCase(TestCase): + ''' + TestCase for code handling state returns. + ''' + + def test_comment_lists_are_converted_to_string(self): + ''' + Tests that states returning a list of comments + have that converted to a single string + ''' + ret = { + 'name': 'myresource', + 'result': True, + 'comment': ['comment 1', 'comment 2'], + 'changes': {}, + } + salt.state.State.verify_ret(ret) # sanity check + with self.assertRaises(salt.exceptions.SaltException): + # Not suitable for export as is + salt.state.State.verify_ret_for_export(ret) + salt.state.State.munge_ret_for_export(ret) + self.assertIsInstance(ret[u'comment'], six.string_types) + salt.state.State.verify_ret_for_export(ret) diff --git a/tests/unit/utils/test_args.py b/tests/unit/utils/test_args.py index a92851a025..7389978265 100644 --- a/tests/unit/utils/test_args.py +++ b/tests/unit/utils/test_args.py @@ -5,10 +5,18 @@ from __future__ import absolute_import from collections import namedtuple # Import Salt Libs +from salt.exceptions import SaltInvocationError import salt.utils.args # Import Salt Testing Libs -from tests.support.unit import TestCase +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + create_autospec, + DEFAULT, + NO_MOCK, + NO_MOCK_REASON, + patch +) class ArgsTestCase(TestCase): @@ -45,3 +53,44 @@ class ArgsTestCase(TestCase): ret = salt.utils.args.parse_kwarg('foobar') self.assertEqual(ret, (None, None)) + + def test_arg_lookup(self): + def dummy_func(first, second, third, fourth='fifth'): + pass + + expected_dict = {'args': ['first', 'second', 'third'], 'kwargs': {'fourth': 'fifth'}} + ret = salt.utils.args.arg_lookup(dummy_func) + self.assertEqual(expected_dict, ret) + + @skipIf(NO_MOCK, NO_MOCK_REASON) + def test_format_call(self): + with patch('salt.utils.args.arg_lookup') as arg_lookup: + def dummy_func(first=None, second=None, third=None): + pass + + arg_lookup.return_value = {'args': ['first', 'second', 'third'], 'kwargs': {}} + get_function_argspec = DEFAULT + get_function_argspec.return_value = namedtuple('ArgSpec', 'args varargs keywords defaults')( + args=['first', 'second', 'third', 'fourth'], varargs=None, keywords=None, defaults=('fifth',)) + + # Make sure we raise an error if we don't pass in the requisite number of arguments + self.assertRaises(SaltInvocationError, salt.utils.format_call, dummy_func, {'1': 2}) + + # Make sure we warn on invalid kwargs + ret = salt.utils.format_call(dummy_func, {'first': 2, 'second': 2, 'third': 3}) + self.assertGreaterEqual(len(ret['warnings']), 1) + + ret = salt.utils.format_call(dummy_func, {'first': 2, 'second': 2, 'third': 3}, + expected_extra_kws=('first', 'second', 'third')) + self.assertDictEqual(ret, {'args': [], 'kwargs': {}}) + + @skipIf(NO_MOCK, NO_MOCK_REASON) + def test_argspec_report(self): + def _test_spec(arg1, arg2, kwarg1=None): + pass + + sys_mock = create_autospec(_test_spec) + test_functions = {'test_module.test_spec': sys_mock} + ret = salt.utils.args.argspec_report(test_functions, 'test_module.test_spec') + self.assertDictEqual(ret, {'test_module.test_spec': + {'kwargs': True, 'args': None, 'defaults': None, 'varargs': True}}) diff --git a/tests/unit/utils/test_color.py b/tests/unit/utils/test_color.py new file mode 100644 index 0000000000..cc4c835b65 --- /dev/null +++ b/tests/unit/utils/test_color.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +''' +Unit tests for salt.utils.color.py +''' + +# Import python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from tests.support.unit import TestCase + +# Import Salt libs +import salt.utils.color + + +class ColorUtilsTestCase(TestCase): + + def test_get_colors(self): + ret = salt.utils.color.get_colors() + self.assertEqual('\x1b[0;37m', str(ret['LIGHT_GRAY'])) + + ret = salt.utils.color.get_colors(use=False) + self.assertDictContainsSubset({'LIGHT_GRAY': ''}, ret) + + ret = salt.utils.color.get_colors(use='LIGHT_GRAY') + # LIGHT_YELLOW now == LIGHT_GRAY + self.assertEqual(str(ret['LIGHT_YELLOW']), str(ret['LIGHT_GRAY'])) diff --git a/tests/unit/utils/test_configparser.py b/tests/unit/utils/test_configparser.py new file mode 100644 index 0000000000..8fe98aee11 --- /dev/null +++ b/tests/unit/utils/test_configparser.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +''' +tests.unit.utils.test_configparser +================================== + +Test the funcs in the custom parsers in salt.utils.configparser +''' +# Import Python Libs +from __future__ import absolute_import +import copy +import errno +import logging +import os + +log = logging.getLogger(__name__) + +# Import Salt Testing Libs +from tests.support.unit import TestCase +from tests.support.paths import TMP + +# Import salt libs +import salt.utils.files +import salt.utils.stringutils +import salt.utils.configparser + +# The user.name param here is intentionally indented with spaces instead of a +# tab to test that we properly load a file with mixed indentation. +ORIG_CONFIG = u'''[user] + name = Артём Анисимов +\temail = foo@bar.com +[remote "origin"] +\turl = https://github.com/terminalmage/salt.git +\tfetch = +refs/heads/*:refs/remotes/origin/* +\tpushurl = git@github.com:terminalmage/salt.git +[color "diff"] +\told = 196 +\tnew = 39 +[core] +\tpager = less -R +\trepositoryformatversion = 0 +\tfilemode = true +\tbare = false +\tlogallrefupdates = true +[alias] +\tmodified = ! git status --porcelain | awk 'match($1, "M"){print $2}' +\tgraph = log --all --decorate --oneline --graph +\thist = log --pretty=format:\\"%h %ad | %s%d [%an]\\" --graph --date=short +[http] +\tsslverify = false'''.split(u'\n') # future lint: disable=non-unicode-string + + +class TestGitConfigParser(TestCase): + ''' + Tests for salt.utils.configparser.GitConfigParser + ''' + maxDiff = None + orig_config = os.path.join(TMP, u'test_gitconfig.orig') + new_config = os.path.join(TMP, u'test_gitconfig.new') + remote = u'remote "origin"' + + def tearDown(self): + del self.conf + try: + os.remove(self.new_config) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise + + def setUp(self): + if not os.path.exists(self.orig_config): + with salt.utils.files.fopen(self.orig_config, u'wb') as fp_: + fp_.write( + salt.utils.stringutils.to_bytes( + u'\n'.join(ORIG_CONFIG) + ) + ) + self.conf = salt.utils.configparser.GitConfigParser() + self.conf.read(self.orig_config) + + @classmethod + def tearDownClass(cls): + try: + os.remove(cls.orig_config) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise + + @staticmethod + def fix_indent(lines): + ''' + Fixes the space-indented 'user' line, because when we write the config + object to a file space indentation will be replaced by tab indentation. + ''' + ret = copy.copy(lines) + for i, _ in enumerate(ret): + if ret[i].startswith(salt.utils.configparser.GitConfigParser.SPACEINDENT): + ret[i] = ret[i].replace(salt.utils.configparser.GitConfigParser.SPACEINDENT, u'\t') + return ret + + @staticmethod + def get_lines(path): + with salt.utils.files.fopen(path, u'r') as fp_: + return salt.utils.stringutils.to_unicode(fp_.read()).splitlines() + + def _test_write(self, mode): + with salt.utils.files.fopen(self.new_config, mode) as fp_: + self.conf.write(fp_) + self.assertEqual( + self.get_lines(self.new_config), + self.fix_indent(ORIG_CONFIG) + ) + + def test_get(self): + ''' + Test getting an option's value + ''' + # Numeric values should be loaded as strings + self.assertEqual(self.conf.get(u'color "diff"', u'old'), u'196') + # Complex strings should be loaded with their literal quotes and + # slashes intact + self.assertEqual( + self.conf.get(u'alias', u'modified'), + u"""! git status --porcelain | awk 'match($1, "M"){print $2}'""" + ) + # future lint: disable=non-unicode-string + self.assertEqual( + self.conf.get(u'alias', u'hist'), + salt.utils.stringutils.to_unicode( + r"""log --pretty=format:\"%h %ad | %s%d [%an]\" --graph --date=short""" + ) + ) + # future lint: enable=non-unicode-string + + def test_read_space_indent(self): + ''' + Test that user.name was successfully loaded despite being indented + using spaces instead of a tab. Additionally, this tests that the value + was loaded as a unicode type on PY2. + ''' + self.assertEqual(self.conf.get(u'user', u'name'), u'Артём Анисимов') + + def test_set_new_option(self): + ''' + Test setting a new option in an existing section + ''' + self.conf.set(u'http', u'useragent', u'myawesomeagent') + self.assertEqual(self.conf.get(u'http', u'useragent'), u'myawesomeagent') + + def test_add_section(self): + ''' + Test adding a section and adding an item to that section + ''' + self.conf.add_section(u'foo') + self.conf.set(u'foo', u'bar', u'baz') + self.assertEqual(self.conf.get(u'foo', u'bar'), u'baz') + + def test_replace_option(self): + ''' + Test replacing an existing option + ''' + # We're also testing the normalization of key names, here. Setting + # "sslVerify" should actually set an "sslverify" option. + self.conf.set(u'http', u'sslVerify', u'true') + self.assertEqual(self.conf.get(u'http', u'sslverify'), u'true') + + def test_set_multivar(self): + ''' + Test setting a multivar and then writing the resulting file + ''' + orig_refspec = u'+refs/heads/*:refs/remotes/origin/*' + new_refspec = u'+refs/tags/*:refs/tags/*' + # Make sure that the original value is a string + self.assertEqual( + self.conf.get(self.remote, u'fetch'), + orig_refspec + ) + # Add another refspec + self.conf.set_multivar(self.remote, u'fetch', new_refspec) + # The value should now be a list + self.assertEqual( + self.conf.get(self.remote, u'fetch'), + [orig_refspec, new_refspec] + ) + # Write the config object to a file + with salt.utils.files.fopen(self.new_config, u'w') as fp_: + self.conf.write(fp_) + # Confirm that the new file was written correctly + expected = self.fix_indent(ORIG_CONFIG) + expected.insert(6, u'\tfetch = %s' % new_refspec) # pylint: disable=string-substitution-usage-error + self.assertEqual(self.get_lines(self.new_config), expected) + + def test_remove_option(self): + ''' + test removing an option, including all items from a multivar + ''' + for item in (u'fetch', u'pushurl'): + self.conf.remove_option(self.remote, item) + # To confirm that the option is now gone, a get should raise an + # NoOptionError exception. + self.assertRaises( + salt.utils.configparser.NoOptionError, + self.conf.get, + self.remote, + item) + + def test_remove_option_regexp(self): + ''' + test removing an option, including all items from a multivar + ''' + orig_refspec = u'+refs/heads/*:refs/remotes/origin/*' + new_refspec_1 = u'+refs/tags/*:refs/tags/*' + new_refspec_2 = u'+refs/foo/*:refs/foo/*' + # First, add both refspecs + self.conf.set_multivar(self.remote, u'fetch', new_refspec_1) + self.conf.set_multivar(self.remote, u'fetch', new_refspec_2) + # Make sure that all three values are there + self.assertEqual( + self.conf.get(self.remote, u'fetch'), + [orig_refspec, new_refspec_1, new_refspec_2] + ) + # If the regex doesn't match, no items should be removed + self.assertFalse( + self.conf.remove_option_regexp( + self.remote, + u'fetch', + salt.utils.stringutils.to_unicode(r'\d{7,10}') # future lint: disable=non-unicode-string + ) + ) + # Make sure that all three values are still there (since none should + # have been removed) + self.assertEqual( + self.conf.get(self.remote, u'fetch'), + [orig_refspec, new_refspec_1, new_refspec_2] + ) + # Remove one of the values + self.assertTrue( + self.conf.remove_option_regexp(self.remote, u'fetch', u'tags')) + # Confirm that the value is gone + self.assertEqual( + self.conf.get(self.remote, u'fetch'), + [orig_refspec, new_refspec_2] + ) + # Remove the other one we added earlier + self.assertTrue( + self.conf.remove_option_regexp(self.remote, u'fetch', u'foo')) + # Since the option now only has one value, it should be a string + self.assertEqual(self.conf.get(self.remote, u'fetch'), orig_refspec) + # Remove the last remaining option + self.assertTrue( + self.conf.remove_option_regexp(self.remote, u'fetch', u'heads')) + # Trying to do a get now should raise an exception + self.assertRaises( + salt.utils.configparser.NoOptionError, + self.conf.get, + self.remote, + u'fetch') + + def test_write(self): + ''' + Test writing using non-binary filehandle + ''' + self._test_write(mode=u'w') + + def test_write_binary(self): + ''' + Test writing using binary filehandle + ''' + self._test_write(mode=u'wb') diff --git a/tests/unit/utils/test_dictdiffer.py b/tests/unit/utils/test_dictdiffer.py new file mode 100644 index 0000000000..2c6243bbd8 --- /dev/null +++ b/tests/unit/utils/test_dictdiffer.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# Import python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from tests.support.unit import TestCase + +# Import Salt libs +import salt.utils.dictdiffer as dictdiffer + + +NONE = dictdiffer.RecursiveDictDiffer.NONE_VALUE + + +class RecursiveDictDifferTestCase(TestCase): + + def setUp(self): + old_dict = {'a': {'b': 1, 'c': 2, 'e': 'old_value', + 'f': 'old_key'}, + 'j': 'value'} + new_dict = {'a': {'b': 1, 'c': 4, 'e': 'new_value', + 'g': 'new_key'}, + 'h': 'new_key', 'i': None, + 'j': 'value'} + self.recursive_diff = \ + dictdiffer.recursive_diff(old_dict, new_dict, + ignore_missing_keys=False) + self.recursive_diff_ign = dictdiffer.recursive_diff(old_dict, new_dict) + + def tearDown(self): + for attrname in ('recursive_diff', 'recursive_diff_missing_keys'): + try: + delattr(self, attrname) + except AttributeError: + continue + + def test_added(self): + self.assertEqual(self.recursive_diff.added(), ['a.g', 'h', 'i']) + + def test_removed(self): + self.assertEqual(self.recursive_diff.removed(), ['a.f']) + + def test_changed_with_ignore_unset_values(self): + self.recursive_diff.ignore_unset_values = True + self.assertEqual(self.recursive_diff.changed(), + ['a.c', 'a.e']) + + def test_changed_without_ignore_unset_values(self): + self.recursive_diff.ignore_unset_values = False + self.assertEqual(self.recursive_diff.changed(), + ['a.c', 'a.e', 'a.g', 'a.f', 'h', 'i']) + + def test_unchanged(self): + self.assertEqual(self.recursive_diff.unchanged(), + ['a.b', 'j']) + + def test_diffs(self): + self.assertDictEqual(self.recursive_diff.diffs, + {'a': {'c': {'old': 2, 'new': 4}, + 'e': {'old': 'old_value', + 'new': 'new_value'}, + 'f': {'old': 'old_key', 'new': NONE}, + 'g': {'old': NONE, 'new': 'new_key'}}, + 'h': {'old': NONE, 'new': 'new_key'}, + 'i': {'old': NONE, 'new': None}}) + self.assertDictEqual(self.recursive_diff_ign.diffs, + {'a': {'c': {'old': 2, 'new': 4}, + 'e': {'old': 'old_value', + 'new': 'new_value'}, + 'g': {'old': NONE, 'new': 'new_key'}}, + 'h': {'old': NONE, 'new': 'new_key'}, + 'i': {'old': NONE, 'new': None}}) + + def test_new_values(self): + self.assertDictEqual(self.recursive_diff.new_values, + {'a': {'c': 4, 'e': 'new_value', + 'f': NONE, 'g': 'new_key'}, + 'h': 'new_key', 'i': None}) + + def test_old_values(self): + self.assertDictEqual(self.recursive_diff.old_values, + {'a': {'c': 2, 'e': 'old_value', + 'f': 'old_key', 'g': NONE}, + 'h': NONE, 'i': NONE}) + + def test_changes_str(self): + self.assertEqual(self.recursive_diff.changes_str, + 'a:\n' + ' c from 2 to 4\n' + ' e from \'old_value\' to \'new_value\'\n' + ' g from nothing to \'new_key\'\n' + ' f from \'old_key\' to nothing\n' + 'h from nothing to \'new_key\'\n' + 'i from nothing to None') diff --git a/tests/unit/utils/test_gitfs.py b/tests/unit/utils/test_gitfs.py index c8942e4695..070a46fe75 100644 --- a/tests/unit/utils/test_gitfs.py +++ b/tests/unit/utils/test_gitfs.py @@ -66,7 +66,7 @@ class TestGitFSProvider(TestCase): ('git_pillar', salt.utils.gitfs.GitPillar), ('winrepo', salt.utils.gitfs.WinRepo)): key = '{0}_provider'.format(role_name) - for provider in salt.utils.gitfs.VALID_PROVIDERS: + for provider in salt.utils.gitfs.GIT_PROVIDERS: verify = 'verify_gitpython' mock1 = _get_mock(verify, provider) with patch.object(role_class, verify, mock1): diff --git a/tests/unit/utils/test_listdiffer.py b/tests/unit/utils/test_listdiffer.py new file mode 100644 index 0000000000..ae8288c81c --- /dev/null +++ b/tests/unit/utils/test_listdiffer.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +# Import python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from tests.support.unit import TestCase + +# Import Salt libs +from salt.utils.listdiffer import list_diff + +from salt.utils import dictdiffer +NONE = dictdiffer.RecursiveDictDiffer.NONE_VALUE + + +class ListDictDifferTestCase(TestCase): + + def setUp(self): + old_list = [{'key': 1, 'value': 'foo1', 'int_value': 101}, + {'key': 2, 'value': 'foo2', 'int_value': 102}, + {'key': 3, 'value': 'foo3', 'int_value': 103}] + new_list = [{'key': 1, 'value': 'foo1', 'int_value': 101}, + {'key': 2, 'value': 'foo2', 'int_value': 112}, + {'key': 5, 'value': 'foo5', 'int_value': 105}] + self.list_diff = list_diff(old_list, new_list, key='key') + + def tearDown(self): + for attrname in ('list_diff',): + try: + delattr(self, attrname) + except AttributeError: + continue + + def test_added(self): + self.assertEqual(self.list_diff.added, + [{'key': 5, 'value': 'foo5', 'int_value': 105}]) + + def test_removed(self): + self.assertEqual(self.list_diff.removed, + [{'key': 3, 'value': 'foo3', 'int_value': 103}]) + + def test_diffs(self): + self.assertEqual(self.list_diff.diffs, + [{2: {'int_value': {'new': 112, 'old': 102}}}, + # Added items + {5: {'int_value': {'new': 105, 'old': NONE}, + 'key': {'new': 5, 'old': NONE}, + 'value': {'new': 'foo5', 'old': NONE}}}, + # Removed items + {3: {'int_value': {'new': NONE, 'old': 103}, + 'key': {'new': NONE, 'old': 3}, + 'value': {'new': NONE, 'old': 'foo3'}}}]) + + def test_new_values(self): + self.assertEqual(self.list_diff.new_values, + [{'key': 2, 'int_value': 112}, + {'key': 5, 'value': 'foo5', 'int_value': 105}]) + + def test_old_values(self): + self.assertEqual(self.list_diff.old_values, + [{'key': 2, 'int_value': 102}, + {'key': 3, 'value': 'foo3', 'int_value': 103}]) + + def test_changed_all(self): + self.assertEqual(self.list_diff.changed(selection='all'), + ['key.2.int_value', 'key.5.int_value', 'key.5.value', + 'key.3.int_value', 'key.3.value']) + + def test_changed_intersect(self): + self.assertEqual(self.list_diff.changed(selection='intersect'), + ['key.2.int_value']) + + def test_changes_str(self): + self.assertEqual(self.list_diff.changes_str, + '\tidentified by key 2:\n' + '\tint_value from 102 to 112\n' + '\tidentified by key 3:\n' + '\twill be removed\n' + '\tidentified by key 5:\n' + '\twill be added\n') + + def test_changes_str2(self): + self.assertEqual(self.list_diff.changes_str2, + ' key=2 (updated):\n' + ' int_value from 102 to 112\n' + ' key=3 (removed)\n' + ' key=5 (added): {\'int_value\': 105, \'key\': 5, ' + '\'value\': \'foo5\'}') diff --git a/tests/unit/utils/test_minions.py b/tests/unit/utils/test_minions.py index d6e4df652b..fbec4e2c8b 100644 --- a/tests/unit/utils/test_minions.py +++ b/tests/unit/utils/test_minions.py @@ -57,7 +57,9 @@ class CkMinionsTestCase(TestCase): ret = self.ckminions.spec_check(auth_list, 'test.arg', {}, 'wheel') self.assertFalse(ret) ret = self.ckminions.spec_check(auth_list, 'testarg', {}, 'runner') - self.assertFalse(ret) + mock_ret = {'error': {'name': 'SaltInvocationError', + 'message': 'A command invocation error occurred: Check syntax.'}} + self.assertDictEqual(mock_ret, ret) # Test spec in plural form auth_list = ['@runners'] diff --git a/tests/unit/utils/test_reactor.py b/tests/unit/utils/test_reactor.py index 765b11d80c..b0a10d581f 100644 --- a/tests/unit/utils/test_reactor.py +++ b/tests/unit/utils/test_reactor.py @@ -1,74 +1,556 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -import time -import shutil -import tempfile +import codecs +import glob +import logging import os +import textwrap +import yaml -from contextlib import contextmanager - -import salt.utils.files -from salt.utils.process import clean_proc +import salt.loader +import salt.utils import salt.utils.reactor as reactor -from tests.integration import AdaptedConfigurationTestCaseMixin -from tests.support.paths import TMP from tests.support.unit import TestCase, skipIf -from tests.support.mock import patch, MagicMock +from tests.support.mixins import AdaptedConfigurationTestCaseMixin +from tests.support.mock import ( + NO_MOCK, + NO_MOCK_REASON, + patch, + MagicMock, + Mock, + mock_open, +) + +REACTOR_CONFIG = '''\ +reactor: + - old_runner: + - /srv/reactor/old_runner.sls + - old_wheel: + - /srv/reactor/old_wheel.sls + - old_local: + - /srv/reactor/old_local.sls + - old_cmd: + - /srv/reactor/old_cmd.sls + - old_caller: + - /srv/reactor/old_caller.sls + - new_runner: + - /srv/reactor/new_runner.sls + - new_wheel: + - /srv/reactor/new_wheel.sls + - new_local: + - /srv/reactor/new_local.sls + - new_cmd: + - /srv/reactor/new_cmd.sls + - new_caller: + - /srv/reactor/new_caller.sls +''' + +REACTOR_DATA = { + 'runner': {'data': {'message': 'This is an error'}}, + 'wheel': {'data': {'id': 'foo'}}, + 'local': {'data': {'pkg': 'zsh', 'repo': 'updates'}}, + 'cmd': {'data': {'pkg': 'zsh', 'repo': 'updates'}}, + 'caller': {'data': {'path': '/tmp/foo'}}, +} + +SLS = { + '/srv/reactor/old_runner.sls': textwrap.dedent('''\ + raise_error: + runner.error.error: + - name: Exception + - message: {{ data['data']['message'] }} + '''), + '/srv/reactor/old_wheel.sls': textwrap.dedent('''\ + remove_key: + wheel.key.delete: + - match: {{ data['data']['id'] }} + '''), + '/srv/reactor/old_local.sls': textwrap.dedent('''\ + install_zsh: + local.state.single: + - tgt: test + - arg: + - pkg.installed + - {{ data['data']['pkg'] }} + - kwarg: + fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/old_cmd.sls': textwrap.dedent('''\ + install_zsh: + cmd.state.single: + - tgt: test + - arg: + - pkg.installed + - {{ data['data']['pkg'] }} + - kwarg: + fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/old_caller.sls': textwrap.dedent('''\ + touch_file: + caller.file.touch: + - args: + - {{ data['data']['path'] }} + '''), + '/srv/reactor/new_runner.sls': textwrap.dedent('''\ + raise_error: + runner.error.error: + - args: + - name: Exception + - message: {{ data['data']['message'] }} + '''), + '/srv/reactor/new_wheel.sls': textwrap.dedent('''\ + remove_key: + wheel.key.delete: + - args: + - match: {{ data['data']['id'] }} + '''), + '/srv/reactor/new_local.sls': textwrap.dedent('''\ + install_zsh: + local.state.single: + - tgt: test + - args: + - fun: pkg.installed + - name: {{ data['data']['pkg'] }} + - fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/new_cmd.sls': textwrap.dedent('''\ + install_zsh: + cmd.state.single: + - tgt: test + - args: + - fun: pkg.installed + - name: {{ data['data']['pkg'] }} + - fromrepo: {{ data['data']['repo'] }} + '''), + '/srv/reactor/new_caller.sls': textwrap.dedent('''\ + touch_file: + caller.file.touch: + - args: + - name: {{ data['data']['path'] }} + '''), +} + +LOW_CHUNKS = { + # Note that the "name" value in the chunk has been overwritten by the + # "name" argument in the SLS. This is one reason why the new schema was + # needed. + 'old_runner': [{ + 'state': 'runner', + '__id__': 'raise_error', + '__sls__': '/srv/reactor/old_runner.sls', + 'order': 1, + 'fun': 'error.error', + 'name': 'Exception', + 'message': 'This is an error', + }], + 'old_wheel': [{ + 'state': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/old_wheel.sls', + 'order': 1, + 'fun': 'key.delete', + 'match': 'foo', + }], + 'old_local': [{ + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_local.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }], + 'old_cmd': [{ + 'state': 'local', # 'cmd' should be aliased to 'local' + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_cmd.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }], + 'old_caller': [{ + 'state': 'caller', + '__id__': 'touch_file', + 'name': 'touch_file', + '__sls__': '/srv/reactor/old_caller.sls', + 'order': 1, + 'fun': 'file.touch', + 'args': ['/tmp/foo'], + }], + 'new_runner': [{ + 'state': 'runner', + '__id__': 'raise_error', + 'name': 'raise_error', + '__sls__': '/srv/reactor/new_runner.sls', + 'order': 1, + 'fun': 'error.error', + 'args': [ + {'name': 'Exception'}, + {'message': 'This is an error'}, + ], + }], + 'new_wheel': [{ + 'state': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/new_wheel.sls', + 'order': 1, + 'fun': 'key.delete', + 'args': [ + {'match': 'foo'}, + ], + }], + 'new_local': [{ + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_local.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'args': [ + {'fun': 'pkg.installed'}, + {'name': 'zsh'}, + {'fromrepo': 'updates'}, + ], + }], + 'new_cmd': [{ + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_cmd.sls', + 'order': 1, + 'tgt': 'test', + 'fun': 'state.single', + 'args': [ + {'fun': 'pkg.installed'}, + {'name': 'zsh'}, + {'fromrepo': 'updates'}, + ], + }], + 'new_caller': [{ + 'state': 'caller', + '__id__': 'touch_file', + 'name': 'touch_file', + '__sls__': '/srv/reactor/new_caller.sls', + 'order': 1, + 'fun': 'file.touch', + 'args': [ + {'name': '/tmp/foo'}, + ], + }], +} + +WRAPPER_CALLS = { + 'old_runner': ( + 'error.error', + { + '__state__': 'runner', + '__id__': 'raise_error', + '__sls__': '/srv/reactor/old_runner.sls', + '__user__': 'Reactor', + 'order': 1, + 'arg': [], + 'kwarg': { + 'name': 'Exception', + 'message': 'This is an error', + }, + 'name': 'Exception', + 'message': 'This is an error', + }, + ), + 'old_wheel': ( + 'key.delete', + { + '__state__': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/old_wheel.sls', + 'order': 1, + '__user__': 'Reactor', + 'arg': ['foo'], + 'kwarg': {}, + 'match': 'foo', + }, + ), + 'old_local': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_local.sls', + 'order': 1, + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }, + }, + 'old_cmd': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/old_cmd.sls', + 'order': 1, + 'arg': ['pkg.installed', 'zsh'], + 'kwarg': {'fromrepo': 'updates'}, + }, + }, + 'old_caller': { + 'args': ('file.touch', '/tmp/foo'), + 'kwargs': {}, + }, + 'new_runner': ( + 'error.error', + { + '__state__': 'runner', + '__id__': 'raise_error', + 'name': 'raise_error', + '__sls__': '/srv/reactor/new_runner.sls', + '__user__': 'Reactor', + 'order': 1, + 'arg': (), + 'kwarg': { + 'name': 'Exception', + 'message': 'This is an error', + }, + }, + ), + 'new_wheel': ( + 'key.delete', + { + '__state__': 'wheel', + '__id__': 'remove_key', + 'name': 'remove_key', + '__sls__': '/srv/reactor/new_wheel.sls', + 'order': 1, + '__user__': 'Reactor', + 'arg': (), + 'kwarg': {'match': 'foo'}, + }, + ), + 'new_local': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_local.sls', + 'order': 1, + 'arg': (), + 'kwarg': { + 'fun': 'pkg.installed', + 'name': 'zsh', + 'fromrepo': 'updates', + }, + }, + }, + 'new_cmd': { + 'args': ('test', 'state.single'), + 'kwargs': { + 'state': 'local', + '__id__': 'install_zsh', + 'name': 'install_zsh', + '__sls__': '/srv/reactor/new_cmd.sls', + 'order': 1, + 'arg': (), + 'kwarg': { + 'fun': 'pkg.installed', + 'name': 'zsh', + 'fromrepo': 'updates', + }, + }, + }, + 'new_caller': { + 'args': ('file.touch',), + 'kwargs': {'name': '/tmp/foo'}, + }, +} + +log = logging.getLogger(__name__) -@contextmanager -def reactor_process(opts, reactor): - opts = dict(opts) - opts['reactor'] = reactor - proc = reactor.Reactor(opts) - proc.start() - try: - if os.environ.get('TRAVIS_PYTHON_VERSION', None) is not None: - # Travis is slow - time.sleep(10) - else: - time.sleep(2) - yield - finally: - clean_proc(proc) - - -def _args_sideffect(*args, **kwargs): - return args, kwargs - - -@skipIf(True, 'Skipping until its clear what and how is this supposed to be testing') +@skipIf(NO_MOCK, NO_MOCK_REASON) class TestReactor(TestCase, AdaptedConfigurationTestCaseMixin): - def setUp(self): - self.opts = self.get_temp_config('master') - self.tempdir = tempfile.mkdtemp(dir=TMP) - self.sls_name = os.path.join(self.tempdir, 'test.sls') - with salt.utils.files.fopen(self.sls_name, 'w') as fh: - fh.write(''' -update_fileserver: - runner.fileserver.update -''') + ''' + Tests for constructing the low chunks to be executed via the Reactor + ''' + @classmethod + def setUpClass(cls): + ''' + Load the reactor config for mocking + ''' + cls.opts = cls.get_temp_config('master') + reactor_config = yaml.safe_load(REACTOR_CONFIG) + cls.opts.update(reactor_config) + cls.reactor = reactor.Reactor(cls.opts) + cls.reaction_map = salt.utils.repack_dictlist(reactor_config['reactor']) + renderers = salt.loader.render(cls.opts, {}) + cls.render_pipe = [(renderers[x], '') for x in ('jinja', 'yaml')] - def tearDown(self): - if os.path.isdir(self.tempdir): - shutil.rmtree(self.tempdir) - del self.opts - del self.tempdir - del self.sls_name + @classmethod + def tearDownClass(cls): + del cls.opts + del cls.reactor + del cls.render_pipe - def test_basic(self): - reactor_config = [ - {'salt/tagA': ['/srv/reactor/A.sls']}, - {'salt/tagB': ['/srv/reactor/B.sls']}, - {'*': ['/srv/reactor/all.sls']}, - ] - wrap = reactor.ReactWrap(self.opts) - with patch.object(reactor.ReactWrap, 'local', MagicMock(side_effect=_args_sideffect)): - ret = wrap.run({'fun': 'test.ping', - 'state': 'local', - 'order': 1, - 'name': 'foo_action', - '__id__': 'foo_action'}) - raise Exception(ret) + def test_list_reactors(self): + ''' + Ensure that list_reactors() returns the correct list of reactor SLS + files for each tag. + ''' + for schema in ('old', 'new'): + for rtype in REACTOR_DATA: + tag = '_'.join((schema, rtype)) + self.assertEqual( + self.reactor.list_reactors(tag), + self.reaction_map[tag] + ) + + def test_reactions(self): + ''' + Ensure that the correct reactions are built from the configured SLS + files and tag data. + ''' + for schema in ('old', 'new'): + for rtype in REACTOR_DATA: + tag = '_'.join((schema, rtype)) + log.debug('test_reactions: processing %s', tag) + reactors = self.reactor.list_reactors(tag) + log.debug('test_reactions: %s reactors: %s', tag, reactors) + # No globbing in our example SLS, and the files don't actually + # exist, so mock glob.glob to just return back the path passed + # to it. + with patch.object( + glob, + 'glob', + MagicMock(side_effect=lambda x: [x])): + # The below four mocks are all so that + # salt.template.compile_template() will read the templates + # we've mocked up in the SLS global variable above. + with patch.object( + os.path, 'isfile', + MagicMock(return_value=True)): + with patch.object( + salt.utils, 'is_empty', + MagicMock(return_value=False)): + with patch.object( + codecs, 'open', + mock_open(read_data=SLS[reactors[0]])): + with patch.object( + salt.template, 'template_shebang', + MagicMock(return_value=self.render_pipe)): + reactions = self.reactor.reactions( + tag, + REACTOR_DATA[rtype], + reactors, + ) + log.debug( + 'test_reactions: %s reactions: %s', + tag, reactions + ) + self.assertEqual(reactions, LOW_CHUNKS[tag]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class TestReactWrap(TestCase, AdaptedConfigurationTestCaseMixin): + ''' + Tests that we are formulating the wrapper calls properly + ''' + @classmethod + def setUpClass(cls): + cls.wrap = reactor.ReactWrap(cls.get_temp_config('master')) + + @classmethod + def tearDownClass(cls): + del cls.wrap + + def test_runner(self): + ''' + Test runner reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'runner')) + chunk = LOW_CHUNKS[tag][0] + thread_pool = Mock() + thread_pool.fire_async = Mock() + with patch.object(self.wrap, 'pool', thread_pool): + self.wrap.run(chunk) + thread_pool.fire_async.assert_called_with( + self.wrap.client_cache['runner'].low, + args=WRAPPER_CALLS[tag] + ) + + def test_wheel(self): + ''' + Test wheel reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'wheel')) + chunk = LOW_CHUNKS[tag][0] + thread_pool = Mock() + thread_pool.fire_async = Mock() + with patch.object(self.wrap, 'pool', thread_pool): + self.wrap.run(chunk) + thread_pool.fire_async.assert_called_with( + self.wrap.client_cache['wheel'].low, + args=WRAPPER_CALLS[tag] + ) + + def test_local(self): + ''' + Test local reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'local')) + chunk = LOW_CHUNKS[tag][0] + client_cache = {'local': Mock()} + client_cache['local'].cmd_async = Mock() + with patch.object(self.wrap, 'client_cache', client_cache): + self.wrap.run(chunk) + client_cache['local'].cmd_async.assert_called_with( + *WRAPPER_CALLS[tag]['args'], + **WRAPPER_CALLS[tag]['kwargs'] + ) + + def test_cmd(self): + ''' + Test cmd reactions (alias for 'local') using both the old and new + config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'cmd')) + chunk = LOW_CHUNKS[tag][0] + client_cache = {'local': Mock()} + client_cache['local'].cmd_async = Mock() + with patch.object(self.wrap, 'client_cache', client_cache): + self.wrap.run(chunk) + client_cache['local'].cmd_async.assert_called_with( + *WRAPPER_CALLS[tag]['args'], + **WRAPPER_CALLS[tag]['kwargs'] + ) + + def test_caller(self): + ''' + Test caller reactions using both the old and new config schema + ''' + for schema in ('old', 'new'): + tag = '_'.join((schema, 'caller')) + chunk = LOW_CHUNKS[tag][0] + client_cache = {'caller': Mock()} + client_cache['caller'].cmd = Mock() + with patch.object(self.wrap, 'client_cache', client_cache): + self.wrap.run(chunk) + client_cache['caller'].cmd.assert_called_with( + *WRAPPER_CALLS[tag]['args'], + **WRAPPER_CALLS[tag]['kwargs'] + ) diff --git a/tests/unit/utils/test_schema.py b/tests/unit/utils/test_schema.py index a8181ffe86..edf8841b6b 100644 --- a/tests/unit/utils/test_schema.py +++ b/tests/unit/utils/test_schema.py @@ -136,7 +136,7 @@ class ConfigTestCase(TestCase): def test_optional_requirements_config(self): class BaseRequirements(schema.Schema): - driver = schema.StringItem(default='digital_ocean', format='hidden') + driver = schema.StringItem(default='digitalocean', format='hidden') class SSHKeyFileSchema(schema.Schema): ssh_key_file = schema.StringItem( @@ -149,13 +149,13 @@ class ConfigTestCase(TestCase): ssh_key_names = schema.StringItem( title='SSH Key Names', description='The names of an SSH key being managed on ' - 'Digital Ocean account which will be used to ' + 'DigitalOcean account which will be used to ' 'authenticate on the deployed VMs', ) class Requirements(BaseRequirements): - title = 'Digital Ocean' - description = 'Digital Ocean Cloud VM configuration requirements.' + title = 'DigitalOcean' + description = 'DigitalOcean Cloud VM configuration requirements.' personal_access_token = schema.StringItem( title='Personal Access Token', @@ -174,12 +174,12 @@ class ConfigTestCase(TestCase): expected = { '$schema': 'http://json-schema.org/draft-04/schema#', - 'title': 'Digital Ocean', - 'description': 'Digital Ocean Cloud VM configuration requirements.', + 'title': 'DigitalOcean', + 'description': 'DigitalOcean Cloud VM configuration requirements.', 'type': 'object', 'properties': { 'driver': { - 'default': 'digital_ocean', + 'default': 'digitalocean', 'format': 'hidden', 'type': 'string', 'title': 'driver' @@ -198,9 +198,8 @@ class ConfigTestCase(TestCase): }, 'ssh_key_names': { 'type': 'string', - 'description': 'The names of an SSH key being managed on Digital ' - 'Ocean account which will be used to authenticate ' - 'on the deployed VMs', + 'description': 'The names of an SSH key being managed on DigitalOcean ' + 'account which will be used to authenticate on the deployed VMs', 'title': 'SSH Key Names' } }, @@ -222,8 +221,8 @@ class ConfigTestCase(TestCase): self.assertDictEqual(expected, Requirements.serialize()) class Requirements2(BaseRequirements): - title = 'Digital Ocean' - description = 'Digital Ocean Cloud VM configuration requirements.' + title = 'DigitalOcean' + description = 'DigitalOcean Cloud VM configuration requirements.' personal_access_token = schema.StringItem( title='Personal Access Token', @@ -239,7 +238,7 @@ class ConfigTestCase(TestCase): ssh_key_names = schema.StringItem( title='SSH Key Names', description='The names of an SSH key being managed on ' - 'Digital Ocean account which will be used to ' + 'DigitalOcean account which will be used to ' 'authenticate on the deployed VMs') requirements_definition = schema.AnyOfItem( @@ -251,12 +250,12 @@ class ConfigTestCase(TestCase): expected = { '$schema': 'http://json-schema.org/draft-04/schema#', - 'title': 'Digital Ocean', - 'description': 'Digital Ocean Cloud VM configuration requirements.', + 'title': 'DigitalOcean', + 'description': 'DigitalOcean Cloud VM configuration requirements.', 'type': 'object', 'properties': { 'driver': { - 'default': 'digital_ocean', + 'default': 'digitalocean', 'format': 'hidden', 'type': 'string', 'title': 'driver' @@ -275,9 +274,8 @@ class ConfigTestCase(TestCase): }, 'ssh_key_names': { 'type': 'string', - 'description': 'The names of an SSH key being managed on Digital ' - 'Ocean account which will be used to authenticate ' - 'on the deployed VMs', + 'description': 'The names of an SSH key being managed on DigitalOcean ' + 'account which will be used to authenticate on the deployed VMs', 'title': 'SSH Key Names' } }, @@ -299,19 +297,19 @@ class ConfigTestCase(TestCase): self.assertDictContainsSubset(expected, Requirements2.serialize()) class Requirements3(schema.Schema): - title = 'Digital Ocean' - description = 'Digital Ocean Cloud VM configuration requirements.' + title = 'DigitalOcean' + description = 'DigitalOcean Cloud VM configuration requirements.' merge_reqs = Requirements(flatten=True) expected = { '$schema': 'http://json-schema.org/draft-04/schema#', - 'title': 'Digital Ocean', - 'description': 'Digital Ocean Cloud VM configuration requirements.', + 'title': 'DigitalOcean', + 'description': 'DigitalOcean Cloud VM configuration requirements.', 'type': 'object', 'properties': { 'driver': { - 'default': 'digital_ocean', + 'default': 'digitalocean', 'format': 'hidden', 'type': 'string', 'title': 'driver' @@ -330,9 +328,8 @@ class ConfigTestCase(TestCase): }, 'ssh_key_names': { 'type': 'string', - 'description': 'The names of an SSH key being managed on Digital ' - 'Ocean account which will be used to authenticate ' - 'on the deployed VMs', + 'description': 'The names of an SSH key being managed on DigitalOcean ' + 'account which will be used to authenticate on the deployed VMs', 'title': 'SSH Key Names' } }, @@ -354,8 +351,8 @@ class ConfigTestCase(TestCase): self.assertDictContainsSubset(expected, Requirements3.serialize()) class Requirements4(schema.Schema): - title = 'Digital Ocean' - description = 'Digital Ocean Cloud VM configuration requirements.' + title = 'DigitalOcean' + description = 'DigitalOcean Cloud VM configuration requirements.' merge_reqs = Requirements(flatten=True) @@ -367,7 +364,7 @@ class ConfigTestCase(TestCase): ssh_key_names_2 = schema.StringItem( title='SSH Key Names', description='The names of an SSH key being managed on ' - 'Digital Ocean account which will be used to ' + 'DigitalOcean account which will be used to ' 'authenticate on the deployed VMs') requirements_definition_2 = schema.AnyOfItem( @@ -379,12 +376,12 @@ class ConfigTestCase(TestCase): expected = { '$schema': 'http://json-schema.org/draft-04/schema#', - 'title': 'Digital Ocean', - 'description': 'Digital Ocean Cloud VM configuration requirements.', + 'title': 'DigitalOcean', + 'description': 'DigitalOcean Cloud VM configuration requirements.', 'type': 'object', 'properties': { 'driver': { - 'default': 'digital_ocean', + 'default': 'digitalocean', 'format': 'hidden', 'type': 'string', 'title': 'driver' @@ -403,9 +400,8 @@ class ConfigTestCase(TestCase): }, 'ssh_key_names': { 'type': 'string', - 'description': 'The names of an SSH key being managed on Digital ' - 'Ocean account which will be used to authenticate ' - 'on the deployed VMs', + 'description': 'The names of an SSH key being managed on DigitalOcean ' + 'account which will be used to authenticate on the deployed VMs', 'title': 'SSH Key Names' }, 'ssh_key_file_2': { @@ -416,9 +412,8 @@ class ConfigTestCase(TestCase): }, 'ssh_key_names_2': { 'type': 'string', - 'description': 'The names of an SSH key being managed on Digital ' - 'Ocean account which will be used to authenticate ' - 'on the deployed VMs', + 'description': 'The names of an SSH key being managed on DigitalOcean ' + 'account which will be used to authenticate on the deployed VMs', 'title': 'SSH Key Names' } }, @@ -446,7 +441,7 @@ class ConfigTestCase(TestCase): @skipIf(HAS_JSONSCHEMA is False, 'The \'jsonschema\' library is missing') def test_optional_requirements_config_validation(self): class BaseRequirements(schema.Schema): - driver = schema.StringItem(default='digital_ocean', format='hidden') + driver = schema.StringItem(default='digitalocean', format='hidden') class SSHKeyFileSchema(schema.Schema): ssh_key_file = schema.StringItem( @@ -462,8 +457,8 @@ class ConfigTestCase(TestCase): 'authenticate on the deployed VMs') class Requirements(BaseRequirements): - title = 'Digital Ocean' - description = 'Digital Ocean Cloud VM configuration requirements.' + title = 'DigitalOcean' + description = 'DigitalOcean Cloud VM configuration requirements.' personal_access_token = schema.StringItem( title='Personal Access Token', diff --git a/tests/unit/utils/test_state.py b/tests/unit/utils/test_state.py new file mode 100644 index 0000000000..18a7c2c9af --- /dev/null +++ b/tests/unit/utils/test_state.py @@ -0,0 +1,688 @@ +# -*- coding: utf-8 -*- +''' +Unit Tests for functions located in salt.utils.state.py. +''' + +# Import python libs +from __future__ import absolute_import +import copy +import textwrap + +# Import Salt libs +from salt.ext import six +import salt.utils.odict +import salt.utils.state + +# Import Salt Testing libs +from tests.support.unit import TestCase + + +class StateUtilTestCase(TestCase): + ''' + Test case for state util. + ''' + def test_check_result(self): + self.assertFalse(salt.utils.state.check_result(None), + 'Failed to handle None as an invalid data type.') + self.assertFalse(salt.utils.state.check_result([]), + 'Failed to handle an invalid data type.') + self.assertFalse(salt.utils.state.check_result({}), + 'Failed to handle an empty dictionary.') + self.assertFalse(salt.utils.state.check_result({'host1': []}), + 'Failed to handle an invalid host data structure.') + test_valid_state = {'host1': {'test_state': {'result': 'We have liftoff!'}}} + self.assertTrue(salt.utils.state.check_result(test_valid_state)) + test_valid_false_states = { + 'test1': salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': False}), + ])), + ]), + 'test2': salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': True}), + ])), + ('host2', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': False}), + ])), + ]), + 'test3': ['a'], + 'test4': salt.utils.odict.OrderedDict([ + ('asup', salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': True}), + ])), + ('host2', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': False}), + ])) + ])) + ]), + 'test5': salt.utils.odict.OrderedDict([ + ('asup', salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': True}), + ])), + ('host2', salt.utils.odict.OrderedDict([])) + ])) + ]) + } + for test, data in six.iteritems(test_valid_false_states): + self.assertFalse( + salt.utils.state.check_result(data), + msg='{0} failed'.format(test)) + test_valid_true_states = { + 'test1': salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': True}), + ])), + ]), + 'test3': salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': True}), + ])), + ('host2', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': True}), + ])), + ]), + 'test4': salt.utils.odict.OrderedDict([ + ('asup', salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': True}), + ])), + ('host2', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': True}), + ])) + ])) + ]), + 'test2': salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': None}), + ('test_state', {'result': True}), + ])), + ('host2', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': True}), + ('test_state', {'result': 'abc'}), + ])) + ]) + } + for test, data in six.iteritems(test_valid_true_states): + self.assertTrue( + salt.utils.state.check_result(data), + msg='{0} failed'.format(test)) + test_invalid_true_ht_states = { + 'test_onfail_simple2': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_vstate0', {'result': False}), + ('test_vstate1', {'result': True}), + ])), + ]), + { + 'test_vstate0': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}]}, + 'test_vstate1': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + salt.utils.odict.OrderedDict([ + ('onfail_stop', True), + ('onfail', + [salt.utils.odict.OrderedDict([('cmd', 'test_vstate0')])]) + ]), + 'run', + {'order': 10004}]}, + } + ), + 'test_onfail_integ2': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('t_|-test_ivstate0_|-echo_|-run', { + 'result': False}), + ('cmd_|-test_ivstate0_|-echo_|-run', { + 'result': False}), + ('cmd_|-test_ivstate1_|-echo_|-run', { + 'result': False}), + ])), + ]), + { + 'test_ivstate0': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}], + 't': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}]}, + 'test_ivstate1': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + salt.utils.odict.OrderedDict([ + ('onfail_stop', False), + ('onfail', + [salt.utils.odict.OrderedDict([('cmd', 'test_ivstate0')])]) + ]), + 'run', + {'order': 10004}]}, + } + ), + 'test_onfail_integ3': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('t_|-test_ivstate0_|-echo_|-run', { + 'result': True}), + ('cmd_|-test_ivstate0_|-echo_|-run', { + 'result': False}), + ('cmd_|-test_ivstate1_|-echo_|-run', { + 'result': False}), + ])), + ]), + { + 'test_ivstate0': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}], + 't': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}]}, + 'test_ivstate1': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + salt.utils.odict.OrderedDict([ + ('onfail_stop', False), + ('onfail', + [salt.utils.odict.OrderedDict([('cmd', 'test_ivstate0')])]) + ]), + 'run', + {'order': 10004}]}, + } + ), + 'test_onfail_integ4': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('t_|-test_ivstate0_|-echo_|-run', { + 'result': False}), + ('cmd_|-test_ivstate0_|-echo_|-run', { + 'result': False}), + ('cmd_|-test_ivstate1_|-echo_|-run', { + 'result': True}), + ])), + ]), + { + 'test_ivstate0': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}], + 't': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}]}, + 'test_ivstate1': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + salt.utils.odict.OrderedDict([ + ('onfail_stop', False), + ('onfail', + [salt.utils.odict.OrderedDict([('cmd', 'test_ivstate0')])]) + ]), + 'run', + {'order': 10004}]}, + 'test_ivstate2': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + salt.utils.odict.OrderedDict([ + ('onfail_stop', True), + ('onfail', + [salt.utils.odict.OrderedDict([('cmd', 'test_ivstate0')])]) + ]), + 'run', + {'order': 10004}]}, + } + ), + 'test_onfail': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': False}), + ('test_state', {'result': True}), + ])), + ]), + None + ), + 'test_onfail_d': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_state0', {'result': False}), + ('test_state', {'result': True}), + ])), + ]), + {} + ) + } + for test, testdata in six.iteritems(test_invalid_true_ht_states): + data, ht = testdata + for t_ in [a for a in data['host1']]: + tdata = data['host1'][t_] + if '_|-' in t_: + t_ = t_.split('_|-')[1] + tdata['__id__'] = t_ + self.assertFalse( + salt.utils.state.check_result(data, highstate=ht), + msg='{0} failed'.format(test)) + + test_valid_true_ht_states = { + 'test_onfail_integ': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('cmd_|-test_ivstate0_|-echo_|-run', { + 'result': False}), + ('cmd_|-test_ivstate1_|-echo_|-run', { + 'result': True}), + ])), + ]), + { + 'test_ivstate0': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}]}, + 'test_ivstate1': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + salt.utils.odict.OrderedDict([ + ('onfail_stop', False), + ('onfail', + [salt.utils.odict.OrderedDict([('cmd', 'test_ivstate0')])]) + ]), + 'run', + {'order': 10004}]}, + } + ), + 'test_onfail_intega3': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('t_|-test_ivstate0_|-echo_|-run', { + 'result': True}), + ('cmd_|-test_ivstate0_|-echo_|-run', { + 'result': False}), + ('cmd_|-test_ivstate1_|-echo_|-run', { + 'result': True}), + ])), + ]), + { + 'test_ivstate0': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}], + 't': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}]}, + 'test_ivstate1': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + salt.utils.odict.OrderedDict([ + ('onfail_stop', False), + ('onfail', + [salt.utils.odict.OrderedDict([('cmd', 'test_ivstate0')])]) + ]), + 'run', + {'order': 10004}]}, + } + ), + 'test_onfail_simple': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_vstate0', {'result': False}), + ('test_vstate1', {'result': True}), + ])), + ]), + { + 'test_vstate0': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}]}, + 'test_vstate1': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + salt.utils.odict.OrderedDict([ + ('onfail_stop', False), + ('onfail', + [salt.utils.odict.OrderedDict([('cmd', 'test_vstate0')])]) + ]), + 'run', + {'order': 10004}]}, + } + ), # order is different + 'test_onfail_simple_rev': ( + salt.utils.odict.OrderedDict([ + ('host1', + salt.utils.odict.OrderedDict([ + ('test_vstate0', {'result': False}), + ('test_vstate1', {'result': True}), + ])), + ]), + { + 'test_vstate0': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + 'run', + {'order': 10002}]}, + 'test_vstate1': { + '__env__': 'base', + '__sls__': u'a', + 'cmd': [salt.utils.odict.OrderedDict([('name', '/bin/true')]), + salt.utils.odict.OrderedDict([ + ('onfail', + [salt.utils.odict.OrderedDict([('cmd', 'test_vstate0')])]) + ]), + salt.utils.odict.OrderedDict([('onfail_stop', False)]), + 'run', + {'order': 10004}]}, + } + ) + } + for test, testdata in six.iteritems(test_valid_true_ht_states): + data, ht = testdata + for t_ in [a for a in data['host1']]: + tdata = data['host1'][t_] + if '_|-' in t_: + t_ = t_.split('_|-')[1] + tdata['__id__'] = t_ + self.assertTrue( + salt.utils.state.check_result(data, highstate=ht), + msg='{0} failed'.format(test)) + test_valid_false_state = {'host1': {'test_state': {'result': False}}} + self.assertFalse(salt.utils.state.check_result(test_valid_false_state)) + + +class UtilStateMergeSubreturnTestcase(TestCase): + ''' + Test cases for salt.utils.state.merge_subreturn function. + ''' + main_ret = { + 'name': 'primary', + # result may be missing, as primarysalt.utils.state is still in progress + 'comment': '', + 'changes': {}, + } + sub_ret = { + 'name': 'secondary', + 'result': True, + 'comment': '', + 'changes': {}, + } + + def test_merge_result(self): + # result not created if not needed + for no_effect_result in [True, None]: + m = copy.deepcopy(self.main_ret) + s = copy.deepcopy(self.sub_ret) + s['result'] = no_effect_result + res = salt.utils.state.merge_subreturn(m, s) + self.assertNotIn('result', res) + + # False subresult is propagated to existing result + for original_result in [True, None, False]: + m = copy.deepcopy(self.main_ret) + m['result'] = original_result + s = copy.deepcopy(self.sub_ret) + s['result'] = False + res = salt.utils.state.merge_subreturn(m, s) + self.assertFalse(res['result']) + + # False result cannot be overriden + for any_result in [True, None, False]: + m = copy.deepcopy(self.main_ret) + m['result'] = False + s = copy.deepcopy(self.sub_ret) + s['result'] = any_result + res = salt.utils.state.merge_subreturn(m, s) + self.assertFalse(res['result']) + + def test_merge_changes(self): + # The main changes dict should always already exist, + # and there should always be a changes dict in the secondary. + primary_changes = {'old': None, 'new': 'my_resource'} + secondary_changes = {'old': None, 'new': ['alarm-1', 'alarm-2']} + + # No changes case + m = copy.deepcopy(self.main_ret) + s = copy.deepcopy(self.sub_ret) + res = salt.utils.state.merge_subreturn(m, s) + self.assertDictEqual(res['changes'], {}) + + # New changes don't get rid of existing changes + m = copy.deepcopy(self.main_ret) + m['changes'] = copy.deepcopy(primary_changes) + s = copy.deepcopy(self.sub_ret) + s['changes'] = copy.deepcopy(secondary_changes) + res = salt.utils.state.merge_subreturn(m, s) + self.assertDictEqual(res['changes'], { + 'old': None, + 'new': 'my_resource', + 'secondary': secondary_changes, + }) + + # The subkey parameter is respected + m = copy.deepcopy(self.main_ret) + m['changes'] = copy.deepcopy(primary_changes) + s = copy.deepcopy(self.sub_ret) + s['changes'] = copy.deepcopy(secondary_changes) + res = salt.utils.state.merge_subreturn(m, s, subkey='alarms') + self.assertDictEqual(res['changes'], { + 'old': None, + 'new': 'my_resource', + 'alarms': secondary_changes, + }) + + def test_merge_pchanges(self): + primary_pchanges = {'old': None, 'new': 'my_resource'} + secondary_pchanges = {'old': None, 'new': ['alarm-1', 'alarm-2']} + + # Neither main nor sub pchanges case + m = copy.deepcopy(self.main_ret) + s = copy.deepcopy(self.sub_ret) + res = salt.utils.state.merge_subreturn(m, s) + self.assertNotIn('pchanges', res) + + # No main pchanges, sub pchanges + m = copy.deepcopy(self.main_ret) + s = copy.deepcopy(self.sub_ret) + s['pchanges'] = copy.deepcopy(secondary_pchanges) + res = salt.utils.state.merge_subreturn(m, s) + self.assertDictEqual(res['pchanges'], { + 'secondary': secondary_pchanges + }) + + # Main pchanges, no sub pchanges + m = copy.deepcopy(self.main_ret) + m['pchanges'] = copy.deepcopy(primary_pchanges) + s = copy.deepcopy(self.sub_ret) + res = salt.utils.state.merge_subreturn(m, s) + self.assertDictEqual(res['pchanges'], primary_pchanges) + + # Both main and sub pchanges, new pchanges don't affect existing ones + m = copy.deepcopy(self.main_ret) + m['pchanges'] = copy.deepcopy(primary_pchanges) + s = copy.deepcopy(self.sub_ret) + s['pchanges'] = copy.deepcopy(secondary_pchanges) + res = salt.utils.state.merge_subreturn(m, s) + self.assertDictEqual(res['pchanges'], { + 'old': None, + 'new': 'my_resource', + 'secondary': secondary_pchanges, + }) + + # The subkey parameter is respected + m = copy.deepcopy(self.main_ret) + m['pchanges'] = copy.deepcopy(primary_pchanges) + s = copy.deepcopy(self.sub_ret) + s['pchanges'] = copy.deepcopy(secondary_pchanges) + res = salt.utils.state.merge_subreturn(m, s, subkey='alarms') + self.assertDictEqual(res['pchanges'], { + 'old': None, + 'new': 'my_resource', + 'alarms': secondary_pchanges, + }) + + def test_merge_comments(self): + main_comment_1 = 'First primary comment.' + main_comment_2 = 'Second primary comment.' + sub_comment_1 = 'First secondary comment,\nwhich spans two lines.' + sub_comment_2 = 'Second secondary comment: {0}'.format( + 'some error\n And a traceback', + ) + final_comment = textwrap.dedent('''\ + First primary comment. + Second primary comment. + First secondary comment, + which spans two lines. + Second secondary comment: some error + And a traceback + '''.rstrip()) + + # Joining two strings + m = copy.deepcopy(self.main_ret) + m['comment'] = main_comment_1 + u'\n' + main_comment_2 + s = copy.deepcopy(self.sub_ret) + s['comment'] = sub_comment_1 + u'\n' + sub_comment_2 + res = salt.utils.state.merge_subreturn(m, s) + self.assertMultiLineEqual(res['comment'], final_comment) + + # Joining string and a list + m = copy.deepcopy(self.main_ret) + m['comment'] = main_comment_1 + u'\n' + main_comment_2 + s = copy.deepcopy(self.sub_ret) + s['comment'] = [sub_comment_1, sub_comment_2] + res = salt.utils.state.merge_subreturn(m, s) + self.assertMultiLineEqual(res['comment'], final_comment) + + # For tests where output is a list, + # also test that final joined output will match + # Joining list and a string + m = copy.deepcopy(self.main_ret) + m['comment'] = [main_comment_1, main_comment_2] + s = copy.deepcopy(self.sub_ret) + s['comment'] = sub_comment_1 + u'\n' + sub_comment_2 + res = salt.utils.state.merge_subreturn(m, s) + self.assertEqual(res['comment'], [ + main_comment_1, + main_comment_2, + sub_comment_1 + u'\n' + sub_comment_2, + ]) + self.assertMultiLineEqual(u'\n'.join(res['comment']), final_comment) + + # Joining two lists + m = copy.deepcopy(self.main_ret) + m['comment'] = [main_comment_1, main_comment_2] + s = copy.deepcopy(self.sub_ret) + s['comment'] = [sub_comment_1, sub_comment_2] + res = salt.utils.state.merge_subreturn(m, s) + self.assertEqual(res['comment'], [ + main_comment_1, + main_comment_2, + sub_comment_1, + sub_comment_2, + ]) + self.assertMultiLineEqual(u'\n'.join(res['comment']), final_comment) + + def test_merge_empty_comments(self): + # Since the primarysalt.utils.state is in progress, + # the main comment may be empty, either '' or []. + # Note that [''] is a degenerate case and should never happen, + # hence the behavior is left unspecified in that case. + # The secondary comment should never be empty, + # because thatsalt.utils.state has already returned, + # so we leave the behavior unspecified in that case. + sub_comment_1 = 'Secondary comment about changes:' + sub_comment_2 = 'A diff that goes with the previous comment' + # No contributions from primary + final_comment = sub_comment_1 + u'\n' + sub_comment_2 + + # Joining empty string and a string + m = copy.deepcopy(self.main_ret) + m['comment'] = '' + s = copy.deepcopy(self.sub_ret) + s['comment'] = sub_comment_1 + u'\n' + sub_comment_2 + res = salt.utils.state.merge_subreturn(m, s) + self.assertEqual(res['comment'], final_comment) + + # Joining empty string and a list + m = copy.deepcopy(self.main_ret) + m['comment'] = '' + s = copy.deepcopy(self.sub_ret) + s['comment'] = [sub_comment_1, sub_comment_2] + res = salt.utils.state.merge_subreturn(m, s) + self.assertEqual(res['comment'], final_comment) + + # For tests where output is a list, + # also test that final joined output will match + # Joining empty list and a string + m = copy.deepcopy(self.main_ret) + m['comment'] = [] + s = copy.deepcopy(self.sub_ret) + s['comment'] = sub_comment_1 + u'\n' + sub_comment_2 + res = salt.utils.state.merge_subreturn(m, s) + self.assertEqual(res['comment'], [final_comment]) + self.assertEqual(u'\n'.join(res['comment']), final_comment) + + # Joining empty list and a list + m = copy.deepcopy(self.main_ret) + m['comment'] = [] + s = copy.deepcopy(self.sub_ret) + s['comment'] = [sub_comment_1, sub_comment_2] + res = salt.utils.state.merge_subreturn(m, s) + self.assertEqual(res['comment'], [sub_comment_1, sub_comment_2]) + self.assertEqual(u'\n'.join(res['comment']), final_comment) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 87fe9b0018..f13e9cdf73 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -9,8 +9,7 @@ from __future__ import absolute_import # Import Salt Testing libs from tests.support.unit import TestCase, skipIf from tests.support.mock import ( - patch, DEFAULT, - create_autospec, + patch, NO_MOCK, NO_MOCK_REASON ) @@ -20,17 +19,15 @@ import salt.utils import salt.utils.jid import salt.utils.yamlencoding import salt.utils.zeromq -from salt.utils.odict import OrderedDict -from salt.exceptions import (SaltInvocationError, SaltSystemExit, CommandNotFoundError) +from salt.exceptions import SaltSystemExit, CommandNotFoundError # Import Python libraries import datetime +import os import yaml import zmq -from collections import namedtuple # Import 3rd-party libs -from salt.ext import six try: import timelib # pylint: disable=import-error,unused-import HAS_TIMELIB = True @@ -101,35 +98,6 @@ class UtilsTestCase(TestCase): ret = salt.utils.build_whitespace_split_regex(' '.join(LOREM_IPSUM.split()[:5])) self.assertEqual(ret, expected_regex) - def test_arg_lookup(self): - def dummy_func(first, second, third, fourth='fifth'): - pass - - expected_dict = {'args': ['first', 'second', 'third'], 'kwargs': {'fourth': 'fifth'}} - ret = salt.utils.arg_lookup(dummy_func) - self.assertEqual(expected_dict, ret) - - @skipIf(NO_MOCK, NO_MOCK_REASON) - def test_format_call(self): - with patch('salt.utils.arg_lookup') as arg_lookup: - def dummy_func(first=None, second=None, third=None): - pass - arg_lookup.return_value = {'args': ['first', 'second', 'third'], 'kwargs': {}} - get_function_argspec = DEFAULT - get_function_argspec.return_value = namedtuple('ArgSpec', 'args varargs keywords defaults')( - args=['first', 'second', 'third', 'fourth'], varargs=None, keywords=None, defaults=('fifth',)) - - # Make sure we raise an error if we don't pass in the requisite number of arguments - self.assertRaises(SaltInvocationError, salt.utils.format_call, dummy_func, {'1': 2}) - - # Make sure we warn on invalid kwargs - ret = salt.utils.format_call(dummy_func, {'first': 2, 'second': 2, 'third': 3}) - self.assertGreaterEqual(len(ret['warnings']), 1) - - ret = salt.utils.format_call(dummy_func, {'first': 2, 'second': 2, 'third': 3}, - expected_extra_kws=('first', 'second', 'third')) - self.assertDictEqual(ret, {'args': [], 'kwargs': {}}) - def test_isorted(self): test_list = ['foo', 'Foo', 'bar', 'Bar'] expected_list = ['bar', 'Bar', 'foo', 'Foo'] @@ -290,427 +258,6 @@ class UtilsTestCase(TestCase): self.assertEqual(salt.utils.sanitize_win_path_string('\\windows\\system'), '\\windows\\system') self.assertEqual(salt.utils.sanitize_win_path_string('\\bo:g|us\\p?at*h>'), '\\bo_g_us\\p_at_h_') - def test_check_state_result(self): - self.assertFalse(salt.utils.check_state_result(None), "Failed to handle None as an invalid data type.") - self.assertFalse(salt.utils.check_state_result([]), "Failed to handle an invalid data type.") - self.assertFalse(salt.utils.check_state_result({}), "Failed to handle an empty dictionary.") - self.assertFalse(salt.utils.check_state_result({'host1': []}), "Failed to handle an invalid host data structure.") - test_valid_state = {'host1': {'test_state': {'result': 'We have liftoff!'}}} - self.assertTrue(salt.utils.check_state_result(test_valid_state)) - test_valid_false_states = { - 'test1': OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': False}), - ])), - ]), - 'test2': OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': True}), - ])), - ('host2', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': False}), - ])), - ]), - 'test3': ['a'], - 'test4': OrderedDict([ - ('asup', OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': True}), - ])), - ('host2', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': False}), - ])) - ])) - ]), - 'test5': OrderedDict([ - ('asup', OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': True}), - ])), - ('host2', OrderedDict([])) - ])) - ]) - } - for test, data in six.iteritems(test_valid_false_states): - self.assertFalse( - salt.utils.check_state_result(data), - msg='{0} failed'.format(test)) - test_valid_true_states = { - 'test1': OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': True}), - ])), - ]), - 'test3': OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': True}), - ])), - ('host2', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': True}), - ])), - ]), - 'test4': OrderedDict([ - ('asup', OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': True}), - ])), - ('host2', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': True}), - ])) - ])) - ]), - 'test2': OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': None}), - ('test_state', {'result': True}), - ])), - ('host2', - OrderedDict([ - ('test_state0', {'result': True}), - ('test_state', {'result': 'abc'}), - ])) - ]) - } - for test, data in six.iteritems(test_valid_true_states): - self.assertTrue( - salt.utils.check_state_result(data), - msg='{0} failed'.format(test)) - test_invalid_true_ht_states = { - 'test_onfail_simple2': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('test_vstate0', {'result': False}), - ('test_vstate1', {'result': True}), - ])), - ]), - { - 'test_vstate0': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}]}, - 'test_vstate1': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - OrderedDict([ - ('onfail_stop', True), - ('onfail', - [OrderedDict([('cmd', 'test_vstate0')])]) - ]), - 'run', - {'order': 10004}]}, - } - ), - 'test_onfail_integ2': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('t_|-test_ivstate0_|-echo_|-run', { - 'result': False}), - ('cmd_|-test_ivstate0_|-echo_|-run', { - 'result': False}), - ('cmd_|-test_ivstate1_|-echo_|-run', { - 'result': False}), - ])), - ]), - { - 'test_ivstate0': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}], - 't': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}]}, - 'test_ivstate1': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - OrderedDict([ - ('onfail_stop', False), - ('onfail', - [OrderedDict([('cmd', 'test_ivstate0')])]) - ]), - 'run', - {'order': 10004}]}, - } - ), - 'test_onfail_integ3': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('t_|-test_ivstate0_|-echo_|-run', { - 'result': True}), - ('cmd_|-test_ivstate0_|-echo_|-run', { - 'result': False}), - ('cmd_|-test_ivstate1_|-echo_|-run', { - 'result': False}), - ])), - ]), - { - 'test_ivstate0': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}], - 't': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}]}, - 'test_ivstate1': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - OrderedDict([ - ('onfail_stop', False), - ('onfail', - [OrderedDict([('cmd', 'test_ivstate0')])]) - ]), - 'run', - {'order': 10004}]}, - } - ), - 'test_onfail_integ4': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('t_|-test_ivstate0_|-echo_|-run', { - 'result': False}), - ('cmd_|-test_ivstate0_|-echo_|-run', { - 'result': False}), - ('cmd_|-test_ivstate1_|-echo_|-run', { - 'result': True}), - ])), - ]), - { - 'test_ivstate0': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}], - 't': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}]}, - 'test_ivstate1': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - OrderedDict([ - ('onfail_stop', False), - ('onfail', - [OrderedDict([('cmd', 'test_ivstate0')])]) - ]), - 'run', - {'order': 10004}]}, - 'test_ivstate2': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - OrderedDict([ - ('onfail_stop', True), - ('onfail', - [OrderedDict([('cmd', 'test_ivstate0')])]) - ]), - 'run', - {'order': 10004}]}, - } - ), - 'test_onfail': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': False}), - ('test_state', {'result': True}), - ])), - ]), - None - ), - 'test_onfail_d': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('test_state0', {'result': False}), - ('test_state', {'result': True}), - ])), - ]), - {} - ) - } - for test, testdata in six.iteritems(test_invalid_true_ht_states): - data, ht = testdata - for t_ in [a for a in data['host1']]: - tdata = data['host1'][t_] - if '_|-' in t_: - t_ = t_.split('_|-')[1] - tdata['__id__'] = t_ - self.assertFalse( - salt.utils.check_state_result(data, highstate=ht), - msg='{0} failed'.format(test)) - - test_valid_true_ht_states = { - 'test_onfail_integ': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('cmd_|-test_ivstate0_|-echo_|-run', { - 'result': False}), - ('cmd_|-test_ivstate1_|-echo_|-run', { - 'result': True}), - ])), - ]), - { - 'test_ivstate0': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}]}, - 'test_ivstate1': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - OrderedDict([ - ('onfail_stop', False), - ('onfail', - [OrderedDict([('cmd', 'test_ivstate0')])]) - ]), - 'run', - {'order': 10004}]}, - } - ), - 'test_onfail_intega3': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('t_|-test_ivstate0_|-echo_|-run', { - 'result': True}), - ('cmd_|-test_ivstate0_|-echo_|-run', { - 'result': False}), - ('cmd_|-test_ivstate1_|-echo_|-run', { - 'result': True}), - ])), - ]), - { - 'test_ivstate0': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}], - 't': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}]}, - 'test_ivstate1': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - OrderedDict([ - ('onfail_stop', False), - ('onfail', - [OrderedDict([('cmd', 'test_ivstate0')])]) - ]), - 'run', - {'order': 10004}]}, - } - ), - 'test_onfail_simple': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('test_vstate0', {'result': False}), - ('test_vstate1', {'result': True}), - ])), - ]), - { - 'test_vstate0': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}]}, - 'test_vstate1': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - OrderedDict([ - ('onfail_stop', False), - ('onfail', - [OrderedDict([('cmd', 'test_vstate0')])]) - ]), - 'run', - {'order': 10004}]}, - } - ), # order is different - 'test_onfail_simple_rev': ( - OrderedDict([ - ('host1', - OrderedDict([ - ('test_vstate0', {'result': False}), - ('test_vstate1', {'result': True}), - ])), - ]), - { - 'test_vstate0': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - 'run', - {'order': 10002}]}, - 'test_vstate1': { - '__env__': 'base', - '__sls__': u'a', - 'cmd': [OrderedDict([('name', '/bin/true')]), - OrderedDict([ - ('onfail', - [OrderedDict([('cmd', 'test_vstate0')])]) - ]), - OrderedDict([('onfail_stop', False)]), - 'run', - {'order': 10004}]}, - } - ) - } - for test, testdata in six.iteritems(test_valid_true_ht_states): - data, ht = testdata - for t_ in [a for a in data['host1']]: - tdata = data['host1'][t_] - if '_|-' in t_: - t_ = t_.split('_|-')[1] - tdata['__id__'] = t_ - self.assertTrue( - salt.utils.check_state_result(data, highstate=ht), - msg='{0} failed'.format(test)) - test_valid_false_state = {'host1': {'test_state': {'result': False}}} - self.assertFalse(salt.utils.check_state_result(test_valid_false_state)) - @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not hasattr(zmq, 'IPC_PATH_MAX_LEN'), "ZMQ does not have max length support.") def test_check_ipc_length(self): @@ -805,17 +352,6 @@ class UtilsTestCase(TestCase): expected_ret = {'foo': {'new': 'woz', 'old': 'bar'}} self.assertDictEqual(ret, expected_ret) - @skipIf(NO_MOCK, NO_MOCK_REASON) - def test_argspec_report(self): - def _test_spec(arg1, arg2, kwarg1=None): - pass - - sys_mock = create_autospec(_test_spec) - test_functions = {'test_module.test_spec': sys_mock} - ret = salt.utils.argspec_report(test_functions, 'test_module.test_spec') - self.assertDictEqual(ret, {'test_module.test_spec': - {'kwargs': True, 'args': None, 'defaults': None, 'varargs': True}}) - def test_decode_list(self): test_data = [u'unicode_str', [u'unicode_item_in_list', 'second_item_in_list'], {'dict_key': u'dict_val'}] expected_ret = ['unicode_str', ['unicode_item_in_list', 'second_item_in_list'], {'dict_key': 'dict_val'}] @@ -895,17 +431,6 @@ class UtilsTestCase(TestCase): ret = salt.utils.repack_dictlist(LOREM_IPSUM) self.assertDictEqual(ret, {}) - def test_get_colors(self): - ret = salt.utils.get_colors() - self.assertEqual('\x1b[0;37m', str(ret['LIGHT_GRAY'])) - - ret = salt.utils.get_colors(use=False) - self.assertDictContainsSubset({'LIGHT_GRAY': ''}, ret) - - ret = salt.utils.get_colors(use='LIGHT_GRAY') - # LIGHT_YELLOW now == LIGHT_GRAY - self.assertEqual(str(ret['LIGHT_YELLOW']), str(ret['LIGHT_GRAY'])) - @skipIf(NO_MOCK, NO_MOCK_REASON) def test_daemonize_if(self): # pylint: disable=assignment-from-none @@ -930,8 +455,13 @@ class UtilsTestCase(TestCase): now = datetime.datetime(2002, 12, 25, 12, 00, 00, 00) with patch('datetime.datetime'): datetime.datetime.now.return_value = now - ret = salt.utils.jid.gen_jid() + ret = salt.utils.jid.gen_jid({}) self.assertEqual(ret, '20021225120000000000') + salt.utils.jid.LAST_JID_DATETIME = None + ret = salt.utils.jid.gen_jid({'unique_jid': True}) + self.assertEqual(ret, '20021225120000000000_{0}'.format(os.getpid())) + ret = salt.utils.jid.gen_jid({'unique_jid': True}) + self.assertEqual(ret, '20021225120000000001_{0}'.format(os.getpid())) @skipIf(NO_MOCK, NO_MOCK_REASON) def test_check_or_die(self): diff --git a/tests/unit/utils/test_verify.py b/tests/unit/utils/test_verify.py index d1bd3a2ea5..abba7ba83d 100644 --- a/tests/unit/utils/test_verify.py +++ b/tests/unit/utils/test_verify.py @@ -10,10 +10,15 @@ import os import sys import stat import shutil -import resource import tempfile import socket +# Import third party libs +if sys.platform.startswith('win'): + import win32file +else: + import resource + # Import Salt Testing libs from tests.support.unit import skipIf, TestCase from tests.support.paths import TMP @@ -82,7 +87,10 @@ class TestVerify(TestCase): writer = FakeWriter() sys.stderr = writer # Now run the test - self.assertFalse(check_user('nouser')) + if sys.platform.startswith('win'): + self.assertTrue(check_user('nouser')) + else: + self.assertFalse(check_user('nouser')) # Restore sys.stderr sys.stderr = stderr if writer.output != 'CRITICAL: User not found: "nouser"\n': @@ -118,7 +126,6 @@ class TestVerify(TestCase): # not support IPv6. pass - @skipIf(True, 'Skipping until we can find why Jenkins is bailing out') def test_max_open_files(self): with TestsLoggingHandler() as handler: logmsg_dbg = ( @@ -139,15 +146,31 @@ class TestVerify(TestCase): 'raise the salt\'s max_open_files setting. Please consider ' 'raising this value.' ) + if sys.platform.startswith('win'): + logmsg_crash = ( + '{0}:The number of accepted minion keys({1}) should be lower ' + 'than 1/4 of the max open files soft setting({2}). ' + 'salt-master will crash pretty soon! Please consider ' + 'raising this value.' + ) - mof_s, mof_h = resource.getrlimit(resource.RLIMIT_NOFILE) + if sys.platform.startswith('win'): + # Check the Windows API for more detail on this + # http://msdn.microsoft.com/en-us/library/xt874334(v=vs.71).aspx + # and the python binding http://timgolden.me.uk/pywin32-docs/win32file.html + mof_s = mof_h = win32file._getmaxstdio() + else: + mof_s, mof_h = resource.getrlimit(resource.RLIMIT_NOFILE) tempdir = tempfile.mkdtemp(prefix='fake-keys') keys_dir = os.path.join(tempdir, 'minions') os.makedirs(keys_dir) mof_test = 256 - resource.setrlimit(resource.RLIMIT_NOFILE, (mof_test, mof_h)) + if sys.platform.startswith('win'): + win32file._setmaxstdio(mof_test) + else: + resource.setrlimit(resource.RLIMIT_NOFILE, (mof_test, mof_h)) try: prev = 0 @@ -181,7 +204,7 @@ class TestVerify(TestCase): level, newmax, mof_test, - mof_h - newmax, + mof_test - newmax if sys.platform.startswith('win') else mof_h - newmax, ), handler.messages ) @@ -206,7 +229,7 @@ class TestVerify(TestCase): 'CRITICAL', newmax, mof_test, - mof_h - newmax, + mof_test - newmax if sys.platform.startswith('win') else mof_h - newmax, ), handler.messages ) @@ -218,7 +241,10 @@ class TestVerify(TestCase): raise finally: shutil.rmtree(tempdir) - resource.setrlimit(resource.RLIMIT_NOFILE, (mof_s, mof_h)) + if sys.platform.startswith('win'): + win32file._setmaxstdio(mof_h) + else: + resource.setrlimit(resource.RLIMIT_NOFILE, (mof_s, mof_h)) @skipIf(NO_MOCK, NO_MOCK_REASON) def test_verify_log(self): diff --git a/tests/unit/utils/test_vsan.py b/tests/unit/utils/test_vsan.py new file mode 100644 index 0000000000..9d76d6dcae --- /dev/null +++ b/tests/unit/utils/test_vsan.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu ` + + Tests functions in salt.utils.vsan +''' + +# Import python libraries +from __future__ import absolute_import +import logging + +# Import Salt testing libraries +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, \ + PropertyMock + +# Import Salt libraries +from salt.exceptions import VMwareApiError, VMwareRuntimeError +from salt.utils import vsan + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False +HAS_PYVSAN = vsan.HAS_PYVSAN + + +# Get Logging Started +log = logging.getLogger(__name__) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class VsanSupportedTestCase(TestCase): + '''Tests for salt.utils.vsan.vsan_supported''' + + def test_supported_api_version(self): + mock_si = MagicMock(content=MagicMock(about=MagicMock())) + type(mock_si.content.about).apiVersion = \ + PropertyMock(return_value='6.0') + self.assertTrue(vsan.vsan_supported(mock_si)) + + def test_unsupported_api_version(self): + mock_si = MagicMock(content=MagicMock(about=MagicMock())) + type(mock_si.content.about).apiVersion = \ + PropertyMock(return_value='5.0') + self.assertFalse(vsan.vsan_supported(mock_si)) + + def test_api_version_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + mock_si = MagicMock(content=MagicMock(about=MagicMock())) + type(mock_si.content.about).apiVersion = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.vsan_supported(mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_api_version_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + mock_si = MagicMock(content=MagicMock(about=MagicMock())) + type(mock_si.content.about).apiVersion = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.vsan_supported(mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_api_version_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + mock_si = MagicMock(content=MagicMock(about=MagicMock())) + type(mock_si.content.about).apiVersion = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan.vsan_supported(mock_si) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class GetVsanClusterConfigSystemTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.utils.vsan.get_vsan_cluster_config_system''' + def setup_loader_modules(self): + return {vsan: { + '__virtual__': MagicMock(return_value='vsan'), + 'sys': MagicMock(), + 'ssl': MagicMock()}} + + def setUp(self): + self.mock_si = MagicMock() + self.mock_ret = MagicMock() + patches = (('salt.utils.vsan.vsanapiutils.GetVsanVcMos', + MagicMock( + return_value={'vsan-cluster-config-system': + self.mock_ret})),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + type(vsan.sys).version_info = PropertyMock(return_value=(2, 7, 9)) + self.mock_context = MagicMock() + self.mock_create_default_context = \ + MagicMock(return_value=self.mock_context) + vsan.ssl.create_default_context = self.mock_create_default_context + + def tearDown(self): + for attr in ('mock_si', 'mock_ret', 'mock_context', + 'mock_create_default_context'): + delattr(self, attr) + + def test_ssl_default_context_loaded(self): + vsan.get_vsan_cluster_config_system(self.mock_si) + self.mock_create_default_context.assert_called_once_with() + self.assertFalse(self.mock_context.check_hostname) + self.assertEqual(self.mock_context.verify_mode, vsan.ssl.CERT_NONE) + + def test_ssl_default_context_not_loaded(self): + type(vsan.sys).version_info = PropertyMock(return_value=(2, 7, 8)) + vsan.get_vsan_cluster_config_system(self.mock_si) + self.assertEqual(self.mock_create_default_context.call_count, 0) + + def test_GetVsanVcMos_call(self): + mock_get_vsan_vc_mos = MagicMock() + with patch('salt.utils.vsan.vsanapiutils.GetVsanVcMos', + mock_get_vsan_vc_mos): + vsan.get_vsan_cluster_config_system(self.mock_si) + mock_get_vsan_vc_mos.assert_called_once_with(self.mock_si._stub, + context=self.mock_context) + + def test_return(self): + ret = vsan.get_vsan_cluster_config_system(self.mock_si) + self.assertEqual(ret, self.mock_ret) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class GetClusterVsanInfoTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.utils.vsan.get_cluster_vsan_info''' + def setup_loader_modules(self): + return {vsan: { + '__virtual__': MagicMock(return_value='vsan')}} + + def setUp(self): + self.mock_cl_ref = MagicMock() + self.mock_si = MagicMock() + patches = ( + ('salt.utils.vmware.get_managed_object_name', MagicMock()), + ('salt.utils.vmware.get_service_instance_from_managed_object', + MagicMock(return_value=self.mock_si)), + ('salt.utils.vsan.get_vsan_cluster_config_system', MagicMock())) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_cl_ref'): + delattr(self, attr) + + def test_get_managed_object_name_call(self): + mock_get_managed_object_name = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vsan.get_cluster_vsan_info(self.mock_cl_ref) + mock_get_managed_object_name.assert_called_once_with(self.mock_cl_ref) + + def test_get_vsan_cluster_config_system_call(self): + mock_get_vsan_cl_syst = MagicMock() + with patch('salt.utils.vsan.get_vsan_cluster_config_system', + mock_get_vsan_cl_syst): + vsan.get_cluster_vsan_info(self.mock_cl_ref) + mock_get_vsan_cl_syst.assert_called_once_with(self.mock_si) + + def test_VsanClusterGetConfig_call(self): + mock_vsan_sys = MagicMock() + with patch('salt.utils.vsan.get_vsan_cluster_config_system', + MagicMock(return_value=mock_vsan_sys)): + vsan.get_cluster_vsan_info(self.mock_cl_ref) + mock_vsan_sys.VsanClusterGetConfig.assert_called_once_with( + self.mock_cl_ref) + + def test_VsanClusterGetConfig_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + with patch('salt.utils.vsan.get_vsan_cluster_config_system', + MagicMock(return_value=MagicMock( + VsanClusterGetConfig=MagicMock(side_effect=exc)))): + with self.assertRaises(VMwareApiError) as excinfo: + vsan.get_cluster_vsan_info(self.mock_cl_ref) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_VsanClusterGetConfig_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + with patch('salt.utils.vsan.get_vsan_cluster_config_system', + MagicMock(return_value=MagicMock( + VsanClusterGetConfig=MagicMock(side_effect=exc)))): + with self.assertRaises(VMwareApiError) as excinfo: + vsan.get_cluster_vsan_info(self.mock_cl_ref) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_VsanClusterGetConfig_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + with patch('salt.utils.vsan.get_vsan_cluster_config_system', + MagicMock(return_value=MagicMock( + VsanClusterGetConfig=MagicMock(side_effect=exc)))): + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan.get_cluster_vsan_info(self.mock_cl_ref) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class ReconfigureClusterVsanTestCase(TestCase): + '''Tests for salt.utils.vsan.reconfigure_cluster_vsan''' + def setUp(self): + self.mock_si = MagicMock() + self.mock_task = MagicMock() + self.mock_cl_reconf = MagicMock(return_value=self.mock_task) + self.mock_get_vsan_conf_sys = MagicMock( + return_value=MagicMock(VsanClusterReconfig=self.mock_cl_reconf)) + self.mock_cl_ref = MagicMock() + self.mock_cl_vsan_spec = MagicMock() + patches = ( + ('salt.utils.vmware.get_managed_object_name', MagicMock()), + ('salt.utils.vmware.get_service_instance_from_managed_object', + MagicMock(return_value=self.mock_si)), + ('salt.utils.vsan.get_vsan_cluster_config_system', + self.mock_get_vsan_conf_sys), + ('salt.utils.vsan._wait_for_tasks', MagicMock())) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_cl_reconf', 'mock_get_vsan_conf_sys', + 'mock_cl_ref', 'mock_cl_vsan_spec', 'mock_task'): + delattr(self, attr) + + def test_get_cluster_name_call(self): + get_managed_object_name_mock = MagicMock() + with patch('salt.utils.vmware.get_managed_object_name', + get_managed_object_name_mock): + + vsan.reconfigure_cluster_vsan(self.mock_cl_ref, + self.mock_cl_vsan_spec) + get_managed_object_name_mock.assert_called_once_with( + self.mock_cl_ref) + + def test_get_service_instance_call(self): + get_service_instance_from_managed_object_mock = MagicMock() + with patch( + 'salt.utils.vmware.get_service_instance_from_managed_object', + get_service_instance_from_managed_object_mock): + + vsan.reconfigure_cluster_vsan(self.mock_cl_ref, + self.mock_cl_vsan_spec) + get_service_instance_from_managed_object_mock.assert_called_once_with( + self.mock_cl_ref) + + def test_get_vsan_cluster_config_system_call(self): + vsan.reconfigure_cluster_vsan(self.mock_cl_ref, + self.mock_cl_vsan_spec) + self.mock_get_vsan_conf_sys.assert_called_once_with(self.mock_si) + + def test_cluster_reconfig_call(self): + vsan.reconfigure_cluster_vsan(self.mock_cl_ref, + self.mock_cl_vsan_spec) + self.mock_cl_reconf.assert_called_once_with( + self.mock_cl_ref, self.mock_cl_vsan_spec) + + def test_cluster_reconfig_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + with patch('salt.utils.vsan.get_vsan_cluster_config_system', + MagicMock(return_value=MagicMock( + VsanClusterReconfig=MagicMock(side_effect=exc)))): + with self.assertRaises(VMwareApiError) as excinfo: + vsan.reconfigure_cluster_vsan(self.mock_cl_ref, + self.mock_cl_vsan_spec) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_cluster_reconfig_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + with patch('salt.utils.vsan.get_vsan_cluster_config_system', + MagicMock(return_value=MagicMock( + VsanClusterReconfig=MagicMock(side_effect=exc)))): + with self.assertRaises(VMwareApiError) as excinfo: + vsan.reconfigure_cluster_vsan(self.mock_cl_ref, + self.mock_cl_vsan_spec) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_cluster_reconfig_raises_vmodl_runtime_error(self): + exc = vmodl.RuntimeFault() + exc.msg = 'VimRuntime msg' + with patch('salt.utils.vsan.get_vsan_cluster_config_system', + MagicMock(return_value=MagicMock( + VsanClusterReconfig=MagicMock(side_effect=exc)))): + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan.reconfigure_cluster_vsan(self.mock_cl_ref, + self.mock_cl_vsan_spec) + self.assertEqual(excinfo.exception.strerror, 'VimRuntime msg') + + def test__wait_for_tasks_call(self): + mock_wait_for_tasks = MagicMock() + with patch('salt.utils.vsan._wait_for_tasks', mock_wait_for_tasks): + vsan.reconfigure_cluster_vsan(self.mock_cl_ref, + self.mock_cl_vsan_spec) + mock_wait_for_tasks.assert_called_once_with([self.mock_task], + self.mock_si) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class _WaitForTasks(TestCase, LoaderModuleMockMixin): + '''Tests for salt.utils.vsan._wait_for_tasks''' + def setup_loader_modules(self): + return {vsan: { + '__virtual__': MagicMock(return_value='vsan')}} + + def setUp(self): + self.mock_si = MagicMock() + self.mock_tasks = MagicMock() + patches = (('salt.utils.vsan.vsanapiutils.WaitForTasks', MagicMock()),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_tasks'): + delattr(self, attr) + + def test_wait_for_tasks_call(self): + mock_wait_for_tasks = MagicMock() + with patch('salt.utils.vsan.vsanapiutils.WaitForTasks', + mock_wait_for_tasks): + vsan._wait_for_tasks(self.mock_tasks, self.mock_si) + mock_wait_for_tasks.assert_called_once_with(self.mock_tasks, + self.mock_si) + + def test_wait_for_tasks_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + with patch('salt.utils.vsan.vsanapiutils.WaitForTasks', + MagicMock(side_effect=exc)): + with self.assertRaises(VMwareApiError) as excinfo: + vsan._wait_for_tasks(self.mock_tasks, self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_wait_for_tasks_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + with patch('salt.utils.vsan.vsanapiutils.WaitForTasks', + MagicMock(side_effect=exc)): + with self.assertRaises(VMwareApiError) as excinfo: + vsan._wait_for_tasks(self.mock_tasks, self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_wait_for_tasks_raises_vmodl_runtime_error(self): + exc = vmodl.RuntimeFault() + exc.msg = 'VimRuntime msg' + with patch('salt.utils.vsan.vsanapiutils.WaitForTasks', + MagicMock(side_effect=exc)): + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan._wait_for_tasks(self.mock_tasks, self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimRuntime msg') diff --git a/tests/unit/utils/vmware/test_common.py b/tests/unit/utils/vmware/test_common.py index fd50ed12d0..5a946e8aa9 100644 --- a/tests/unit/utils/vmware/test_common.py +++ b/tests/unit/utils/vmware/test_common.py @@ -547,6 +547,8 @@ class GetPropertiesOfManagedObjectTestCase(TestCase): 'retrieved', excinfo.exception.strerror) +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class GetManagedObjectName(TestCase): '''Tests for salt.utils.get_managed_object_name''' @@ -900,3 +902,51 @@ class GetRootFolderTestCase(TestCase): def test_return(self): ret = salt.utils.vmware.get_root_folder(self.mock_si) self.assertEqual(ret, self.mock_root_folder) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetServiceInfoTestCase(TestCase): + '''Tests for salt.utils.vmware.get_service_info''' + def setUp(self): + self.mock_about = MagicMock() + self.mock_si = MagicMock(content=MagicMock()) + type(self.mock_si.content).about = \ + PropertyMock(return_value=self.mock_about) + + def tearDown(self): + for attr in ('mock_si', 'mock_about'): + delattr(self, attr) + + def test_about_ret(self): + ret = salt.utils.vmware.get_service_info(self.mock_si) + self.assertEqual(ret, self.mock_about) + + def test_about_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_si.content).about = \ + PropertyMock(side_effect=exc) + with self.assertRaises(excs.VMwareApiError) as excinfo: + salt.utils.vmware.get_service_info(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_about_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_si.content).about = \ + PropertyMock(side_effect=exc) + with self.assertRaises(excs.VMwareApiError) as excinfo: + salt.utils.vmware.get_service_info(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_about_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_si.content).about = \ + PropertyMock(side_effect=exc) + with self.assertRaises(excs.VMwareRuntimeError) as excinfo: + salt.utils.vmware.get_service_info(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') diff --git a/tests/unit/utils/vmware/test_license.py b/tests/unit/utils/vmware/test_license.py new file mode 100644 index 0000000000..471cb0b4c1 --- /dev/null +++ b/tests/unit/utils/vmware/test_license.py @@ -0,0 +1,663 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu ` + + Tests for license related functions in salt.utils.vmware +''' + +# Import python libraries +from __future__ import absolute_import +import logging + +# Import Salt testing libraries +from tests.support.unit import TestCase, skipIf +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, \ + PropertyMock + + +# Import Salt libraries +import salt.utils.vmware +from salt.exceptions import VMwareObjectRetrievalError, VMwareApiError, \ + VMwareRuntimeError + +# Import Third Party Libs +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + +# Get Logging Started +log = logging.getLogger(__name__) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetLicenseManagerTestCase(TestCase): + '''Tests for salt.utils.vmware.get_license_manager''' + + def setUp(self): + self.mock_si = MagicMock() + self.mock_lic_mgr = MagicMock() + type(self.mock_si.content).licenseManager = PropertyMock( + return_value=self.mock_lic_mgr) + + def tearDown(self): + for attr in ('mock_si', 'mock_lic_mgr'): + delattr(self, attr) + + def test_raise_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_si.content).licenseManager = PropertyMock( + side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_license_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_raise_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_si.content).licenseManager = PropertyMock( + side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_license_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_raise_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_si.content).licenseManager = PropertyMock( + side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.vmware.get_license_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_valid_assignment_manager(self): + ret = salt.utils.vmware.get_license_manager(self.mock_si) + self.assertEqual(ret, self.mock_lic_mgr) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetLicenseAssignmentManagerTestCase(TestCase): + '''Tests for salt.utils.vmware.get_license_assignment_manager''' + + def setUp(self): + self.mock_si = MagicMock() + self.mock_lic_assign_mgr = MagicMock() + type(self.mock_si.content.licenseManager).licenseAssignmentManager = \ + PropertyMock(return_value=self.mock_lic_assign_mgr) + + def tearDown(self): + for attr in ('mock_si', 'mock_lic_assign_mgr'): + delattr(self, attr) + + def test_raise_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_si.content.licenseManager).licenseAssignmentManager = \ + PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_license_assignment_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_raise_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_si.content.licenseManager).licenseAssignmentManager = \ + PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_license_assignment_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_raise_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_si.content.licenseManager).licenseAssignmentManager = \ + PropertyMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.vmware.get_license_assignment_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_empty_license_assignment_manager(self): + type(self.mock_si.content.licenseManager).licenseAssignmentManager = \ + PropertyMock(return_value=None) + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + salt.utils.vmware.get_license_assignment_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'License assignment manager was not retrieved') + + def test_valid_assignment_manager(self): + ret = salt.utils.vmware.get_license_assignment_manager(self.mock_si) + self.assertEqual(ret, self.mock_lic_assign_mgr) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetLicensesTestCase(TestCase): + '''Tests for salt.utils.vmware.get_licenses''' + + def setUp(self): + self.mock_si = MagicMock() + self.mock_licenses = [MagicMock(), MagicMock()] + self.mock_lic_mgr = MagicMock() + type(self.mock_lic_mgr).licenses = \ + PropertyMock(return_value=self.mock_licenses) + patches = ( + ('salt.utils.vmware.get_license_manager', + MagicMock(return_value=self.mock_lic_mgr)),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_lic_mgr', 'mock_licenses'): + delattr(self, attr) + + def test_no_license_manager_passed_in(self): + mock_get_license_manager = MagicMock() + with patch('salt.utils.vmware.get_license_manager', + mock_get_license_manager): + salt.utils.vmware.get_licenses(self.mock_si) + mock_get_license_manager.assert_called_once_with(self.mock_si) + + def test_license_manager_passed_in(self): + mock_licenses = PropertyMock() + mock_lic_mgr = MagicMock() + type(mock_lic_mgr).licenses = mock_licenses + mock_get_license_manager = MagicMock() + with patch('salt.utils.vmware.get_license_manager', + mock_get_license_manager): + salt.utils.vmware.get_licenses(self.mock_si, + license_manager=mock_lic_mgr) + self.assertEqual(mock_get_license_manager.call_count, 0) + self.assertEqual(mock_licenses.call_count, 1) + + def test_raise_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_lic_mgr).licenses = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_licenses(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_raise_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_lic_mgr).licenses = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_licenses(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_raise_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_lic_mgr).licenses = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.vmware.get_licenses(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_valid_licenses(self): + ret = salt.utils.vmware.get_licenses(self.mock_si) + self.assertEqual(ret, self.mock_licenses) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class AddLicenseTestCase(TestCase): + '''Tests for salt.utils.vmware.add_license''' + + def setUp(self): + self.mock_si = MagicMock() + self.mock_license = MagicMock() + self.mock_add_license = MagicMock(return_value=self.mock_license) + self.mock_lic_mgr = MagicMock(AddLicense=self.mock_add_license) + self.mock_label = MagicMock() + patches = ( + ('salt.utils.vmware.get_license_manager', + MagicMock(return_value=self.mock_lic_mgr)), + ('salt.utils.vmware.vim.KeyValue', + MagicMock(return_value=self.mock_label))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_lic_mgr', 'mock_license', + 'mock_add_license', 'mock_label'): + delattr(self, attr) + + def test_no_license_manager_passed_in(self): + mock_get_license_manager = MagicMock() + with patch('salt.utils.vmware.get_license_manager', + mock_get_license_manager): + salt.utils.vmware.add_license(self.mock_si, + 'fake_license_key', + 'fake_license_description') + mock_get_license_manager.assert_called_once_with(self.mock_si) + + def test_license_manager_passed_in(self): + mock_get_license_manager = MagicMock() + with patch('salt.utils.vmware.get_license_manager', + mock_get_license_manager): + salt.utils.vmware.add_license(self.mock_si, + 'fake_license_key', + 'fake_license_description', + license_manager=self.mock_lic_mgr) + self.assertEqual(mock_get_license_manager.call_count, 0) + self.assertEqual(self.mock_add_license.call_count, 1) + + def test_label_settings(self): + salt.utils.vmware.add_license(self.mock_si, + 'fake_license_key', + 'fake_license_description') + self.assertEqual(self.mock_label.key, 'VpxClientLicenseLabel') + self.assertEqual(self.mock_label.value, 'fake_license_description') + + def test_add_license_arguments(self): + salt.utils.vmware.add_license(self.mock_si, + 'fake_license_key', + 'fake_license_description') + self.mock_add_license.assert_called_once_with('fake_license_key', + [self.mock_label]) + + def test_add_license_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_lic_mgr.AddLicense = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.add_license(self.mock_si, + 'fake_license_key', + 'fake_license_description') + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_add_license_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_lic_mgr.AddLicense = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.add_license(self.mock_si, + 'fake_license_key', + 'fake_license_description') + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_add_license_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_lic_mgr.AddLicense = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.vmware.add_license(self.mock_si, + 'fake_license_key', + 'fake_license_description') + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_valid_license_added(self): + ret = salt.utils.vmware.add_license(self.mock_si, + 'fake_license_key', + 'fake_license_description') + self.assertEqual(ret, self.mock_license) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetAssignedLicensesTestCase(TestCase): + '''Tests for salt.utils.vmware.get_assigned_licenses''' + + def setUp(self): + self.mock_ent_id = MagicMock() + self.mock_si = MagicMock() + type(self.mock_si.content.about).instanceUuid = \ + PropertyMock(return_value=self.mock_ent_id) + self.mock_moid = MagicMock() + self.prop_mock_moid = PropertyMock(return_value=self.mock_moid) + self.mock_entity_ref = MagicMock() + type(self.mock_entity_ref)._moId = self.prop_mock_moid + self.mock_assignments = [MagicMock(entityDisplayName='fake_ent1'), + MagicMock(entityDisplayName='fake_ent2')] + self.mock_query_assigned_licenses = MagicMock( + return_value=[MagicMock(assignedLicense=self.mock_assignments[0]), + MagicMock(assignedLicense=self.mock_assignments[1])]) + self.mock_lic_assign_mgr = MagicMock( + QueryAssignedLicenses=self.mock_query_assigned_licenses) + patches = ( + ('salt.utils.vmware.get_license_assignment_manager', + MagicMock(return_value=self.mock_lic_assign_mgr)),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_ent_id', 'mock_si', 'mock_moid', 'prop_mock_moid', + 'mock_entity_ref', 'mock_assignments', + 'mock_query_assigned_licenses', 'mock_lic_assign_mgr'): + delattr(self, attr) + + def test_no_license_assignment_manager_passed_in(self): + mock_get_license_assign_manager = MagicMock() + with patch('salt.utils.vmware.get_license_assignment_manager', + mock_get_license_assign_manager): + salt.utils.vmware.get_assigned_licenses(self.mock_si, + self.mock_entity_ref, + 'fake_entity_name') + mock_get_license_assign_manager.assert_called_once_with(self.mock_si) + + def test_license_assignment_manager_passed_in(self): + mock_get_license_assign_manager = MagicMock() + with patch('salt.utils.vmware.get_license_assignment_manager', + mock_get_license_assign_manager): + salt.utils.vmware.get_assigned_licenses( + self.mock_si, self.mock_entity_ref, 'fake_entity_name', + license_assignment_manager=self.mock_lic_assign_mgr) + self.assertEqual(mock_get_license_assign_manager.call_count, 0) + + def test_entity_name(self): + mock_trace = MagicMock() + with patch('salt.log.setup.SaltLoggingClass.trace', mock_trace): + salt.utils.vmware.get_assigned_licenses(self.mock_si, + self.mock_entity_ref, + 'fake_entity_name') + mock_trace.assert_called_once_with('Retrieving licenses assigned to ' + '\'fake_entity_name\'') + + def test_instance_uuid(self): + mock_instance_uuid_prop = PropertyMock() + type(self.mock_si.content.about).instanceUuid = mock_instance_uuid_prop + self.mock_lic_assign_mgr.QueryAssignedLicenses = MagicMock( + return_value=[MagicMock(entityDisplayName='fake_vcenter')]) + salt.utils.vmware.get_assigned_licenses(self.mock_si, + entity_name='fake_vcenter') + self.assertEqual(mock_instance_uuid_prop.call_count, 1) + + def test_instance_uuid_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_si.content.about).instanceUuid = \ + PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_assigned_licenses(self.mock_si, + entity_name='fake_vcenter') + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_instance_uuid_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_si.content.about).instanceUuid = \ + PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_assigned_licenses(self.mock_si, + entity_name='fake_vcenter') + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_instance_uuid_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_si.content.about).instanceUuid = \ + PropertyMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.vmware.get_assigned_licenses(self.mock_si, + entity_name='fake_vcenter') + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_vcenter_entity_too_many_assignements(self): + self.mock_lic_assign_mgr.QueryAssignedLicenses = MagicMock( + return_value=[MagicMock(), MagicMock()]) + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + salt.utils.vmware.get_assigned_licenses(self.mock_si, + entity_name='fake_vcenter') + self.assertEqual(excinfo.exception.strerror, + 'Unexpected return. Expect only a single assignment') + + def test_wrong_vcenter_name(self): + self.mock_lic_assign_mgr.QueryAssignedLicenses = MagicMock( + return_value=[MagicMock(entityDisplayName='bad_vcenter')]) + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + salt.utils.vmware.get_assigned_licenses(self.mock_si, + entity_name='fake_vcenter') + self.assertEqual(excinfo.exception.strerror, + 'Got license assignment info for a different vcenter') + + def test_query_assigned_licenses_vcenter(self): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + salt.utils.vmware.get_assigned_licenses(self.mock_si, + entity_name='fake_vcenter') + self.mock_query_assigned_licenses.assert_called_once_with( + self.mock_ent_id) + + def test_query_assigned_licenses_with_entity(self): + salt.utils.vmware.get_assigned_licenses(self.mock_si, + self.mock_entity_ref, + 'fake_entity_name') + self.mock_query_assigned_licenses.assert_called_once_with( + self.mock_moid) + + def test_query_assigned_licenses_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_lic_assign_mgr.QueryAssignedLicenses = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_assigned_licenses(self.mock_si, + self.mock_entity_ref, + 'fake_entity_name') + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_query_assigned_licenses_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_lic_assign_mgr.QueryAssignedLicenses = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.get_assigned_licenses(self.mock_si, + self.mock_entity_ref, + 'fake_entity_name') + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_query_assigned_licenses_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_lic_assign_mgr.QueryAssignedLicenses = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.vmware.get_assigned_licenses(self.mock_si, + self.mock_entity_ref, + 'fake_entity_name') + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_valid_assignments(self): + ret = salt.utils.vmware.get_assigned_licenses(self.mock_si, + self.mock_entity_ref, + 'fake_entity_name') + self.assertEqual(ret, self.mock_assignments) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class AssignLicenseTestCase(TestCase): + '''Tests for salt.utils.vmware.assign_license''' + + def setUp(self): + self.mock_ent_id = MagicMock() + self.mock_si = MagicMock() + type(self.mock_si.content.about).instanceUuid = \ + PropertyMock(return_value=self.mock_ent_id) + self.mock_lic_key = MagicMock() + self.mock_moid = MagicMock() + self.prop_mock_moid = PropertyMock(return_value=self.mock_moid) + self.mock_entity_ref = MagicMock() + type(self.mock_entity_ref)._moId = self.prop_mock_moid + self.mock_license = MagicMock() + self.mock_update_assigned_license = MagicMock( + return_value=self.mock_license) + self.mock_lic_assign_mgr = MagicMock( + UpdateAssignedLicense=self.mock_update_assigned_license) + patches = ( + ('salt.utils.vmware.get_license_assignment_manager', + MagicMock(return_value=self.mock_lic_assign_mgr)),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def test_no_license_assignment_manager_passed_in(self): + mock_get_license_assign_manager = MagicMock() + with patch('salt.utils.vmware.get_license_assignment_manager', + mock_get_license_assign_manager): + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + self.mock_entity_ref, + 'fake_entity_name') + mock_get_license_assign_manager.assert_called_once_with(self.mock_si) + + def test_license_assignment_manager_passed_in(self): + mock_get_license_assign_manager = MagicMock() + with patch('salt.utils.vmware.get_license_assignment_manager', + mock_get_license_assign_manager): + salt.utils.vmware.assign_license( + self.mock_si, self.mock_lic_key, 'fake_license_name', + self.mock_entity_ref, 'fake_entity_name', + license_assignment_manager=self.mock_lic_assign_mgr) + self.assertEqual(mock_get_license_assign_manager.call_count, 0) + self.assertEqual(self.mock_update_assigned_license.call_count, 1) + + def test_entity_name(self): + mock_trace = MagicMock() + with patch('salt.log.setup.SaltLoggingClass.trace', mock_trace): + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + self.mock_entity_ref, + 'fake_entity_name') + mock_trace.assert_called_once_with('Assigning license to ' + '\'fake_entity_name\'') + + def test_instance_uuid(self): + mock_instance_uuid_prop = PropertyMock() + type(self.mock_si.content.about).instanceUuid = mock_instance_uuid_prop + self.mock_lic_assign_mgr.UpdateAssignedLicense = MagicMock( + return_value=[MagicMock(entityDisplayName='fake_vcenter')]) + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + entity_name='fake_entity_name') + self.assertEqual(mock_instance_uuid_prop.call_count, 1) + + def test_instance_uuid_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_si.content.about).instanceUuid = \ + PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + entity_name='fake_entity_name') + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_instance_uuid_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_si.content.about).instanceUuid = \ + PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + entity_name='fake_entity_name') + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_instance_uuid_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_si.content.about).instanceUuid = \ + PropertyMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + entity_name='fake_entity_name') + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_update_assigned_licenses_vcenter(self): + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + entity_name='fake_entity_name') + self.mock_update_assigned_license.assert_called_once_with( + self.mock_ent_id, self.mock_lic_key) + + def test_update_assigned_licenses_call_with_entity(self): + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + self.mock_entity_ref, + 'fake_entity_name') + self.mock_update_assigned_license.assert_called_once_with( + self.mock_moid, self.mock_lic_key) + + def test_update_assigned_licenses_raises_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_lic_assign_mgr.UpdateAssignedLicense = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + self.mock_entity_ref, + 'fake_entity_name') + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_update_assigned_licenses_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_lic_assign_mgr.UpdateAssignedLicense = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + self.mock_entity_ref, + 'fake_entity_name') + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_update_assigned_licenses_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_lic_assign_mgr.UpdateAssignedLicense = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + self.mock_entity_ref, + 'fake_entity_name') + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_valid_assignments(self): + ret = salt.utils.vmware.assign_license(self.mock_si, + self.mock_lic_key, + 'fake_license_name', + self.mock_entity_ref, + 'fake_entity_name') + self.assertEqual(ret, self.mock_license) diff --git a/tests/unit/utils/vmware/test_storage.py b/tests/unit/utils/vmware/test_storage.py new file mode 100644 index 0000000000..43434225ae --- /dev/null +++ b/tests/unit/utils/vmware/test_storage.py @@ -0,0 +1,396 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu ` + + Tests for storage related functions in salt.utils.vmware +''' + +# Import python libraries +from __future__ import absolute_import +import logging + +# Import Salt testing libraries +from tests.support.unit import TestCase, skipIf +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, call +from salt.exceptions import VMwareObjectRetrievalError, VMwareApiError, \ + ArgumentValueError, VMwareRuntimeError + +#i Import Salt libraries +import salt.utils.vmware +# Import Third Party Libs +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + +# Get Logging Started +log = logging.getLogger(__name__) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetStorageSystemTestCase(TestCase): + '''Tests for salt.utils.vmware.get_storage_system''' + def setUp(self): + self.mock_si = MagicMock(content=MagicMock()) + self.mock_host_ref = MagicMock() + self.mock_get_managed_object_name = MagicMock(return_value='fake_host') + self.mock_traversal_spec = MagicMock() + self.mock_obj = MagicMock() + self.mock_get_mors = \ + MagicMock(return_value=[{'object': self.mock_obj}]) + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + self.mock_get_managed_object_name), + ('salt.utils.vmware.get_mors_with_properties', + self.mock_get_mors), + ('salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + MagicMock(return_value=self.mock_traversal_spec))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_host_ref', + 'mock_get_managed_object_name', + 'mock_traversal_spec', 'mock_obj'): + delattr(self, attr) + + def test_no_hostname_argument(self): + salt.utils.vmware.get_storage_system(self.mock_si, + self.mock_host_ref) + self.mock_get_managed_object_name.assert_called_once_with( + self.mock_host_ref) + + def test_hostname_argument(self): + salt.utils.vmware.get_storage_system(self.mock_si, + self.mock_host_ref, + hostname='fake_host') + self.assertEqual(self.mock_get_managed_object_name.call_count, 0) + + def test_traversal_spec(self): + mock_traversal_spec = MagicMock(return_value=[{'object': + self.mock_obj}]) + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec): + + salt.utils.vmware.get_storage_system(self.mock_si, + self.mock_host_ref) + mock_traversal_spec.assert_called_once_with( + path='configManager.storageSystem', + type=vim.HostSystem, + skip=False) + + def test_get_mors_with_properties(self): + salt.utils.vmware.get_storage_system(self.mock_si, + self.mock_host_ref) + self.mock_get_mors.assert_called_once_with( + self.mock_si, + vim.HostStorageSystem, + property_list=['systemFile'], + container_ref=self.mock_host_ref, + traversal_spec=self.mock_traversal_spec) + + def test_empty_mors_result(self): + with patch('salt.utils.vmware.get_mors_with_properties', + MagicMock(return_value=[])): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + salt.utils.vmware.get_storage_system(self.mock_si, + self.mock_host_ref) + self.assertEqual(excinfo.exception.strerror, + 'Host\'s \'fake_host\' storage system was ' + 'not retrieved') + + def test_valid_mors_result(self): + res = salt.utils.vmware.get_storage_system(self.mock_si, + self.mock_host_ref) + self.assertEqual(res, self.mock_obj) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetDatastoresTestCase(TestCase): + '''Tests for salt.utils.vmware.get_datastores''' + + def setUp(self): + self.mock_si = MagicMock() + self.mock_reference = MagicMock(spec=vim.HostSystem) + self.mock_mount_infos = [ + MagicMock(volume=MagicMock(spec=vim.HostVmfsVolume, + extent=[MagicMock( + diskName='fake_disk2')])), + MagicMock(volume=MagicMock(spec=vim.HostVmfsVolume, + extent=[MagicMock( + diskName='fake_disk3')]))] + self.mock_mount_infos[0].volume.name = 'fake_ds2' + self.mock_mount_infos[1].volume.name = 'fake_ds3' + self.mock_entries = [{'name': 'fake_ds1', 'object': MagicMock()}, + {'name': 'fake_ds2', 'object': MagicMock()}, + {'name': 'fake_ds3', 'object': MagicMock()}] + self.mock_storage_system = MagicMock() + self.mock_get_storage_system = MagicMock( + return_value=self.mock_storage_system) + self.mock_get_managed_object_name = MagicMock(return_value='fake_host') + self.mock_traversal_spec = MagicMock() + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + self.mock_get_managed_object_name), + ('salt.utils.vmware.get_storage_system', + self.mock_get_storage_system), + ('salt.utils.vmware.get_properties_of_managed_object', + MagicMock(return_value={'fileSystemVolumeInfo.mountInfo': + self.mock_mount_infos})), + ('salt.utils.vmware.get_mors_with_properties', + MagicMock(return_value=self.mock_entries)), + ('salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + MagicMock(return_value=self.mock_traversal_spec))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_reference', 'mock_storage_system', + 'mock_get_storage_system', 'mock_mount_infos', + 'mock_entries', 'mock_get_managed_object_name', + 'mock_traversal_spec'): + delattr(self, attr) + + def test_get_reference_name_call(self): + salt.utils.vmware.get_datastores(self.mock_si, + self.mock_reference) + self.mock_get_managed_object_name.assert_called_once_with( + self.mock_reference) + + def test_get_no_datastores(self): + res = salt.utils.vmware.get_datastores(self.mock_si, + self.mock_reference) + self.assertEqual(res, []) + + def test_get_storage_system_call(self): + salt.utils.vmware.get_datastores(self.mock_si, + self.mock_reference, + backing_disk_ids=['fake_disk1']) + self.mock_get_storage_system.assert_called_once_with( + self.mock_si, self.mock_reference, 'fake_host') + + def test_get_mount_info_call(self): + mock_get_properties_of_managed_object = MagicMock() + with patch('salt.utils.vmware.get_properties_of_managed_object', + mock_get_properties_of_managed_object): + salt.utils.vmware.get_datastores(self.mock_si, + self.mock_reference, + backing_disk_ids=['fake_disk1']) + mock_get_properties_of_managed_object.assert_called_once_with( + self.mock_storage_system, ['fileSystemVolumeInfo.mountInfo']) + + def test_backing_disks_no_mount_info(self): + with patch('salt.utils.vmware.get_properties_of_managed_object', + MagicMock(return_value={})): + res = salt.utils.vmware.get_datastores( + self.mock_si, self.mock_reference, + backing_disk_ids=['fake_disk_id']) + self.assertEqual(res, []) + + def test_host_traversal_spec(self): + # Reference is of type vim.HostSystem + mock_traversal_spec_init = MagicMock() + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec_init): + + salt.utils.vmware.get_datastores( + self.mock_si, + self.mock_reference, + get_all_datastores=True) + mock_traversal_spec_init.assert_called_once_with( + name='host_datastore_traversal', + path='datastore', + skip=False, + type=vim.HostSystem) + + def test_cluster_traversal_spec(self): + mock_traversal_spec_init = MagicMock() + # Reference is of type vim.ClusterComputeResource + mock_reference = MagicMock(spec=vim.ClusterComputeResource) + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec_init): + + salt.utils.vmware.get_datastores( + self.mock_si, + mock_reference, + get_all_datastores=True) + mock_traversal_spec_init.assert_called_once_with( + name='cluster_datastore_traversal', + path='datastore', + skip=False, + type=vim.ClusterComputeResource) + + def test_datacenter_traversal_spec(self): + mock_traversal_spec_init = MagicMock() + # Reference is of type vim.ClusterComputeResource + mock_reference = MagicMock(spec=vim.Datacenter) + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec_init): + + salt.utils.vmware.get_datastores( + self.mock_si, + mock_reference, + get_all_datastores=True) + mock_traversal_spec_init.assert_called_once_with( + name='datacenter_datastore_traversal', + path='datastore', + skip=False, + type=vim.Datacenter) + + def test_root_folder_traversal_spec(self): + mock_traversal_spec_init = MagicMock(return_value='traversal') + mock_reference = MagicMock(spec=vim.Folder) + with patch('salt.utils.vmware.get_managed_object_name', + MagicMock(side_effect=['fake_host', 'Datacenters'])): + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec_init): + + salt.utils.vmware.get_datastores( + self.mock_si, + mock_reference, + get_all_datastores=True) + + mock_traversal_spec_init.assert_called([ + call(path='childEntity', + selectSet=['traversal'], + skip=False, + type=vim.Folder), + call(path='datastore', + skip=False, + type=vim.Datacenter)]) + + def test_unsupported_reference_type(self): + class FakeClass(object): + pass + + mock_reference = MagicMock(spec=FakeClass) + with self.assertRaises(ArgumentValueError) as excinfo: + salt.utils.vmware.get_datastores( + self.mock_si, + mock_reference, + get_all_datastores=True) + self.assertEqual(excinfo.exception.strerror, + 'Unsupported reference type \'FakeClass\'') + + def test_get_mors_with_properties(self): + mock_get_mors_with_properties = MagicMock() + with patch('salt.utils.vmware.get_mors_with_properties', + mock_get_mors_with_properties): + salt.utils.vmware.get_datastores( + self.mock_si, + self.mock_reference, + get_all_datastores=True) + mock_get_mors_with_properties.assert_called_once_with( + self.mock_si, + object_type=vim.Datastore, + property_list=['name'], + container_ref=self.mock_reference, + traversal_spec=self.mock_traversal_spec) + + def test_get_all_datastores(self): + res = salt.utils.vmware.get_datastores(self.mock_si, + self.mock_reference, + get_all_datastores=True) + self.assertEqual(res, [self.mock_entries[0]['object'], + self.mock_entries[1]['object'], + self.mock_entries[2]['object']]) + + def test_get_datastores_filtered_by_name(self): + res = salt.utils.vmware.get_datastores(self.mock_si, + self.mock_reference, + datastore_names=['fake_ds1', + 'fake_ds2']) + self.assertEqual(res, [self.mock_entries[0]['object'], + self.mock_entries[1]['object']]) + + def test_get_datastores_filtered_by_backing_disk(self): + res = salt.utils.vmware.get_datastores( + self.mock_si, self.mock_reference, + backing_disk_ids=['fake_disk2', 'fake_disk3']) + self.assertEqual(res, [self.mock_entries[1]['object'], + self.mock_entries[2]['object']]) + + def test_get_datastores_filtered_by_both_name_and_backing_disk(self): + # Simulate VMware data model for volumes fake_ds2, fake_ds3 + res = salt.utils.vmware.get_datastores( + self.mock_si, self.mock_reference, + datastore_names=['fake_ds1'], + backing_disk_ids=['fake_disk3']) + self.assertEqual(res, [self.mock_entries[0]['object'], + self.mock_entries[2]['object']]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class RenameDatastoreTestCase(TestCase): + '''Tests for salt.utils.vmware.rename_datastore''' + + def setUp(self): + self.mock_ds_ref = MagicMock() + self.mock_get_managed_object_name = MagicMock(return_value='fake_ds') + + patches = ( + ('salt.utils.vmware.get_managed_object_name', + self.mock_get_managed_object_name),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_ds_ref', 'mock_get_managed_object_name'): + delattr(self, attr) + + def test_datastore_name_call(self): + salt.utils.vmware.rename_datastore(self.mock_ds_ref, + 'fake_new_name') + self.mock_get_managed_object_name.assert_called_once_with( + self.mock_ds_ref) + + def test_rename_datastore_raise_no_permission(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_ds_ref).RenameDatastore = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.rename_datastore(self.mock_ds_ref, + 'fake_new_name') + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_rename_datastore_raise_vim_fault(self): + exc = vim.VimFault() + exc.msg = 'vim_fault' + type(self.mock_ds_ref).RenameDatastore = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.vmware.rename_datastore(self.mock_ds_ref, + 'fake_new_name') + self.assertEqual(excinfo.exception.message, 'vim_fault') + + def test_rename_datastore_raise_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'runtime_fault' + type(self.mock_ds_ref).RenameDatastore = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.vmware.rename_datastore(self.mock_ds_ref, + 'fake_new_name') + self.assertEqual(excinfo.exception.message, 'runtime_fault') + + def test_rename_datastore(self): + salt.utils.vmware.rename_datastore(self.mock_ds_ref, 'fake_new_name') + self.mock_ds_ref.RenameDatastore.assert_called_once_with( + 'fake_new_name') From 16ae8253c1504948da26cfd51234aec214b3c24f Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 27 Sep 2017 14:41:25 -0600 Subject: [PATCH 324/633] Mock which, use os.linesep for cmd.run return --- tests/unit/modules/test_status.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/unit/modules/test_status.py b/tests/unit/modules/test_status.py index 8ad8a02efb..6cf1de1dbd 100644 --- a/tests/unit/modules/test_status.py +++ b/tests/unit/modules/test_status.py @@ -2,6 +2,7 @@ # Import Python libs from __future__ import absolute_import +import os # Import Salt Libs import salt.utils @@ -78,8 +79,9 @@ class StatusTestCase(TestCase, LoaderModuleMockMixin): is_darwin=MagicMock(return_value=False), is_freebsd=MagicMock(return_value=False), is_openbsd=MagicMock(return_value=False), - is_netbsd=MagicMock(return_value=False)): - with patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value="1\n2\n3")}): + is_netbsd=MagicMock(return_value=False), + which=MagicMock(return_value=True)): + with patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value=os.linesep.join(['1', '2', '3']))}): with patch('time.time', MagicMock(return_value=m.now)): with patch('os.path.exists', MagicMock(return_value=True)): proc_uptime = '{0} {1}'.format(m.ut, m.idle) @@ -103,9 +105,10 @@ class StatusTestCase(TestCase, LoaderModuleMockMixin): is_darwin=MagicMock(return_value=False), is_freebsd=MagicMock(return_value=False), is_openbsd=MagicMock(return_value=False), - is_netbsd=MagicMock(return_value=False)): + is_netbsd=MagicMock(return_value=False), + which=MagicMock(return_value=True)): - with patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value="1\n2\n3"), + with patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value=os.linesep.join(['1', '2', '3'])), 'cmd.run_all': MagicMock(return_value=m2.ret)}): with patch('time.time', MagicMock(return_value=m.now)): ret = status.uptime() @@ -125,8 +128,9 @@ class StatusTestCase(TestCase, LoaderModuleMockMixin): is_darwin=MagicMock(return_value=True), is_freebsd=MagicMock(return_value=False), is_openbsd=MagicMock(return_value=False), - is_netbsd=MagicMock(return_value=False)): - with patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value="1\n2\n3"), + is_netbsd=MagicMock(return_value=False), + which=MagicMock(return_value=True)): + with patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value=os.linesep.join(['1', '2', '3'])), 'sysctl.get': MagicMock(return_value=kern_boottime)}): with patch('time.time', MagicMock(return_value=m.now)): ret = status.uptime() From 23afe05102467fd2135f7c1d00be049747651649 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Thu, 7 Sep 2017 15:10:18 -0600 Subject: [PATCH 325/633] skeleton vagrant module --- salt/modules/vagrant.py | 384 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 salt/modules/vagrant.py diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py new file mode 100644 index 0000000000..0ea4812bd3 --- /dev/null +++ b/salt/modules/vagrant.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +''' +Work with virtual machines managed by vagrant + + .. versionadded:: Oxygen +''' + + +# Import python libs +from __future__ import absolute_import +import os +import re +import sys +import shutil +import subprocess +import string # pylint: disable=deprecated-module +import logging +import time +import datetime + +# Import third party libs +import yaml +import jinja2 +import jinja2.exceptions +from salt.ext import six +from salt.ext.six.moves import StringIO as _StringIO # pylint: disable=import-error + +# Import salt libs +import salt.utils +import salt.utils.files +import salt.utils.path +import salt.utils.stringutils +import salt.utils.templates +import salt.utils.validate.net +from salt.exceptions import CommandExecutionError, SaltInvocationError + +log = logging.getLogger(__name__) + +# Set up template environment +JINJA = jinja2.Environment( + loader=jinja2.FileSystemLoader( + os.path.join(salt.utils.templates.TEMPLATE_DIRNAME, 'virt') + ) +) + +__virtualname__ = 'varant' + + +def __virtual__(): + ''' + run Vagrant commands if possible + ''' + if salt.utils.path.which('vagrant') is None: + return (False, 'The vagrant module could not be loaded: vagrant command not found') + return __virtualname__ + + +def __get_conn(): + ''' + Detects what type of dom this node is and attempts to connect to the + correct hypervisor via libvirt. + ''' + # This has only been tested on kvm and xen, it needs to be expanded to + # support all vm layers supported by libvirt + + + hypervisor = __salt__['config.get']('libvirt:hypervisor', 'qemu') + + try: + conn = conn_func = NotImplemented # conn_func[hypervisor][0](*conn_func[hypervisor][1]) + except Exception: + raise CommandExecutionError( + 'Sorry, {0} failed to open a connection to the hypervisor ' + 'software at {1}'.format( + __grains__['fqdn'], + conn_func[hypervisor][1][0] + ) + ) + return conn + + +def _get_domain(*vms, **kwargs): + ''' + Return a domain object for the named VM or return domain object for all VMs. + ''' + ret = list() + lookup_vms = list() + conn = __get_conn() + + all_vms = list_domains() + if not all_vms: + raise CommandExecutionError('No virtual machines found.') + + if vms: + for vm in vms: + if vm not in all_vms: + raise CommandExecutionError('The VM "{name}" is not present'.format(name=vm)) + else: + lookup_vms.append(vm) + else: + lookup_vms = list(all_vms) + + for vm in lookup_vms: + ret.append(conn.lookupByName(vm)) + + return len(ret) == 1 and not kwargs.get('iterable') and ret[0] or ret + + + +def init(name, + cwd='', # path to find Vagrantfile + machine='', # name of machine in Vagrantfile + runas=None, # defaults to SUDO_USER + start=True, # pylint: disable=redefined-outer-name + # saltenv='base', + # seed=True, + install=True, + # pub_key=None, + # priv_key=None, + # seed_cmd='seed.apply', + **kwargs): + ''' + Initialize a new Vagrant vm + + CLI Example: + + .. code-block:: bash + + salt 'hypervisor' vagrant.init salt_id /path/to/Vagrantfile machine_name + salt my_laptop vagrant.init x1 /projects/bevy_master q1 + + ''' + + # if False: + # # Seed only if there is an image specified + # if seed and disk_image: + # log.debug('Seed command is {0}'.format(seed_cmd)) + # __salt__[seed_cmd]( + # img_dest, + # id_=name, + # config=kwargs.get('config'), + # install=install, + # pub_key=pub_key, + # priv_key=priv_key, + + + # log.debug('Generating VM XML') + # kwargs['enable_vnc'] = enable_vnc + # xml = _gen_xml(name, cpu, mem, diskp, nicp, hypervisor, **kwargs) + # try: + # define_xml_str(xml) + # except libvirtError as err: + # # check if failure is due to this domain already existing + # if "domain '{}' already exists".format(name) in str(err): + # # continue on to seeding + # log.warning(err) + # else: + # raise err # a real error we should report upwards + + if start: + log.debug('Starting VM {0}'.format(name)) + _get_domain(name).create() + + return True + + +def list_domains(): + ''' + Return a list of available domains. + + CLI Example: + + .. code-block:: bash + + salt '*' virt.list_domains + ''' + vms = [] + vms.extend(list_active_vms()) + vms.extend(list_inactive_vms()) + return vms + + +def list_active_vms(): + ''' + Return a list of names for active virtual machine on the minion + + CLI Example: + + .. code-block:: bash + + salt '*' virt.list_active_vms + ''' + conn = __get_conn() + vms = [] + for id_ in conn.listDomainsID(): + vms.append(conn.lookupByID(id_).name()) + return vms + + +def list_inactive_vms(): + ''' + Return a list of names for inactive virtual machine on the minion + + CLI Example: + + .. code-block:: bash + + salt '*' virt.list_inactive_vms + ''' + conn = __get_conn() + vms = [] + for id_ in conn.listDefinedDomains(): + vms.append(id_) + return vms + + + +def vm_state(vm_=None): + ''' + Return list of all the vms and their state. + + If you pass a VM name in as an argument then it will return info + for just the named VM, otherwise it will return all VMs. + + CLI Example: + + .. code-block:: bash + + salt '*' virt.vm_state + ''' + def _info(vm_): + state = '' + dom = _get_domain(vm_) + raw = dom.info() + state = NotImplemented #.get(raw[0], 'unknown') + return state + info = {} + if vm_: + info[vm_] = _info(vm_) + else: + for vm_ in list_domains(): + info[vm_] = _info(vm_) + return info + + + + +def shutdown(vm_): + ''' + Send a soft shutdown signal to the named vm + + CLI Example: + + .. code-block:: bash + + salt '*' virt.shutdown + ''' + dom = _get_domain(vm_) + return dom.shutdown() == 0 + + +def pause(vm_): + ''' + Pause the named vm + + CLI Example: + + .. code-block:: bash + + salt '*' virt.pause + ''' + dom = _get_domain(vm_) + return dom.suspend() == 0 + + +def resume(vm_): + ''' + Resume the named vm + + CLI Example: + + .. code-block:: bash + + salt '*' virt.resume + ''' + dom = _get_domain(vm_) + return dom.resume() == 0 + + +def start(name): + ''' + Start a defined domain + + CLI Example: + + .. code-block:: bash + + salt '*' virt.start + ''' + return _get_domain(name).create() == 0 + + +def stop(name): + ''' + Hard power down the virtual machine, this is equivalent to pulling the power. + + CLI Example: + + .. code-block:: bash + + salt '*' virt.stop + ''' + return _get_domain(name).destroy() == 0 + + +def reboot(name): + ''' + Reboot a domain via ACPI request + + CLI Example: + + .. code-block:: bash + + salt '*' virt.reboot + ''' + return _get_domain(name).reboot(NotImplemented) == 0 + + +def reset(vm_): + ''' + Reset a VM by emulating the reset button on a physical machine + + CLI Example: + + .. code-block:: bash + + salt '*' virt.reset + ''' + dom = _get_domain(vm_) + + # reset takes a flag, like reboot, but it is not yet used + # so we just pass in 0 + # see: http://libvirt.org/html/libvirt-libvirt.html#virDomainReset + return dom.reset(0) == 0 + + + +def undefine(vm_): + ''' + Remove a defined vm, this does not purge the virtual machine image, and + this only works if the vm is powered down + + CLI Example: + + .. code-block:: bash + + salt '*' virt.undefine + ''' + dom = _get_domain(vm_) + return dom.undefine() == 0 + + +def purge(vm_, dirs=False): + ''' + Recursively destroy and delete a virtual machine, pass True for dir's to + also delete the directories containing the virtual machine disk images - + USE WITH EXTREME CAUTION! + + CLI Example: + + .. code-block:: bash + + salt '*' virt.purge + ''' + + directories = set() + dirs = NotImplemented + + if dirs: + for dir_ in directories: + shutil.rmtree(dir_) + undefine(vm_) + return True + From 14c7de618f53f6f99b5509e67eec64e1300e5b99 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Tue, 12 Sep 2017 18:51:21 -0600 Subject: [PATCH 326/633] implement vagrant exection module --- salt/modules/vagrant.py | 425 ++++++++++++++++++++-------------------- 1 file changed, 209 insertions(+), 216 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 0ea4812bd3..a419a40e6e 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -7,43 +7,19 @@ Work with virtual machines managed by vagrant # Import python libs -from __future__ import absolute_import -import os -import re -import sys -import shutil +from __future__ import absolute_import, print_function import subprocess -import string # pylint: disable=deprecated-module import logging -import time -import datetime - -# Import third party libs -import yaml -import jinja2 -import jinja2.exceptions -from salt.ext import six -from salt.ext.six.moves import StringIO as _StringIO # pylint: disable=import-error # Import salt libs +import salt.cache import salt.utils -import salt.utils.files -import salt.utils.path import salt.utils.stringutils -import salt.utils.templates -import salt.utils.validate.net -from salt.exceptions import CommandExecutionError, SaltInvocationError +from salt.exceptions import CommandExecutionError, SaltCacheError log = logging.getLogger(__name__) -# Set up template environment -JINJA = jinja2.Environment( - loader=jinja2.FileSystemLoader( - os.path.join(salt.utils.templates.TEMPLATE_DIRNAME, 'virt') - ) -) - -__virtualname__ = 'varant' +__virtualname__ = 'vagrant' def __virtual__(): @@ -55,70 +31,36 @@ def __virtual__(): return __virtualname__ -def __get_conn(): +def _user(vm_): ''' - Detects what type of dom this node is and attempts to connect to the - correct hypervisor via libvirt. + prepend "sudo -u username " if _vm['runas'] is defined + :param vm_: the virtual machine configuration dictionary + :return: "sudo -u " or "" as needed ''' - # This has only been tested on kvm and xen, it needs to be expanded to - # support all vm layers supported by libvirt - - - hypervisor = __salt__['config.get']('libvirt:hypervisor', 'qemu') - try: - conn = conn_func = NotImplemented # conn_func[hypervisor][0](*conn_func[hypervisor][1]) - except Exception: - raise CommandExecutionError( - 'Sorry, {0} failed to open a connection to the hypervisor ' - 'software at {1}'.format( - __grains__['fqdn'], - conn_func[hypervisor][1][0] - ) - ) - return conn + return 'sudo -u {} '.format(vm_['runas']) + except KeyError: + return '' -def _get_domain(*vms, **kwargs): - ''' - Return a domain object for the named VM or return domain object for all VMs. - ''' - ret = list() - lookup_vms = list() - conn = __get_conn() - - all_vms = list_domains() - if not all_vms: - raise CommandExecutionError('No virtual machines found.') - - if vms: - for vm in vms: - if vm not in all_vms: - raise CommandExecutionError('The VM "{name}" is not present'.format(name=vm)) - else: - lookup_vms.append(vm) - else: - lookup_vms = list(all_vms) - - for vm in lookup_vms: - ret.append(conn.lookupByName(vm)) - - return len(ret) == 1 and not kwargs.get('iterable') and ret[0] or ret +def _update_cache(name, vm_): + vm_cache = salt.cache.Cache(__opts__, expire=13140000) # keep data for ten years + vm_cache.store('vagrant', name, vm_) +def _get_cache(name): + vm_cache = salt.cache.Cache(__opts__) + try: + vm_ = vm_cache.fetch('vagrant', name) + except SaltCacheError: + vm_ = {'name': name, 'machine': '', 'cwd': '.'} + log.warn('Trouble reading Salt cache for vagrant[%S]', name) + return vm_ -def init(name, - cwd='', # path to find Vagrantfile - machine='', # name of machine in Vagrantfile - runas=None, # defaults to SUDO_USER - start=True, # pylint: disable=redefined-outer-name - # saltenv='base', - # seed=True, - install=True, - # pub_key=None, - # priv_key=None, - # seed_cmd='seed.apply', - **kwargs): + +def init(name, # Salt id for created VM + cwd, # path to find Vagrantfile + **kwargs): # other keyword arguments ''' Initialize a new Vagrant vm @@ -126,42 +68,29 @@ def init(name, .. code-block:: bash - salt 'hypervisor' vagrant.init salt_id /path/to/Vagrantfile machine_name - salt my_laptop vagrant.init x1 /projects/bevy_master q1 + salt 'hypervisor' vagrant.init salt_id /path/to/Vagrantfile + salt my_laptop vagrant.init x1 /projects/bevy_master machine=q1 + optional keyword arguments: + machine='', # name of machine in Vagrantfile + runas=None, # defaults to SUDO_USER + start=True, # start the machine when initialized + deploy=True, # load Salt on the machine + vm={}, # a dictionary of configuration settings ''' + vm_ = kwargs.copy() # any keyword arguments are stored as configuration data + if 'vm' in kwargs: # allow caller to pass in a dictionary + vm_.update(kwargs.pop('vm')) + vm_.update(name=name, cwd=cwd) - # if False: - # # Seed only if there is an image specified - # if seed and disk_image: - # log.debug('Seed command is {0}'.format(seed_cmd)) - # __salt__[seed_cmd]( - # img_dest, - # id_=name, - # config=kwargs.get('config'), - # install=install, - # pub_key=pub_key, - # priv_key=priv_key, - - - # log.debug('Generating VM XML') - # kwargs['enable_vnc'] = enable_vnc - # xml = _gen_xml(name, cpu, mem, diskp, nicp, hypervisor, **kwargs) - # try: - # define_xml_str(xml) - # except libvirtError as err: - # # check if failure is due to this domain already existing - # if "domain '{}' already exists".format(name) in str(err): - # # continue on to seeding - # log.warning(err) - # else: - # raise err # a real error we should report upwards + _update_cache(name, vm_) if start: log.debug('Starting VM {0}'.format(name)) - _get_domain(name).create() - - return True + ret = start(name, vm_) + else: + ret = True + return ret def list_domains(): @@ -172,11 +101,24 @@ def list_domains(): .. code-block:: bash - salt '*' virt.list_domains + salt '*' vagrant.list_domains ''' vms = [] - vms.extend(list_active_vms()) - vms.extend(list_inactive_vms()) + cmd = 'vagrant global-status {}' + log.info('Executing command "%s"', cmd) + output = subprocess.check_output( + [cmd], + shell=True, + ) + reply = salt.utils.stringutils.to_str(output) + for line in reply.split('\n'): # build a list of the text reply + print(line) + tokens = line.strip().split() + try: + _ = int(tokens[0], 16) # valid id numbers are hexadecimal + vms.append(tokens) + except (ValueError, IndexError): + pass return vms @@ -188,12 +130,22 @@ def list_active_vms(): .. code-block:: bash - salt '*' virt.list_active_vms + salt '*' vagrant.list_active_vms cwd=/projects/project_1 ''' - conn = __get_conn() vms = [] - for id_ in conn.listDomainsID(): - vms.append(conn.lookupByID(id_).name()) + cmd = 'vagrant status' + log.info('Executing command "%s"', cmd) + output = subprocess.check_output( + [cmd], + shell=True, + ) + reply = salt.utils.stringutils.to_str(output) + for line in reply.split('\n'): # build a list of the text reply + print(line) + tokens = line.strip().split() + if len(tokens) > 1: + if tokens[1] == 'running': + vms.append(tokens[0]) return vms @@ -205,17 +157,26 @@ def list_inactive_vms(): .. code-block:: bash - salt '*' virt.list_inactive_vms + salt '*' virt.list_inactive_vms cwd=/projects/project_1 ''' - conn = __get_conn() vms = [] - for id_ in conn.listDefinedDomains(): - vms.append(id_) + cmd = 'vagrant status' + log.info('Executing command "%s"', cmd) + output = subprocess.check_output( + [cmd], + shell=True, + ) + reply = salt.utils.stringutils.to_str(output) + for line in reply.split('\n'): # build a list of the text reply + print(line) + tokens = line.strip().split() + if len(tokens) > 1: + if tokens[1] != 'running': + vms.append(tokens[0]) return vms - -def vm_state(vm_=None): +def vm_state(name=''): ''' Return list of all the vms and their state. @@ -226,26 +187,29 @@ def vm_state(vm_=None): .. code-block:: bash - salt '*' virt.vm_state + salt '*' vagrant.vm_state cwd='/projects/project_1' ''' - def _info(vm_): - state = '' - dom = _get_domain(vm_) - raw = dom.info() - state = NotImplemented #.get(raw[0], 'unknown') - return state info = {} - if vm_: - info[vm_] = _info(vm_) - else: - for vm_ in list_domains(): - info[vm_] = _info(vm_) + cmd = 'vagrant status {}'.format(name) + log.info('Executing command "%s"', cmd) + output = subprocess.check_output( + [cmd], + shell=True, + ) + reply = salt.utils.stringutils.to_str(output) + for line in reply.split('\n'): # build a list of the text reply + print(line) + tokens = line.strip().split() + if tokens[-1].endswith(')') : + try: + info[tokens[0]]['state'] = tokens[1] + info[tokens[0]]['provider'] = tokens[2] - '(' - ')' + except IndexError: + pass return info - - -def shutdown(vm_): +def shutdown(name): ''' Send a soft shutdown signal to the named vm @@ -253,51 +217,94 @@ def shutdown(vm_): .. code-block:: bash - salt '*' virt.shutdown + salt '*' vagrant.shutdown ''' - dom = _get_domain(vm_) - return dom.shutdown() == 0 + return stop(name) -def pause(vm_): +def pause(name): ''' - Pause the named vm + Pause (suspend) the named vm CLI Example: .. code-block:: bash - salt '*' virt.pause + salt '*' vagrant.pause ''' - dom = _get_domain(vm_) - return dom.suspend() == 0 + vm_ = _get_cache(name) + machine = vm_['machine'] + + cmd = '{}vagrant suspend {}'.format(_user(vm_), machine) + log.info('Executing command "%s"', cmd) + ret = subprocess.call( + [cmd], + shell=True, + cwd=vm_.get('cwd', None) + ) + return ret -def resume(vm_): + +def start(name, vm_=None): ''' - Resume the named vm + Start a defined virtual machine. The machine must have been previously defined + using "vagrant.init". CLI Example: .. code-block:: bash - salt '*' virt.resume + salt '*' vagrant.start ''' - dom = _get_domain(vm_) - return dom.resume() == 0 + fudged_opts = __opts__.copy() # make a mock of cloud configuration info + fudged_opts['profiles'] = {} + fudged_opts['providers'] = {} + + if vm_ is None: + vm_ = _get_cache(name) + + machine = vm_['machine'] + + cmd = '{}vagrant up {}'.format(_user(vm_), machine) + log.info('Executing command "%s"', cmd) + ret = subprocess.call( + [cmd], + shell=True, + cwd=vm_.get('cwd', None) + ) + if ret: + raise CommandExecutionError('Error starting Vagrant machine') + + # the ssh address and port are not known until after the machine boots. + # so we must detect it and record it then + if not vm_['ssh_host']: + log.info('requesting vagrant ssh-config for %s', machine or 'Vagrant default') + output = subprocess.check_output( + ['{}vagrant ssh-config {}'.format(_user(vm_), machine)], + shell=True, + cwd=vm_.get('cwd', None) + ) + reply = salt.utils.stringutils.to_str(output) + ssh_config = {} + for line in reply.split('\n'): # build a dictionary of the text reply + tokens = line.strip().split() + if len(tokens) == 2: # each two-token line becomes a key:value pair + ssh_config[tokens[0]] = tokens[1] + log.debug('ssh_config=%s', repr(ssh_config)) -def start(name): - ''' - Start a defined domain + vm_.setdefault('key_filename', ssh_config['IdentityFile']) + vm_.setdefault('ssh_username', ssh_config['User']) + vm_['ssh_host'] = ssh_config['HostName'] + vm_.setdefault('ssh_port', ssh_config['Port']) + _update_cache(name, vm_) - CLI Example: + log.info('Provisioning machine %s as node %s using ssh %s', + machine, vm_['name'], vm_['ssh_host']) + ret = __utils__['cloud.bootstrap'](vm_, fudged_opts) - .. code-block:: bash - - salt '*' virt.start - ''' - return _get_domain(name).create() == 0 + return ret def stop(name): @@ -308,77 +315,63 @@ def stop(name): .. code-block:: bash - salt '*' virt.stop + salt '*' vagrant.stop ''' - return _get_domain(name).destroy() == 0 + vm_ = _get_cache(name) + machine = vm_['machine'] + + cmd = '{}vagrant halt {}'.format(_user(vm_), machine) + log.info('Executing command "%s"', cmd) + ret = subprocess.call( + [cmd], + shell=True, + cwd=vm_.get('cwd', None) + ) + return ret def reboot(name): ''' - Reboot a domain via ACPI request + Reboot a VM CLI Example: .. code-block:: bash - salt '*' virt.reboot + salt '*' vagrant.reboot ''' - return _get_domain(name).reboot(NotImplemented) == 0 + vm_ = _get_cache(name) + machine = vm_['machine'] + + cmd = '{}vagrant reload {}'.format(_user(vm_), machine) + log.info('Executing command "%s"', cmd) + ret = subprocess.call( + [cmd], + shell=True, + cwd=vm_.get('cwd', None) + ) + return ret -def reset(vm_): +def destroy(name): ''' - Reset a VM by emulating the reset button on a physical machine + Destroy and delete a virtual machine. CLI Example: .. code-block:: bash - salt '*' virt.reset - ''' - dom = _get_domain(vm_) - - # reset takes a flag, like reboot, but it is not yet used - # so we just pass in 0 - # see: http://libvirt.org/html/libvirt-libvirt.html#virDomainReset - return dom.reset(0) == 0 - - - -def undefine(vm_): - ''' - Remove a defined vm, this does not purge the virtual machine image, and - this only works if the vm is powered down - - CLI Example: - - .. code-block:: bash - - salt '*' virt.undefine - ''' - dom = _get_domain(vm_) - return dom.undefine() == 0 - - -def purge(vm_, dirs=False): - ''' - Recursively destroy and delete a virtual machine, pass True for dir's to - also delete the directories containing the virtual machine disk images - - USE WITH EXTREME CAUTION! - - CLI Example: - - .. code-block:: bash - - salt '*' virt.purge + salt '*' vagrant.destroy ''' - directories = set() - dirs = NotImplemented - - if dirs: - for dir_ in directories: - shutil.rmtree(dir_) - undefine(vm_) - return True + vm_ = _get_cache(name) + machine = vm_['machine'] + cmd = '{}vagrant destroy -f {}'.format(_user(vm_), machine) + log.info('Executing command "%s"', cmd) + ret = subprocess.call( + [cmd], + shell=True, + cwd=vm_.get('cwd', None) + ) + return ret From 2ab728f6007f61f2ae7b4646cb5cf43c2653df0a Mon Sep 17 00:00:00 2001 From: vernoncole Date: Wed, 13 Sep 2017 20:03:42 -0600 Subject: [PATCH 327/633] daily checkpoint of vagrant module --- salt/modules/vagrant.py | 304 ++++++++++++++++++++++++---------------- 1 file changed, 187 insertions(+), 117 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index a419a40e6e..8d047e1c6c 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -10,6 +10,7 @@ Work with virtual machines managed by vagrant from __future__ import absolute_import, print_function import subprocess import logging +from collections import defaultdict # Import salt libs import salt.cache @@ -27,75 +28,68 @@ def __virtual__(): run Vagrant commands if possible ''' if salt.utils.path.which('vagrant') is None: - return (False, 'The vagrant module could not be loaded: vagrant command not found') + return False, 'The vagrant module could not be loaded: vagrant command not found' return __virtualname__ -def _user(vm_): - ''' - prepend "sudo -u username " if _vm['runas'] is defined - :param vm_: the virtual machine configuration dictionary - :return: "sudo -u " or "" as needed - ''' - try: - return 'sudo -u {} '.format(vm_['runas']) - except KeyError: - return '' - - -def _update_cache(name, vm_): +def _update_cache(name, vm_, opts=None): vm_cache = salt.cache.Cache(__opts__, expire=13140000) # keep data for ten years vm_cache.store('vagrant', name, vm_) + if opts: + vm_cache.store('vagrant_opts', name, opts) -def _get_cache(name): +def _get_cached_vm(name): vm_cache = salt.cache.Cache(__opts__) try: vm_ = vm_cache.fetch('vagrant', name) except SaltCacheError: vm_ = {'name': name, 'machine': '', 'cwd': '.'} - log.warn('Trouble reading Salt cache for vagrant[%S]', name) + log.warn('Trouble reading Salt cache for vagrant[%s]', name) return vm_ -def init(name, # Salt id for created VM - cwd, # path to find Vagrantfile - **kwargs): # other keyword arguments +def _get_cached_opts(name): + vm_cache = salt.cache.Cache(__opts__) + try: + opts = vm_cache.fetch('vagrant_opts', name) + except SaltCacheError: + log.warn('Trouble reading Salt opts cache for vagrant[%s]', name) + return None + return opts + + +def _erase_cache(name): + vm_cache = salt.cache.Cache(__opts__) + try: + vm_cache.flush('vagrant', name) + except SaltCacheError: + pass + try: + vm_cache.flush('vagrant_opts', name) + except SaltCacheError: + pass + + +def version(): ''' - Initialize a new Vagrant vm + Return the version of Vagrant on the minion CLI Example: .. code-block:: bash - salt 'hypervisor' vagrant.init salt_id /path/to/Vagrantfile - salt my_laptop vagrant.init x1 /projects/bevy_master machine=q1 - - optional keyword arguments: - machine='', # name of machine in Vagrantfile - runas=None, # defaults to SUDO_USER - start=True, # start the machine when initialized - deploy=True, # load Salt on the machine - vm={}, # a dictionary of configuration settings + salt '*' vagrant.version ''' - vm_ = kwargs.copy() # any keyword arguments are stored as configuration data - if 'vm' in kwargs: # allow caller to pass in a dictionary - vm_.update(kwargs.pop('vm')) - vm_.update(name=name, cwd=cwd) - - _update_cache(name, vm_) - - if start: - log.debug('Starting VM {0}'.format(name)) - ret = start(name, vm_) - else: - ret = True - return ret + cmd = 'vagrant -v' + output = subprocess.check_output([cmd], shell=True) + reply = salt.utils.stringutils.to_str(output) + return reply.strip() def list_domains(): ''' - Return a list of available domains. + Return a cached list of all available Vagrant VMs on host. CLI Example: @@ -106,23 +100,23 @@ def list_domains(): vms = [] cmd = 'vagrant global-status {}' log.info('Executing command "%s"', cmd) - output = subprocess.check_output( - [cmd], - shell=True, - ) + try: + output = subprocess.check_output([cmd], shell=True) + except subprocess.CalledProcessError: + return [] reply = salt.utils.stringutils.to_str(output) for line in reply.split('\n'): # build a list of the text reply print(line) tokens = line.strip().split() try: _ = int(tokens[0], 16) # valid id numbers are hexadecimal - vms.append(tokens) + vms.append(' '.join(tokens)) except (ValueError, IndexError): - pass + pass # skip other lines return vms -def list_active_vms(): +def list_active_vms(cwd=None): ''' Return a list of names for active virtual machine on the minion @@ -135,10 +129,13 @@ def list_active_vms(): vms = [] cmd = 'vagrant status' log.info('Executing command "%s"', cmd) - output = subprocess.check_output( - [cmd], - shell=True, - ) + try: + output = subprocess.check_output( + [cmd], + shell=True, + cwd=cwd) + except subprocess.CalledProcessError: + return [] reply = salt.utils.stringutils.to_str(output) for line in reply.split('\n'): # build a list of the text reply print(line) @@ -149,7 +146,7 @@ def list_active_vms(): return vms -def list_inactive_vms(): +def list_inactive_vms(cwd=None): ''' Return a list of names for inactive virtual machine on the minion @@ -162,21 +159,24 @@ def list_inactive_vms(): vms = [] cmd = 'vagrant status' log.info('Executing command "%s"', cmd) - output = subprocess.check_output( - [cmd], - shell=True, - ) + try: + output = subprocess.check_output( + [cmd], + shell=True, + cwd=cwd) + except subprocess.CalledProcessError: + return [] reply = salt.utils.stringutils.to_str(output) for line in reply.split('\n'): # build a list of the text reply print(line) tokens = line.strip().split() - if len(tokens) > 1: + if len(tokens) > 1 and tokens[-1].endswith(')'): if tokens[1] != 'running': vms.append(tokens[0]) return vms -def vm_state(name=''): +def vm_state(name='', cwd=None): ''' Return list of all the vms and their state. @@ -187,86 +187,113 @@ def vm_state(name=''): .. code-block:: bash - salt '*' vagrant.vm_state cwd='/projects/project_1' + salt '*' vagrant.vm_state cwd=/projects/project_1 ''' info = {} cmd = 'vagrant status {}'.format(name) log.info('Executing command "%s"', cmd) - output = subprocess.check_output( - [cmd], - shell=True, - ) + try: + output = subprocess.check_output( + [cmd], + shell=True, + cwd=cwd) + except subprocess.CalledProcessError: + return {} reply = salt.utils.stringutils.to_str(output) for line in reply.split('\n'): # build a list of the text reply print(line) tokens = line.strip().split() - if tokens[-1].endswith(')') : + if len(tokens) > 1 and tokens[-1].endswith(')') : try: - info[tokens[0]]['state'] = tokens[1] - info[tokens[0]]['provider'] = tokens[2] - '(' - ')' + info[tokens[0]] = {'state': ' '.join(tokens[1:-1])} + info[tokens[0]]['provider'] = tokens[-1].lstrip('(').rstrip(')') except IndexError: pass return info -def shutdown(name): +def init(name, # Salt_id for created VM + cwd, # path to find Vagrantfile + machine='', # name of machine in Vagrantfile + runas=None, # username who owns Vagrant box + start=True, # start the machine when initialized + deploy=None, # load Salt onto the virtual machine, default=True + vagrant_provider='', # vagrant provider engine name + vm={}, # a dictionary of VM configuration settings + opts=None, # a dictionary of master configuration settings + ): ''' - Send a soft shutdown signal to the named vm + Initialize a new Vagrant vm CLI Example: .. code-block:: bash - salt '*' vagrant.shutdown + salt vagrant.init /path/to/Vagrantfile + salt my_laptop vagrant.init x1 /projects/bevy_master machine=quail1 ''' - return stop(name) + vm_ = vm.copy() # passed configuration data + vm_['name'] = name + vm_['cwd'] = cwd + # passed-in keyword arguments overwrite vm dictionary values + vm_['machine'] = machine or vm_.get('machine', machine) + vm_['runas'] = runas or vm_.get('runas', runas) + vm_['deploy'] = deploy if deploy is not None else vm_.get('deploy', True) + vm_['vagrant_provider'] = vagrant_provider or vm_.get('vagrant_provider', '') + _update_cache(name, vm_, opts) - -def pause(name): - ''' - Pause (suspend) the named vm - - CLI Example: - - .. code-block:: bash - - salt '*' vagrant.pause - ''' - vm_ = _get_cache(name) - machine = vm_['machine'] - - cmd = '{}vagrant suspend {}'.format(_user(vm_), machine) - log.info('Executing command "%s"', cmd) - ret = subprocess.call( - [cmd], - shell=True, - cwd=vm_.get('cwd', None) - ) + if start: + log.debug('Starting VM {0}'.format(name)) + ret = _start(name, vm_, opts) + else: + ret = True return ret - -def start(name, vm_=None): +def _runas_sudo(vm_, command): ''' - Start a defined virtual machine. The machine must have been previously defined - using "vagrant.init". + prepend "sudo -u " if _vm['runas'] is defined + :param vm_: the virtual machine configuration dictionary + :param command: the command line which will be sent + :return: "sudo -u command" or "command" as needed + ''' + runas = vm_.get('runas', False) + if runas: + return 'sudo -u {} {}'.format(runas, command) + return command + + +def start(name, vm_=None, opts=None): + ''' + Start (vagrant up) a defined virtual machine by salt_id name. + The machine must have been previously defined using "vagrant.init". CLI Example: .. code-block:: bash - salt '*' vagrant.start + salt vagrant.start ''' - fudged_opts = __opts__.copy() # make a mock of cloud configuration info - fudged_opts['profiles'] = {} - fudged_opts['providers'] = {} + ret = _start(name, vm_, opts) + + +def _start(name, vm_, opts): # internal call name, because "start" is a keyword argument to vagrant.init + fudged_opts = defaultdict(lambda: None) + if opts is None: + fudged_opts.update(__opts__) + fudged_opts.update(_get_cached_opts(name)) + fudged_opts.setdefault('profiles', defaultdict(lambda: '')) + fudged_opts.setdefault('providers', {}) + fudged_opts.setdefault('deploy_scripts_search_path', vm_.get('deploy_scripts_search_path', [])) if vm_ is None: - vm_ = _get_cache(name) + vm_ = _get_cached_vm(name) machine = vm_['machine'] - cmd = '{}vagrant up {}'.format(_user(vm_), machine) + vagrant_provider = vm_.get('vagrant_provider', '') + provider_ = '--provider={}'.format(vagrant_provider) if vagrant_provider else '' + cmd = _runas_sudo(vm_, 'vagrant up {} {}'.format(machine, provider_)) log.info('Executing command "%s"', cmd) ret = subprocess.call( [cmd], @@ -278,10 +305,10 @@ def start(name, vm_=None): # the ssh address and port are not known until after the machine boots. # so we must detect it and record it then - if not vm_['ssh_host']: + if 'ssh_host' not in vm_: log.info('requesting vagrant ssh-config for %s', machine or 'Vagrant default') output = subprocess.check_output( - ['{}vagrant ssh-config {}'.format(_user(vm_), machine)], + [_runas_sudo(vm_, 'vagrant ssh-config {}'.format(machine))], shell=True, cwd=vm_.get('cwd', None) ) @@ -300,6 +327,9 @@ def start(name, vm_=None): vm_.setdefault('ssh_port', ssh_config['Port']) _update_cache(name, vm_) + vm_.setdefault('driver', 'vagrant.start') # provide a dummy value needed in get_cloud_config_value + vm_.setdefault('provider', '') # provide a dummy value needed in get_cloud_config_value + vm_.setdefault('profile', '') # provide a dummy value needed in get_cloud_config_value log.info('Provisioning machine %s as node %s using ssh %s', machine, vm_['name'], vm_['ssh_host']) ret = __utils__['cloud.bootstrap'](vm_, fudged_opts) @@ -307,20 +337,59 @@ def start(name, vm_=None): return ret -def stop(name): + +def shutdown(name): ''' - Hard power down the virtual machine, this is equivalent to pulling the power. + Send a soft shutdown signal to the named vm. + ( for Vagrant, alternate name for vagrant.stop ) CLI Example: .. code-block:: bash - salt '*' vagrant.stop + salt vagrant.shutdown ''' - vm_ = _get_cache(name) + return stop(name) + + +def stop(name): + ''' + Hard shutdown the virtual machine. + ( Vagrant will attempt a soft shutdown first. ) + + CLI Example: + + .. code-block:: bash + + salt vagrant.stop + ''' + vm_ = _get_cached_vm(name) machine = vm_['machine'] - cmd = '{}vagrant halt {}'.format(_user(vm_), machine) + cmd = _runas_sudo(vm_, 'vagrant halt {}'.format(machine)) + log.info('Executing command "%s"', cmd) + ret = subprocess.call( + [cmd], + shell=True, + cwd=vm_.get('cwd', None) + ) + return ret + + +def pause(name): + ''' + Pause (suspend) the named vm + + CLI Example: + + .. code-block:: bash + + salt vagrant.pause + ''' + vm_ = _get_cached_vm(name) + machine = vm_['machine'] + + cmd = _runas_sudo(vm_, 'vagrant suspend {}'.format(machine)) log.info('Executing command "%s"', cmd) ret = subprocess.call( [cmd], @@ -338,12 +407,12 @@ def reboot(name): .. code-block:: bash - salt '*' vagrant.reboot + salt vagrant.reboot ''' - vm_ = _get_cache(name) + vm_ = _get_cached_vm(name) machine = vm_['machine'] - cmd = '{}vagrant reload {}'.format(_user(vm_), machine) + cmd = _runas_sudo(vm_, 'vagrant reload {}'.format(machine)) log.info('Executing command "%s"', cmd) ret = subprocess.call( [cmd], @@ -361,17 +430,18 @@ def destroy(name): .. code-block:: bash - salt '*' vagrant.destroy + salt vagrant.destroy ''' - vm_ = _get_cache(name) + vm_ = _get_cached_vm(name) machine = vm_['machine'] - cmd = '{}vagrant destroy -f {}'.format(_user(vm_), machine) + cmd = _runas_sudo(vm_, 'vagrant destroy -f {}'.format(machine)) log.info('Executing command "%s"', cmd) ret = subprocess.call( [cmd], shell=True, cwd=vm_.get('cwd', None) ) + _erase_cache(name) return ret From 586242ce4100d7445cbb8805e52863a3ca25cb25 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Fri, 15 Sep 2017 15:50:25 -0600 Subject: [PATCH 328/633] imploment get_ssh_config & etc --- salt/modules/vagrant.py | 189 ++++++++++++++++++++++++++++++---------- 1 file changed, 145 insertions(+), 44 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 8d047e1c6c..978c74cbc3 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -17,6 +17,11 @@ import salt.cache import salt.utils import salt.utils.stringutils from salt.exceptions import CommandExecutionError, SaltCacheError +import salt.ext.six as six +if six.PY3: + import ipaddress +else: + import salt.ext.ipaddress as ipaddress log = logging.getLogger(__name__) @@ -44,8 +49,12 @@ def _get_cached_vm(name): try: vm_ = vm_cache.fetch('vagrant', name) except SaltCacheError: - vm_ = {'name': name, 'machine': '', 'cwd': '.'} - log.warn('Trouble reading Salt cache for vagrant[%s]', name) + vm_ = {} + log.error('Trouble reading Salt cache for vagrant[%s]', name) + try: + _ = vm_['machine'] + except KeyError: + raise ValueError, 'No Vagrant machine defined for Salt-id {}'.format(name) return vm_ @@ -71,6 +80,30 @@ def _erase_cache(name): pass +def _vagrant_ssh_config(vm_): + ''' + get the information for ssh communication from the new VM + :param vm_: the VM's info as we have it now + :return: dictionary of ssh stuff + ''' + machine = vm_['machine'] + ret = {} + log.info('requesting vagrant ssh-config for %s', machine or 'Vagrant default') + output = subprocess.check_output( + [_runas_sudo(vm_, 'vagrant ssh-config {}'.format(machine))], + shell=True, + cwd=vm_.get('cwd', None) + ) + reply = salt.utils.stringutils.to_str(output) + ssh_config = {} + for line in reply.split('\n'): # build a dictionary of the text reply + tokens = line.strip().split() + if len(tokens) == 2: # each two-token line becomes a key:value pair + ssh_config[tokens[0]] = tokens[1] + log.debug('ssh_config=%s', repr(ssh_config)) + return ssh_config + + def version(): ''' Return the version of Vagrant on the minion @@ -82,7 +115,10 @@ def version(): salt '*' vagrant.version ''' cmd = 'vagrant -v' - output = subprocess.check_output([cmd], shell=True) + try: + output = subprocess.check_output([cmd], shell=True) + except subprocess.CalledProcessError: + return 'Error: subprocess error calling ' + cmd reply = salt.utils.stringutils.to_str(output) return reply.strip() @@ -263,7 +299,7 @@ def _runas_sudo(vm_, command): return command -def start(name, vm_=None, opts=None): +def start(name, vm_={}): ''' Start (vagrant up) a defined virtual machine by salt_id name. The machine must have been previously defined using "vagrant.init". @@ -274,10 +310,10 @@ def start(name, vm_=None, opts=None): salt vagrant.start ''' - ret = _start(name, vm_, opts) + return _start(name, vm_) -def _start(name, vm_, opts): # internal call name, because "start" is a keyword argument to vagrant.init +def _start(name, vm_, opts=None): # internal call name, because "start" is a keyword argument to vagrant.init fudged_opts = defaultdict(lambda: None) if opts is None: fudged_opts.update(__opts__) @@ -286,10 +322,13 @@ def _start(name, vm_, opts): # internal call name, because "start" is a keyword fudged_opts.setdefault('providers', {}) fudged_opts.setdefault('deploy_scripts_search_path', vm_.get('deploy_scripts_search_path', [])) - if vm_ is None: + if not vm_: vm_ = _get_cached_vm(name) - machine = vm_['machine'] + try: + machine = vm_['machine'] + except KeyError: + raise ValueError, 'No Vagrant machine defined for Salt-id {}'.format(name) vagrant_provider = vm_.get('vagrant_provider', '') provider_ = '--provider={}'.format(vagrant_provider) if vagrant_provider else '' @@ -300,41 +339,8 @@ def _start(name, vm_, opts): # internal call name, because "start" is a keyword shell=True, cwd=vm_.get('cwd', None) ) - if ret: - raise CommandExecutionError('Error starting Vagrant machine') - # the ssh address and port are not known until after the machine boots. - # so we must detect it and record it then - if 'ssh_host' not in vm_: - log.info('requesting vagrant ssh-config for %s', machine or 'Vagrant default') - output = subprocess.check_output( - [_runas_sudo(vm_, 'vagrant ssh-config {}'.format(machine))], - shell=True, - cwd=vm_.get('cwd', None) - ) - reply = salt.utils.stringutils.to_str(output) - ssh_config = {} - for line in reply.split('\n'): # build a dictionary of the text reply - tokens = line.strip().split() - if len(tokens) == 2: # each two-token line becomes a key:value pair - ssh_config[tokens[0]] = tokens[1] - log.debug('ssh_config=%s', repr(ssh_config)) - - - vm_.setdefault('key_filename', ssh_config['IdentityFile']) - vm_.setdefault('ssh_username', ssh_config['User']) - vm_['ssh_host'] = ssh_config['HostName'] - vm_.setdefault('ssh_port', ssh_config['Port']) - _update_cache(name, vm_) - - vm_.setdefault('driver', 'vagrant.start') # provide a dummy value needed in get_cloud_config_value - vm_.setdefault('provider', '') # provide a dummy value needed in get_cloud_config_value - vm_.setdefault('profile', '') # provide a dummy value needed in get_cloud_config_value - log.info('Provisioning machine %s as node %s using ssh %s', - machine, vm_['name'], vm_['ssh_host']) - ret = __utils__['cloud.bootstrap'](vm_, fudged_opts) - - return ret + return ret == 0 @@ -432,7 +438,6 @@ def destroy(name): salt vagrant.destroy ''' - vm_ = _get_cached_vm(name) machine = vm_['machine'] @@ -444,4 +449,100 @@ def destroy(name): cwd=vm_.get('cwd', None) ) _erase_cache(name) - return ret + return ret == 0 + + +def get_ssh_config(name, network_mask='', get_private_key=False): + ''' + Retrieve hints of how you might connect to a Vagrant VM. + + CLI Example: + + .. code-block:: bash + + salt vagrant.get_ssh_config + salt my_laptop vagrant.get_ssh_config quail1 network_mask=10.0.0.0/8 get_private_key=True + + returns a dictionary containing: + + - key_filename: the name of the private key file on the VM host computer + - ssh_username: the username to be used to log in to the VM + - ssh_host: the IP address used to log in to the VM. (This will usually be `127.0.0.1`) + - ssh_port: the TCP port used to log in to the VM. (This will often be `2222`) + - \[ip_address:\] (if `network_mask` is defined. see below) + - \[private_key:\] (if `get_private_key` is True) the private key for ssh_username + + About `network_mask`: + + Vagrant usually uses a redirected TCP port on its host computer to log in to a VM using ssh. + This makes it impossible for a third machine (such as a salt-cloud master) to contact the VM + unless the VM has another network interface defined. You will usually want a bridged network + defined by having a `config.vm.network "public_network"` statement in your `Vagrantfile`. + + The IP address of the bridged adapter will typically be assigned by DHCP and unknown to you, + but you should be able to determine what IP network the address will be chosen from. + If you enter a CIDR network mask, the module will attempt to find the VM's address for you. + It will send an `ifconfig` command to the VM (using ssh to `ssh_host`:`ssh_port`) and scan the + result, returning the IP address of the first interface it can find which matches your mask. + ''' + vm_ = _get_cached_vm(name) + + ssh_config = _vagrant_ssh_config(vm_) + + ans = { 'key_filename': ssh_config['IdentityFile'], + 'ssh_username': ssh_config['User'], + 'ssh_host': ssh_config['HostName'], + 'ssh_port': ssh_config['Port'], + } + + if network_mask: + # ask the new VM to report its network address + command = 'ssh -i {IdentityFile} -p {Port} ' \ + '-oStrictHostKeyChecking={StrictHostKeyChecking} ' \ + '-oUserKnownHostsFile={UserKnownHostsFile} ' \ + '-oControlPath=none ' \ + '{User}@{HostName} ifconfig'.format(**ssh_config) + + log.info('Trying ssh -p {Port} {User}@{HostName} ifconfig'.format(**ssh_config)) + try: + ret = subprocess.check_output([command], shell=True) + except subprocess.CalledProcessError as e: + raise CommandExecutionError, 'Error trying ssh to %s: %s'.format(name, e) + reply = salt.utils.stringutils.to_str(ret) + log.info(reply) + + ## TODO: move this code to salt-cloud driver + ## target_network = config.get_cloud_config_value( + ## 'target_network', vm_, __opts__, default=None) + target_network_range = ipaddress.ip_network(network_mask, strict=False) + + for line in reply.split('\n'): + try: # try to find a bridged network address + # the lines we are looking for appear like: + # "inet addr:10.124.31.185 Bcast:10.124.31.255 Mask:255.255.248.0" + # or "inet6 addr: fe80::a00:27ff:fe04:7aac/64 Scope:Link" + tokens = line.replace('addr:','',1).split() # remove "addr:" if it exists, then split + found_address = None + if "inet" in tokens: + nxt = tokens.index("inet") + 1 + found_address = ipaddress.ip_address(tokens[nxt]) + elif "inet6" in tokens: + nxt = tokens.index("inet6") + 1 + found_address = ipaddress.ip_address(tokens[nxt].split('/')[0]) + if found_address in target_network_range: + ans['ip_address'] = str(found_address) + break # we have located a good matching address + except (IndexError, AttributeError, TypeError): + pass # all syntax and type errors loop here + # falling out if the loop leaves us remembering the last candidate + log.info('Network IP address in %s detected as: %s', + target_network_range, ans.get('ip_address', '(not found)')) + + if get_private_key: + # retrieve the Vagrant private key from the host + try: + with open(ssh_config['IdentityFile']) as pks: + ans['private_key'] = pks.read() + except (OSError, IOError) as e: + raise CommandExecutionError, "Error processing Vagrant private key file: {}".format(e) + return ans From 2524dd9bbad45d4ff5f0b2ce1354369ef6c9c07e Mon Sep 17 00:00:00 2001 From: vernoncole Date: Fri, 15 Sep 2017 15:50:50 -0600 Subject: [PATCH 329/633] skeleton unit test --- tests/unit/modules/test_vagrant.py | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/unit/modules/test_vagrant.py diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py new file mode 100644 index 0000000000..3efef8cd5a --- /dev/null +++ b/tests/unit/modules/test_vagrant.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +# Import python libs +from __future__ import absolute_import +import re + +# Import Salt Testing libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import skipIf, TestCase +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch + +# Import salt libs +import salt.modules.vagrant as vagrant +import salt.modules.config as config +from salt._compat import ElementTree as ET +import salt.utils + +# Import third party libs +import yaml +from salt.ext import six + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class VagrantTestCase(TestCase, LoaderModuleMockMixin): + ''' + Unit TestCase for the salt.modules.vagrant module. + ''' + # def setup_loader_modules(self): + # return {vsphere: {'__virtual__': MagicMock(return_value='vsphere')}} + + def setup_loader_modules(self): + loader_globals = { + '__salt__': { + 'config.get': config.get, + 'config.option': config.option, + } + } + return {vagrant: loader_globals, config: loader_globals} + + def test_boot_default_dev(self): + diskp = vagrant._disk_profile('default', 'kvm') + nicp = vagrant._nic_profile('default', 'kvm') + xml_data = vagrant._gen_xml( + 'hello', + 1, + 512, + diskp, + nicp, + 'kvm' + ) + root = ET.fromstring(xml_data) + self.assertEqual(root.find('os/boot').attrib['dev'], 'hd') + + + def test_gen_xml_for_telnet_console(self): + diskp = vagrant._disk_profile('default', 'kvm') + nicp = vagrant._nic_profile('default', 'kvm') + xml_data = vagrant._gen_xml( + 'hello', + 1, + 512, + diskp, + nicp, + 'kvm', + serial_type='tcp', + console=True, + telnet_port=22223 + ) + root = ET.fromstring(xml_data) + self.assertEqual(root.find('devices/serial').attrib['type'], 'tcp') + self.assertEqual(root.find('devices/console').attrib['type'], 'tcp') + self.assertEqual(root.find('devices/console/source').attrib['service'], '22223') + + + def test_controller_for_kvm(self): + diskp = vagrant._disk_profile('default', 'kvm') + nicp = vagrant._nic_profile('default', 'kvm') + xml_data = vagrant._gen_xml( + 'hello', + 1, + 512, + diskp, + nicp, + 'kvm' + ) + root = ET.fromstring(xml_data) + controllers = root.findall('.//devices/controller') + # There should be no controller + self.assertTrue(len(controllers) == 0) From fb634b986d5d5968d7c1755b54966d47c8a24410 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Mon, 18 Sep 2017 11:45:35 -0600 Subject: [PATCH 330/633] fix get_ssh_config code --- salt/modules/vagrant.py | 44 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 978c74cbc3..ea1938b4a2 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -87,13 +87,16 @@ def _vagrant_ssh_config(vm_): :return: dictionary of ssh stuff ''' machine = vm_['machine'] - ret = {} - log.info('requesting vagrant ssh-config for %s', machine or 'Vagrant default') - output = subprocess.check_output( - [_runas_sudo(vm_, 'vagrant ssh-config {}'.format(machine))], - shell=True, - cwd=vm_.get('cwd', None) - ) + log.info('requesting vagrant ssh-config for VM %s', machine or '(default)') + cmd = _runas_sudo(vm_, 'vagrant ssh-config {}'.format(machine)) + try: + output = subprocess.check_output( + [cmd], + shell=True, + cwd=vm_.get('cwd', None) + ) + except subprocess.CalledProcessError as e: + output = e.output # if the return code was non-zero, use the output anyway reply = salt.utils.stringutils.to_str(output) ssh_config = {} for line in reply.split('\n'): # build a dictionary of the text reply @@ -256,7 +259,6 @@ def init(name, # Salt_id for created VM deploy=None, # load Salt onto the virtual machine, default=True vagrant_provider='', # vagrant provider engine name vm={}, # a dictionary of VM configuration settings - opts=None, # a dictionary of master configuration settings ): ''' Initialize a new Vagrant vm @@ -276,13 +278,13 @@ def init(name, # Salt_id for created VM vm_['runas'] = runas or vm_.get('runas', runas) vm_['deploy'] = deploy if deploy is not None else vm_.get('deploy', True) vm_['vagrant_provider'] = vagrant_provider or vm_.get('vagrant_provider', '') - _update_cache(name, vm_, opts) + _update_cache(name, vm_) if start: log.debug('Starting VM {0}'.format(name)) - ret = _start(name, vm_, opts) + ret = _start(name, vm_) else: - ret = True + ret = 'Name {} defined using VM {}'.format(name, vm_['machine'] or '(default)') return ret @@ -313,14 +315,7 @@ def start(name, vm_={}): return _start(name, vm_) -def _start(name, vm_, opts=None): # internal call name, because "start" is a keyword argument to vagrant.init - fudged_opts = defaultdict(lambda: None) - if opts is None: - fudged_opts.update(__opts__) - fudged_opts.update(_get_cached_opts(name)) - fudged_opts.setdefault('profiles', defaultdict(lambda: '')) - fudged_opts.setdefault('providers', {}) - fudged_opts.setdefault('deploy_scripts_search_path', vm_.get('deploy_scripts_search_path', [])) +def _start(name, vm_): # internal call name, because "start" is a keyword argument to vagrant.init if not vm_: vm_ = _get_cached_vm(name) @@ -489,12 +484,18 @@ def get_ssh_config(name, network_mask='', get_private_key=False): ssh_config = _vagrant_ssh_config(vm_) - ans = { 'key_filename': ssh_config['IdentityFile'], + try: + ans = { 'key_filename': ssh_config['IdentityFile'], 'ssh_username': ssh_config['User'], 'ssh_host': ssh_config['HostName'], 'ssh_port': ssh_config['Port'], } + except KeyError: + raise CommandExecutionError( + 'Insufficient SSH information to contact VM {}. ' + 'Is it running?'.format(vm_.get('machine', '(default)'))) + if network_mask: # ask the new VM to report its network address command = 'ssh -i {IdentityFile} -p {Port} ' \ @@ -511,9 +512,6 @@ def get_ssh_config(name, network_mask='', get_private_key=False): reply = salt.utils.stringutils.to_str(ret) log.info(reply) - ## TODO: move this code to salt-cloud driver - ## target_network = config.get_cloud_config_value( - ## 'target_network', vm_, __opts__, default=None) target_network_range = ipaddress.ip_network(network_mask, strict=False) for line in reply.split('\n'): From c8a78c7ceb6f77f0e3775622c7eec809fbd1c192 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Mon, 18 Sep 2017 21:21:07 -0600 Subject: [PATCH 331/633] provide unit tests --- salt/modules/vagrant.py | 42 ++++----- tests/unit/modules/test_vagrant.py | 140 +++++++++++++++++------------ 2 files changed, 99 insertions(+), 83 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index ea1938b4a2..bb552220cd 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -16,7 +16,7 @@ from collections import defaultdict import salt.cache import salt.utils import salt.utils.stringutils -from salt.exceptions import CommandExecutionError, SaltCacheError +from salt.exceptions import CommandExecutionError, SaltCacheError, SaltInvocationError import salt.ext.six as six if six.PY3: import ipaddress @@ -37,14 +37,12 @@ def __virtual__(): return __virtualname__ -def _update_cache(name, vm_, opts=None): +def _update_cache(name, vm_): vm_cache = salt.cache.Cache(__opts__, expire=13140000) # keep data for ten years vm_cache.store('vagrant', name, vm_) - if opts: - vm_cache.store('vagrant_opts', name, opts) -def _get_cached_vm(name): +def get_vm_info(name): vm_cache = salt.cache.Cache(__opts__) try: vm_ = vm_cache.fetch('vagrant', name) @@ -54,20 +52,10 @@ def _get_cached_vm(name): try: _ = vm_['machine'] except KeyError: - raise ValueError, 'No Vagrant machine defined for Salt-id {}'.format(name) + raise SaltInvocationError, 'No Vagrant machine defined for Salt-id {}'.format(name) return vm_ -def _get_cached_opts(name): - vm_cache = salt.cache.Cache(__opts__) - try: - opts = vm_cache.fetch('vagrant_opts', name) - except SaltCacheError: - log.warn('Trouble reading Salt opts cache for vagrant[%s]', name) - return None - return opts - - def _erase_cache(name): vm_cache = salt.cache.Cache(__opts__) try: @@ -252,12 +240,12 @@ def vm_state(name='', cwd=None): def init(name, # Salt_id for created VM - cwd, # path to find Vagrantfile + cwd=None, # path to find Vagrantfile machine='', # name of machine in Vagrantfile runas=None, # username who owns Vagrant box - start=True, # start the machine when initialized + start=False, # start the machine when initialized deploy=None, # load Salt onto the virtual machine, default=True - vagrant_provider='', # vagrant provider engine name + vagrant_provider='', # vagrant provider (default=virtualbox) vm={}, # a dictionary of VM configuration settings ): ''' @@ -272,8 +260,10 @@ def init(name, # Salt_id for created VM ''' vm_ = vm.copy() # passed configuration data vm_['name'] = name - vm_['cwd'] = cwd # passed-in keyword arguments overwrite vm dictionary values + vm_['cwd'] = cwd or vm_.get('cwd') + if not vm_['cwd']: + raise SaltInvocationError('Path to Vagrantfile must be defined by \'cwd\' argument') vm_['machine'] = machine or vm_.get('machine', machine) vm_['runas'] = runas or vm_.get('runas', runas) vm_['deploy'] = deploy if deploy is not None else vm_.get('deploy', True) @@ -318,7 +308,7 @@ def start(name, vm_={}): def _start(name, vm_): # internal call name, because "start" is a keyword argument to vagrant.init if not vm_: - vm_ = _get_cached_vm(name) + vm_ = get_vm_info(name) try: machine = vm_['machine'] @@ -364,7 +354,7 @@ def stop(name): salt vagrant.stop ''' - vm_ = _get_cached_vm(name) + vm_ = get_vm_info(name) machine = vm_['machine'] cmd = _runas_sudo(vm_, 'vagrant halt {}'.format(machine)) @@ -387,7 +377,7 @@ def pause(name): salt vagrant.pause ''' - vm_ = _get_cached_vm(name) + vm_ = get_vm_info(name) machine = vm_['machine'] cmd = _runas_sudo(vm_, 'vagrant suspend {}'.format(machine)) @@ -410,7 +400,7 @@ def reboot(name): salt vagrant.reboot ''' - vm_ = _get_cached_vm(name) + vm_ = get_vm_info(name) machine = vm_['machine'] cmd = _runas_sudo(vm_, 'vagrant reload {}'.format(machine)) @@ -433,7 +423,7 @@ def destroy(name): salt vagrant.destroy ''' - vm_ = _get_cached_vm(name) + vm_ = get_vm_info(name) machine = vm_['machine'] cmd = _runas_sudo(vm_, 'vagrant destroy -f {}'.format(machine)) @@ -480,7 +470,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): It will send an `ifconfig` command to the VM (using ssh to `ssh_host`:`ssh_port`) and scan the result, returning the IP address of the first interface it can find which matches your mask. ''' - vm_ = _get_cached_vm(name) + vm_ = get_vm_info(name) ssh_config = _vagrant_ssh_config(vm_) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 3efef8cd5a..c5c2285b54 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -2,21 +2,18 @@ # Import python libs from __future__ import absolute_import -import re # Import Salt Testing libs from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import skipIf, TestCase -from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch +from tests.support.mock import NO_MOCK, NO_MOCK_REASON # Import salt libs import salt.modules.vagrant as vagrant import salt.modules.config as config -from salt._compat import ElementTree as ET -import salt.utils +import salt.exceptions # Import third party libs -import yaml from salt.ext import six @@ -25,65 +22,94 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): ''' Unit TestCase for the salt.modules.vagrant module. ''' - # def setup_loader_modules(self): - # return {vsphere: {'__virtual__': MagicMock(return_value='vsphere')}} def setup_loader_modules(self): - loader_globals = { - '__salt__': { - 'config.get': config.get, - 'config.option': config.option, + vagrant_globals = { + '__opts__': { + 'extension_modules': '', + 'cachedir': '/tmp/salt-tests-tmpdir/cache', + 'cache': 'localfs' } } - return {vagrant: loader_globals, config: loader_globals} + return {vagrant: vagrant_globals} - def test_boot_default_dev(self): - diskp = vagrant._disk_profile('default', 'kvm') - nicp = vagrant._nic_profile('default', 'kvm') - xml_data = vagrant._gen_xml( - 'hello', - 1, - 512, - diskp, - nicp, - 'kvm' + + def test_vagrant_get_vm_info(self): + with self.assertRaises(salt.exceptions.SaltInvocationError): + vagrant.get_vm_info('thisNameDoesNotExist') + + + def test_vagrant_init_positional(self): + resp = vagrant.init( + 'test1', + '/tmp/nowhere', + 'onetest', + 'nobody', + False, + True, + 'french', + {'different': 'very'} ) - root = ET.fromstring(xml_data) - self.assertEqual(root.find('os/boot').attrib['dev'], 'hd') + self.assertIsInstance(resp, six.string_types) + resp = vagrant.get_vm_info('test1') + expected = dict(name='test1', + cwd='/tmp/nowhere', + machine='onetest', + runas='nobody', + deploy=True, + vagrant_provider='french', + different= 'very' + ) + self.assertEqual(resp, expected) - def test_gen_xml_for_telnet_console(self): - diskp = vagrant._disk_profile('default', 'kvm') - nicp = vagrant._nic_profile('default', 'kvm') - xml_data = vagrant._gen_xml( - 'hello', - 1, - 512, - diskp, - nicp, - 'kvm', - serial_type='tcp', - console=True, - telnet_port=22223 - ) - root = ET.fromstring(xml_data) - self.assertEqual(root.find('devices/serial').attrib['type'], 'tcp') - self.assertEqual(root.find('devices/console').attrib['type'], 'tcp') - self.assertEqual(root.find('devices/console/source').attrib['service'], '22223') + def test_vagrant_init_dict(self): + testdict = dict(cwd='/tmp/anywhere', + machine='twotest', + runas='somebody', + deploy=True, + vagrant_provider='english') + vagrant.init('test2', vm=testdict) + resp = vagrant.get_vm_info('test2') + testdict['name'] = 'test2' + self.assertEqual(resp, testdict) - def test_controller_for_kvm(self): - diskp = vagrant._disk_profile('default', 'kvm') - nicp = vagrant._nic_profile('default', 'kvm') - xml_data = vagrant._gen_xml( - 'hello', - 1, - 512, - diskp, - nicp, - 'kvm' - ) - root = ET.fromstring(xml_data) - controllers = root.findall('.//devices/controller') - # There should be no controller - self.assertTrue(len(controllers) == 0) + def test_vagrant_get_ssh_config_fails(self): + testdict = dict(cwd='/tmp/there', + machine='treetest', + runas='anybody', + deploy=False, + vagrant_provider='spansh') + vagrant.init('test3', + cwd='/tmp', + machine='threetest', + runas='him', + deploy=True, + vagrant_provider='polish', + vm=testdict) + resp = vagrant.get_vm_info('test3') + expected = dict(name='test3', + cwd='/tmp', + machine='threetest', + runas='him', + deploy=True, + vagrant_provider='polish') + self.assertEqual(resp, expected) + + + def test_vagrant_get_ssh_config_fails(self): + vagrant.init('test3', cwd='/tmp') + with self.assertRaises(salt.exceptions.CommandExecutionError): + vagrant.get_ssh_config('test3') # has not been started + + + def test_vagrant_destroy_removes_cached_entry(self): + vagrant.init('test3', cwd='/tmp') + # VM has a stored value + self.assertEqual(vagrant.get_vm_info('test3')['name'], 'test3') + # clean up (an error is expected -- machine never started) + self.assertFalse(vagrant.destroy('test3')) + # VM no longer exists + with self.assertRaises(salt.exceptions.SaltInvocationError): + vagrant.get_ssh_config('test3') From a1a81eb906029de88558cf32b5e73bd402789d5c Mon Sep 17 00:00:00 2001 From: vernoncole Date: Mon, 18 Sep 2017 21:33:15 -0600 Subject: [PATCH 332/633] supply Vagrant documentation --- doc/ref/modules/all/index.rst | 1 + doc/ref/modules/all/salt.modules.vagrant.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 doc/ref/modules/all/salt.modules.vagrant.rst diff --git a/doc/ref/modules/all/index.rst b/doc/ref/modules/all/index.rst index b9f4b3f35c..21f7c93fa7 100644 --- a/doc/ref/modules/all/index.rst +++ b/doc/ref/modules/all/index.rst @@ -432,6 +432,7 @@ execution modules uptime useradd uwsgi + vagrant varnish vault vbox_guest diff --git a/doc/ref/modules/all/salt.modules.vagrant.rst b/doc/ref/modules/all/salt.modules.vagrant.rst new file mode 100644 index 0000000000..074b986dba --- /dev/null +++ b/doc/ref/modules/all/salt.modules.vagrant.rst @@ -0,0 +1,6 @@ +==================== +salt.modules.vagrant +==================== + +.. automodule:: salt.modules.vagrant + :members: \ No newline at end of file From f82ec9e72d19262fa3513b3c786bf1e3deec1e61 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Mon, 18 Sep 2017 21:46:48 -0600 Subject: [PATCH 333/633] Revert "Oxygen release notes for vagrant and saltify drivers" This reverts commit afbfe86 --- doc/topics/releases/oxygen.rst | 56 +++------------------------------- 1 file changed, 5 insertions(+), 51 deletions(-) diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 2a6450fca9..4c651bfce9 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -110,57 +110,6 @@ file. For example: These commands will run in sequence **before** the bootstrap script is executed. -New salt-cloud Grains -===================== - -When salt cloud creates a new minon, it will now add grain information -to the minion configuration file, identifying the resources originally used -to create it. - -The generated grain information will appear similar to: - -.. code-block:: yaml - - grains: - salt-cloud: - driver: ec2 - provider: my_ec2:ec2 - profile: ec2-web - -The generation of salt-cloud grains can be surpressed by the -option ``enable_cloud_grains: 'False'`` in the cloud configuration file. - -Upgraded Saltify Driver -======================= - -The salt-cloud Saltify driver is used to provision machines which -are not controlled by a dedicated cloud supervisor (such as typical hardware -machines) by pushing a salt-bootstrap command to them and accepting them on -the salt master. Creation of a node has been its only function and no other -salt-cloud commands were implemented. - -With this upgrade, it can use the salt-api to provide advanced control, -such as rebooting a machine, querying it along with conventional cloud minions, -and, ultimately, disconnecting it from its master. - -After disconnection from ("destroying" on) one master, a machine can be -re-purposed by connecting to ("creating" on) a subsequent master. - -New Vagrant Driver -================== - -The salt-cloud Vagrant driver brings virtual machines running in a limited -environment, such as a programmer's workstation, under salt-cloud control. -This can be useful for experimentation, instruction, or testing salt configurations. - -Using salt-api on the master, and a salt-minion running on the host computer, -the Vagrant driver can create (``vagrant up``), restart (``vagrant reload``), -and destroy (``vagrant destroy``) VMs, as controlled by salt-cloud profiles -which designate a ``Vagrantfile`` on the host machine. - -The master can be a very limited machine, such as a Raspberry Pi, or a small -VagrantBox VM. - Newer PyWinRM Versions ---------------------- @@ -695,6 +644,11 @@ Profitbricks Cloud Updated Dependency The minimum version of the ``profitbrick`` python package for the ``profitbricks`` cloud driver has changed from 3.0.0 to 3.1.0. +Azure Cloud Updated Dependency +------------------------------ + +The azure sdk used for the ``azurearm`` cloud driver now depends on ``azure-cli>=2.0.12`` + Module Deprecations =================== From 7fd1e29ce9553f9d037044c791a74eacfec16686 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Tue, 19 Sep 2017 10:10:18 -0600 Subject: [PATCH 334/633] lint fixes --- salt/modules/vagrant.py | 28 ++++++++++++---------------- tests/unit/modules/test_vagrant.py | 11 ++--------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index bb552220cd..001cb91dae 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -10,7 +10,6 @@ Work with virtual machines managed by vagrant from __future__ import absolute_import, print_function import subprocess import logging -from collections import defaultdict # Import salt libs import salt.cache @@ -52,7 +51,7 @@ def get_vm_info(name): try: _ = vm_['machine'] except KeyError: - raise SaltInvocationError, 'No Vagrant machine defined for Salt-id {}'.format(name) + raise SaltInvocationError('No Vagrant machine defined for Salt-id {}'.format(name)) return vm_ @@ -230,7 +229,7 @@ def vm_state(name='', cwd=None): for line in reply.split('\n'): # build a list of the text reply print(line) tokens = line.strip().split() - if len(tokens) > 1 and tokens[-1].endswith(')') : + if len(tokens) > 1 and tokens[-1].endswith(')'): try: info[tokens[0]] = {'state': ' '.join(tokens[1:-1])} info[tokens[0]]['provider'] = tokens[-1].lstrip('(').rstrip(')') @@ -246,7 +245,7 @@ def init(name, # Salt_id for created VM start=False, # start the machine when initialized deploy=None, # load Salt onto the virtual machine, default=True vagrant_provider='', # vagrant provider (default=virtualbox) - vm={}, # a dictionary of VM configuration settings + vm={}, # a dictionary of VM configuration settings # pylint: disable=W0102 ): ''' Initialize a new Vagrant vm @@ -291,7 +290,7 @@ def _runas_sudo(vm_, command): return command -def start(name, vm_={}): +def start(name): ''' Start (vagrant up) a defined virtual machine by salt_id name. The machine must have been previously defined using "vagrant.init". @@ -302,18 +301,16 @@ def start(name, vm_={}): salt vagrant.start ''' + vm_ = get_vm_info(name) return _start(name, vm_) def _start(name, vm_): # internal call name, because "start" is a keyword argument to vagrant.init - if not vm_: - vm_ = get_vm_info(name) - try: machine = vm_['machine'] except KeyError: - raise ValueError, 'No Vagrant machine defined for Salt-id {}'.format(name) + raise SaltInvocationError('No Vagrant machine defined for Salt-id {}'.format(name)) vagrant_provider = vm_.get('vagrant_provider', '') provider_ = '--provider={}'.format(vagrant_provider) if vagrant_provider else '' @@ -328,7 +325,6 @@ def _start(name, vm_): # internal call name, because "start" is a keyword argum return ret == 0 - def shutdown(name): ''' Send a soft shutdown signal to the named vm. @@ -469,13 +465,13 @@ def get_ssh_config(name, network_mask='', get_private_key=False): If you enter a CIDR network mask, the module will attempt to find the VM's address for you. It will send an `ifconfig` command to the VM (using ssh to `ssh_host`:`ssh_port`) and scan the result, returning the IP address of the first interface it can find which matches your mask. - ''' + ''' # pylint: disable=W1401 vm_ = get_vm_info(name) ssh_config = _vagrant_ssh_config(vm_) try: - ans = { 'key_filename': ssh_config['IdentityFile'], + ans = {'key_filename': ssh_config['IdentityFile'], 'ssh_username': ssh_config['User'], 'ssh_host': ssh_config['HostName'], 'ssh_port': ssh_config['Port'], @@ -498,7 +494,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): try: ret = subprocess.check_output([command], shell=True) except subprocess.CalledProcessError as e: - raise CommandExecutionError, 'Error trying ssh to %s: %s'.format(name, e) + raise CommandExecutionError('Error trying ssh to {}: {}'.format(name, e)) reply = salt.utils.stringutils.to_str(ret) log.info(reply) @@ -509,7 +505,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): # the lines we are looking for appear like: # "inet addr:10.124.31.185 Bcast:10.124.31.255 Mask:255.255.248.0" # or "inet6 addr: fe80::a00:27ff:fe04:7aac/64 Scope:Link" - tokens = line.replace('addr:','',1).split() # remove "addr:" if it exists, then split + tokens = line.replace('addr:', '', 1).split() # remove "addr:" if it exists, then split found_address = None if "inet" in tokens: nxt = tokens.index("inet") + 1 @@ -529,8 +525,8 @@ def get_ssh_config(name, network_mask='', get_private_key=False): if get_private_key: # retrieve the Vagrant private key from the host try: - with open(ssh_config['IdentityFile']) as pks: + with salt.utils.fopen(ssh_config['IdentityFile']) as pks: ans['private_key'] = pks.read() except (OSError, IOError) as e: - raise CommandExecutionError, "Error processing Vagrant private key file: {}".format(e) + raise CommandExecutionError("Error processing Vagrant private key file: {}".format(e)) return ans diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index c5c2285b54..0c16e34906 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -10,7 +10,6 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON # Import salt libs import salt.modules.vagrant as vagrant -import salt.modules.config as config import salt.exceptions # Import third party libs @@ -33,12 +32,10 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): } return {vagrant: vagrant_globals} - def test_vagrant_get_vm_info(self): with self.assertRaises(salt.exceptions.SaltInvocationError): vagrant.get_vm_info('thisNameDoesNotExist') - def test_vagrant_init_positional(self): resp = vagrant.init( 'test1', @@ -58,11 +55,10 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): runas='nobody', deploy=True, vagrant_provider='french', - different= 'very' + different='very' ) self.assertEqual(resp, expected) - def test_vagrant_init_dict(self): testdict = dict(cwd='/tmp/anywhere', machine='twotest', @@ -74,8 +70,7 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): testdict['name'] = 'test2' self.assertEqual(resp, testdict) - - def test_vagrant_get_ssh_config_fails(self): + def test_vagrant_get_vm_info(self): testdict = dict(cwd='/tmp/there', machine='treetest', runas='anybody', @@ -97,13 +92,11 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): vagrant_provider='polish') self.assertEqual(resp, expected) - def test_vagrant_get_ssh_config_fails(self): vagrant.init('test3', cwd='/tmp') with self.assertRaises(salt.exceptions.CommandExecutionError): vagrant.get_ssh_config('test3') # has not been started - def test_vagrant_destroy_removes_cached_entry(self): vagrant.init('test3', cwd='/tmp') # VM has a stored value From e43edae9fe46ef66122042a78baa93fafc8e834d Mon Sep 17 00:00:00 2001 From: vernoncole Date: Tue, 19 Sep 2017 12:58:12 -0600 Subject: [PATCH 335/633] correct logic of vm_state query --- salt/modules/vagrant.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 001cb91dae..d0bb0a90c7 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -215,8 +215,12 @@ def vm_state(name='', cwd=None): salt '*' vagrant.vm_state cwd=/projects/project_1 ''' + + machine = get_vm_info(name)['machine'] if name else '' + info = {} - cmd = 'vagrant status {}'.format(name) + + cmd = 'vagrant status {}'.format(machine) log.info('Executing command "%s"', cmd) try: output = subprocess.check_output( @@ -360,7 +364,7 @@ def stop(name): shell=True, cwd=vm_.get('cwd', None) ) - return ret + return ret == 0 def pause(name): @@ -383,7 +387,7 @@ def pause(name): shell=True, cwd=vm_.get('cwd', None) ) - return ret + return ret == 0 def reboot(name): @@ -406,7 +410,7 @@ def reboot(name): shell=True, cwd=vm_.get('cwd', None) ) - return ret + return ret == 0 def destroy(name): From 2e162280a9b2b8995c54bfcf7f6f5accdd3bd15c Mon Sep 17 00:00:00 2001 From: vernoncole Date: Tue, 19 Sep 2017 15:40:57 -0600 Subject: [PATCH 336/633] replace subprocess calls with salt cmd.x calls --- salt/modules/vagrant.py | 140 +++++++---------------------- tests/unit/modules/test_vagrant.py | 8 +- 2 files changed, 39 insertions(+), 109 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index d0bb0a90c7..a5480828a8 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -5,10 +5,8 @@ Work with virtual machines managed by vagrant .. versionadded:: Oxygen ''' - # Import python libs from __future__ import absolute_import, print_function -import subprocess import logging # Import salt libs @@ -75,16 +73,11 @@ def _vagrant_ssh_config(vm_): ''' machine = vm_['machine'] log.info('requesting vagrant ssh-config for VM %s', machine or '(default)') - cmd = _runas_sudo(vm_, 'vagrant ssh-config {}'.format(machine)) - try: - output = subprocess.check_output( - [cmd], - shell=True, - cwd=vm_.get('cwd', None) - ) - except subprocess.CalledProcessError as e: - output = e.output # if the return code was non-zero, use the output anyway - reply = salt.utils.stringutils.to_str(output) + cmd = 'vagrant ssh-config {}'.format(machine) + reply = __salt__['cmd.shell'](cmd, + runas=vm_.get('runas'), + cwd=vm_.get('cwd'), + ignore_retcode=True) ssh_config = {} for line in reply.split('\n'): # build a dictionary of the text reply tokens = line.strip().split() @@ -105,12 +98,7 @@ def version(): salt '*' vagrant.version ''' cmd = 'vagrant -v' - try: - output = subprocess.check_output([cmd], shell=True) - except subprocess.CalledProcessError: - return 'Error: subprocess error calling ' + cmd - reply = salt.utils.stringutils.to_str(output) - return reply.strip() + return __salt__['cmd.shell'](cmd) def list_domains(): @@ -122,15 +110,14 @@ def list_domains(): .. code-block:: bash salt '*' vagrant.list_domains + + The above shows information about all known Vagrant environments + on this machine. This data is cached and may not be completely + up-to-date. ''' vms = [] cmd = 'vagrant global-status {}' - log.info('Executing command "%s"', cmd) - try: - output = subprocess.check_output([cmd], shell=True) - except subprocess.CalledProcessError: - return [] - reply = salt.utils.stringutils.to_str(output) + reply = __salt__['cmd.shell'](cmd) for line in reply.split('\n'): # build a list of the text reply print(line) tokens = line.strip().split() @@ -154,15 +141,7 @@ def list_active_vms(cwd=None): ''' vms = [] cmd = 'vagrant status' - log.info('Executing command "%s"', cmd) - try: - output = subprocess.check_output( - [cmd], - shell=True, - cwd=cwd) - except subprocess.CalledProcessError: - return [] - reply = salt.utils.stringutils.to_str(output) + reply = __salt__['cmd.shell'](cmd, cwd=cwd) for line in reply.split('\n'): # build a list of the text reply print(line) tokens = line.strip().split() @@ -184,15 +163,7 @@ def list_inactive_vms(cwd=None): ''' vms = [] cmd = 'vagrant status' - log.info('Executing command "%s"', cmd) - try: - output = subprocess.check_output( - [cmd], - shell=True, - cwd=cwd) - except subprocess.CalledProcessError: - return [] - reply = salt.utils.stringutils.to_str(output) + reply = __salt__['cmd.shell'](cmd, cwd=cwd) for line in reply.split('\n'): # build a list of the text reply print(line) tokens = line.strip().split() @@ -217,19 +188,9 @@ def vm_state(name='', cwd=None): ''' machine = get_vm_info(name)['machine'] if name else '' - info = {} - cmd = 'vagrant status {}'.format(machine) - log.info('Executing command "%s"', cmd) - try: - output = subprocess.check_output( - [cmd], - shell=True, - cwd=cwd) - except subprocess.CalledProcessError: - return {} - reply = salt.utils.stringutils.to_str(output) + reply = __salt__['cmd.shell'](cmd, cwd) for line in reply.split('\n'): # build a list of the text reply print(line) tokens = line.strip().split() @@ -281,19 +242,6 @@ def init(name, # Salt_id for created VM return ret -def _runas_sudo(vm_, command): - ''' - prepend "sudo -u " if _vm['runas'] is defined - :param vm_: the virtual machine configuration dictionary - :param command: the command line which will be sent - :return: "sudo -u command" or "command" as needed - ''' - runas = vm_.get('runas', False) - if runas: - return 'sudo -u {} {}'.format(runas, command) - return command - - def start(name): ''' Start (vagrant up) a defined virtual machine by salt_id name. @@ -318,13 +266,8 @@ def _start(name, vm_): # internal call name, because "start" is a keyword argum vagrant_provider = vm_.get('vagrant_provider', '') provider_ = '--provider={}'.format(vagrant_provider) if vagrant_provider else '' - cmd = _runas_sudo(vm_, 'vagrant up {} {}'.format(machine, provider_)) - log.info('Executing command "%s"', cmd) - ret = subprocess.call( - [cmd], - shell=True, - cwd=vm_.get('cwd', None) - ) + cmd = 'vagrant up {} {}'.format(machine, provider_) + ret = __salt__['cmd.retcode'](cmd, runes=vm_.get('runas'), cwd=vm_.get('cwd')) return ret == 0 @@ -357,13 +300,10 @@ def stop(name): vm_ = get_vm_info(name) machine = vm_['machine'] - cmd = _runas_sudo(vm_, 'vagrant halt {}'.format(machine)) - log.info('Executing command "%s"', cmd) - ret = subprocess.call( - [cmd], - shell=True, - cwd=vm_.get('cwd', None) - ) + cmd = 'vagrant halt {}'.format(machine) + ret = __salt__['cmd.retcode'](cmd, + runas=vm_.get('runas'), + cwd=vm_.get('cwd')) return ret == 0 @@ -380,13 +320,10 @@ def pause(name): vm_ = get_vm_info(name) machine = vm_['machine'] - cmd = _runas_sudo(vm_, 'vagrant suspend {}'.format(machine)) - log.info('Executing command "%s"', cmd) - ret = subprocess.call( - [cmd], - shell=True, - cwd=vm_.get('cwd', None) - ) + cmd = 'vagrant suspend {}'.format(machine) + ret = __salt__['cmd.retcode'](cmd, + runas=vm_.get('runas'), + cwd=vm_.get('cwd')) return ret == 0 @@ -403,13 +340,10 @@ def reboot(name): vm_ = get_vm_info(name) machine = vm_['machine'] - cmd = _runas_sudo(vm_, 'vagrant reload {}'.format(machine)) - log.info('Executing command "%s"', cmd) - ret = subprocess.call( - [cmd], - shell=True, - cwd=vm_.get('cwd', None) - ) + cmd = 'vagrant reload {}'.format(machine) + ret = __salt__['cmd.retcode'](cmd, + runas=vm_.get('runas'), + cwd=vm_.get('cwd')) return ret == 0 @@ -426,13 +360,10 @@ def destroy(name): vm_ = get_vm_info(name) machine = vm_['machine'] - cmd = _runas_sudo(vm_, 'vagrant destroy -f {}'.format(machine)) - log.info('Executing command "%s"', cmd) - ret = subprocess.call( - [cmd], - shell=True, - cwd=vm_.get('cwd', None) - ) + cmd = 'vagrant destroy -f {}'.format(machine) + ret = __salt__['cmd.retcode'](cmd, + runas=vm_.get('runas'), + cwd=vm_.get('cwd')) _erase_cache(name) return ret == 0 @@ -495,12 +426,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): '{User}@{HostName} ifconfig'.format(**ssh_config) log.info('Trying ssh -p {Port} {User}@{HostName} ifconfig'.format(**ssh_config)) - try: - ret = subprocess.check_output([command], shell=True) - except subprocess.CalledProcessError as e: - raise CommandExecutionError('Error trying ssh to {}: {}'.format(name, e)) - reply = salt.utils.stringutils.to_str(ret) - log.info(reply) + reply = __salt__['cmd.shell'](command) target_network_range = ipaddress.ip_network(network_mask, strict=False) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 0c16e34906..ccfe70625d 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -10,6 +10,7 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON # Import salt libs import salt.modules.vagrant as vagrant +import salt.modules.cmdmod as cmd import salt.exceptions # Import third party libs @@ -28,8 +29,11 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): 'extension_modules': '', 'cachedir': '/tmp/salt-tests-tmpdir/cache', 'cache': 'localfs' - } - } + }, + '__salt__': { + 'cmd.shell': cmd.shell, + 'cmd.retcode': cmd.retcode, + }} return {vagrant: vagrant_globals} def test_vagrant_get_vm_info(self): From 879ae3b29180baa074a97619143cc95fd2c06204 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Tue, 19 Sep 2017 17:24:55 -0600 Subject: [PATCH 337/633] lint fix round 2 --- salt/modules/vagrant.py | 14 +++++++------- tests/unit/modules/test_vagrant.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index a5480828a8..25882eb522 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -208,9 +208,9 @@ def init(name, # Salt_id for created VM machine='', # name of machine in Vagrantfile runas=None, # username who owns Vagrant box start=False, # start the machine when initialized - deploy=None, # load Salt onto the virtual machine, default=True - vagrant_provider='', # vagrant provider (default=virtualbox) - vm={}, # a dictionary of VM configuration settings # pylint: disable=W0102 + deploy=None, # flag suggesting whether to load Salt onto the VM + vagrant_provider='', # vagrant provider (default=virtualbox) + vm=None, # a dictionary of VM configuration settings ): ''' Initialize a new Vagrant vm @@ -222,9 +222,9 @@ def init(name, # Salt_id for created VM salt vagrant.init /path/to/Vagrantfile salt my_laptop vagrant.init x1 /projects/bevy_master machine=quail1 ''' - vm_ = vm.copy() # passed configuration data + vm_ = {} if vm is None else vm.copy() # passed configuration data vm_['name'] = name - # passed-in keyword arguments overwrite vm dictionary values + # passed-in keyword arguments overwrite vm dictionary values vm_['cwd'] = cwd or vm_.get('cwd') if not vm_['cwd']: raise SaltInvocationError('Path to Vagrantfile must be defined by \'cwd\' argument') @@ -369,7 +369,7 @@ def destroy(name): def get_ssh_config(name, network_mask='', get_private_key=False): - ''' + r''' Retrieve hints of how you might connect to a Vagrant VM. CLI Example: @@ -400,7 +400,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): If you enter a CIDR network mask, the module will attempt to find the VM's address for you. It will send an `ifconfig` command to the VM (using ssh to `ssh_host`:`ssh_port`) and scan the result, returning the IP address of the first interface it can find which matches your mask. - ''' # pylint: disable=W1401 + ''' vm_ = get_vm_info(name) ssh_config = _vagrant_ssh_config(vm_) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index ccfe70625d..f19a6adfc4 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -74,7 +74,7 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): testdict['name'] = 'test2' self.assertEqual(resp, testdict) - def test_vagrant_get_vm_info(self): + def test_vagrant_init_arg_override(self): testdict = dict(cwd='/tmp/there', machine='treetest', runas='anybody', From e086375ec8c20881af50b13e7ba72effaed0280f Mon Sep 17 00:00:00 2001 From: vernoncole Date: Tue, 19 Sep 2017 19:58:23 -0600 Subject: [PATCH 338/633] clean up cache even if other steps fail --- salt/modules/vagrant.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 25882eb522..afd3e68066 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -361,10 +361,12 @@ def destroy(name): machine = vm_['machine'] cmd = 'vagrant destroy -f {}'.format(machine) - ret = __salt__['cmd.retcode'](cmd, + try: + ret = __salt__['cmd.retcode'](cmd, runas=vm_.get('runas'), cwd=vm_.get('cwd')) - _erase_cache(name) + finally: + _erase_cache(name) return ret == 0 From b10ede33e245c5f6e56ae3805193109ca83538a7 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Wed, 20 Sep 2017 05:33:51 -0600 Subject: [PATCH 339/633] Clean up cache even if Vagrant not found --- salt/modules/vagrant.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index afd3e68066..99331759f0 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -365,6 +365,8 @@ def destroy(name): ret = __salt__['cmd.retcode'](cmd, runas=vm_.get('runas'), cwd=vm_.get('cwd')) + except (OSError, CommandExecutionError): + pass finally: _erase_cache(name) return ret == 0 From 1ef63642dce08803330ffa11900aca164a555679 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Wed, 20 Sep 2017 12:09:55 -0600 Subject: [PATCH 340/633] return an error code if Vagrant command fails --- salt/modules/vagrant.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 99331759f0..47b9e652ab 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -366,11 +366,11 @@ def destroy(name): runas=vm_.get('runas'), cwd=vm_.get('cwd')) except (OSError, CommandExecutionError): - pass + ret = 1 finally: _erase_cache(name) return ret == 0 - +ls def get_ssh_config(name, network_mask='', get_private_key=False): r''' From 61b08d7529d51958a4d8412ba6e912c5d40758a6 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Wed, 20 Sep 2017 13:21:12 -0600 Subject: [PATCH 341/633] typo --- salt/modules/vagrant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 47b9e652ab..4ddd3eb303 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -370,7 +370,7 @@ def destroy(name): finally: _erase_cache(name) return ret == 0 -ls + def get_ssh_config(name, network_mask='', get_private_key=False): r''' From 0c91076ca1c48496a73407b88a864582bdf79bf8 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Mon, 25 Sep 2017 10:31:56 -0600 Subject: [PATCH 342/633] remove debug print() lines --- salt/modules/vagrant.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 4ddd3eb303..b1a2fe9a4a 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -46,11 +46,9 @@ def get_vm_info(name): except SaltCacheError: vm_ = {} log.error('Trouble reading Salt cache for vagrant[%s]', name) - try: - _ = vm_['machine'] - except KeyError: - raise SaltInvocationError('No Vagrant machine defined for Salt-id {}'.format(name)) - return vm_ + if 'machine' not in vm_: + raise SaltInvocationError( + 'No Vagrant machine defined for Salt-id {}'.format(name)) def _erase_cache(name): @@ -143,7 +141,6 @@ def list_active_vms(cwd=None): cmd = 'vagrant status' reply = __salt__['cmd.shell'](cmd, cwd=cwd) for line in reply.split('\n'): # build a list of the text reply - print(line) tokens = line.strip().split() if len(tokens) > 1: if tokens[1] == 'running': @@ -165,7 +162,6 @@ def list_inactive_vms(cwd=None): cmd = 'vagrant status' reply = __salt__['cmd.shell'](cmd, cwd=cwd) for line in reply.split('\n'): # build a list of the text reply - print(line) tokens = line.strip().split() if len(tokens) > 1 and tokens[-1].endswith(')'): if tokens[1] != 'running': @@ -192,7 +188,6 @@ def vm_state(name='', cwd=None): cmd = 'vagrant status {}'.format(machine) reply = __salt__['cmd.shell'](cmd, cwd) for line in reply.split('\n'): # build a list of the text reply - print(line) tokens = line.strip().split() if len(tokens) > 1 and tokens[-1].endswith(')'): try: From 4a06d337bb1792e519d6be711a5f645a0ecb0046 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Mon, 25 Sep 2017 14:50:40 -0600 Subject: [PATCH 343/633] revert missing return statement --- salt/modules/vagrant.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index b1a2fe9a4a..da368c5947 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -49,6 +49,7 @@ def get_vm_info(name): if 'machine' not in vm_: raise SaltInvocationError( 'No Vagrant machine defined for Salt-id {}'.format(name)) + return vm_ def _erase_cache(name): From fd85841e2e6bb89c13e57bb8dbbf48d3b1976df8 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Mon, 25 Sep 2017 14:56:28 -0600 Subject: [PATCH 344/633] remove unused keyword argument 'deploy' --- salt/modules/vagrant.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index da368c5947..b8a135a4d8 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -15,6 +15,7 @@ import salt.utils import salt.utils.stringutils from salt.exceptions import CommandExecutionError, SaltCacheError, SaltInvocationError import salt.ext.six as six + if six.PY3: import ipaddress else: @@ -29,6 +30,7 @@ def __virtual__(): ''' run Vagrant commands if possible ''' + # noinspection PyUnresolvedReferences if salt.utils.path.which('vagrant') is None: return False, 'The vagrant module could not be loaded: vagrant command not found' return __virtualname__ @@ -200,11 +202,10 @@ def vm_state(name='', cwd=None): def init(name, # Salt_id for created VM - cwd=None, # path to find Vagrantfile + cwd=None, # path to find Vagrantfile machine='', # name of machine in Vagrantfile runas=None, # username who owns Vagrant box start=False, # start the machine when initialized - deploy=None, # flag suggesting whether to load Salt onto the VM vagrant_provider='', # vagrant provider (default=virtualbox) vm=None, # a dictionary of VM configuration settings ): @@ -226,7 +227,6 @@ def init(name, # Salt_id for created VM raise SaltInvocationError('Path to Vagrantfile must be defined by \'cwd\' argument') vm_['machine'] = machine or vm_.get('machine', machine) vm_['runas'] = runas or vm_.get('runas', runas) - vm_['deploy'] = deploy if deploy is not None else vm_.get('deploy', True) vm_['vagrant_provider'] = vagrant_provider or vm_.get('vagrant_provider', '') _update_cache(name, vm_) @@ -359,8 +359,8 @@ def destroy(name): cmd = 'vagrant destroy -f {}'.format(machine) try: ret = __salt__['cmd.retcode'](cmd, - runas=vm_.get('runas'), - cwd=vm_.get('cwd')) + runas=vm_.get('runas'), + cwd=vm_.get('cwd')) except (OSError, CommandExecutionError): ret = 1 finally: @@ -407,15 +407,15 @@ def get_ssh_config(name, network_mask='', get_private_key=False): try: ans = {'key_filename': ssh_config['IdentityFile'], - 'ssh_username': ssh_config['User'], - 'ssh_host': ssh_config['HostName'], - 'ssh_port': ssh_config['Port'], - } + 'ssh_username': ssh_config['User'], + 'ssh_host': ssh_config['HostName'], + 'ssh_port': ssh_config['Port'], + } except KeyError: raise CommandExecutionError( - 'Insufficient SSH information to contact VM {}. ' - 'Is it running?'.format(vm_.get('machine', '(default)'))) + 'Insufficient SSH information to contact VM {}. ' + 'Is it running?'.format(vm_.get('machine', '(default)'))) if network_mask: # ask the new VM to report its network address @@ -431,7 +431,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): target_network_range = ipaddress.ip_network(network_mask, strict=False) for line in reply.split('\n'): - try: # try to find a bridged network address + try: # try to find a bridged network address # the lines we are looking for appear like: # "inet addr:10.124.31.185 Bcast:10.124.31.255 Mask:255.255.248.0" # or "inet6 addr: fe80::a00:27ff:fe04:7aac/64 Scope:Link" @@ -448,7 +448,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): break # we have located a good matching address except (IndexError, AttributeError, TypeError): pass # all syntax and type errors loop here - # falling out if the loop leaves us remembering the last candidate + # falling out if the loop leaves us remembering the last candidate log.info('Network IP address in %s detected as: %s', target_network_range, ans.get('ip_address', '(not found)')) From eb5cd38f856fdeccb694387939fd76f278edc39a Mon Sep 17 00:00:00 2001 From: vernoncole Date: Mon, 25 Sep 2017 15:09:13 -0600 Subject: [PATCH 345/633] remove unused keyword argument 'deploy' in tests too --- tests/unit/modules/test_vagrant.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index f19a6adfc4..e06c8b9ad5 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -47,7 +47,6 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): 'onetest', 'nobody', False, - True, 'french', {'different': 'very'} ) @@ -57,7 +56,6 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): cwd='/tmp/nowhere', machine='onetest', runas='nobody', - deploy=True, vagrant_provider='french', different='very' ) @@ -67,7 +65,6 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): testdict = dict(cwd='/tmp/anywhere', machine='twotest', runas='somebody', - deploy=True, vagrant_provider='english') vagrant.init('test2', vm=testdict) resp = vagrant.get_vm_info('test2') @@ -78,13 +75,11 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): testdict = dict(cwd='/tmp/there', machine='treetest', runas='anybody', - deploy=False, vagrant_provider='spansh') vagrant.init('test3', cwd='/tmp', machine='threetest', runas='him', - deploy=True, vagrant_provider='polish', vm=testdict) resp = vagrant.get_vm_info('test3') @@ -92,7 +87,6 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): cwd='/tmp', machine='threetest', runas='him', - deploy=True, vagrant_provider='polish') self.assertEqual(resp, expected) From 88d9e7ed83e8bb1bdff97fe32e2a4c4988ae6429 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Tue, 26 Sep 2017 23:17:37 -0600 Subject: [PATCH 346/633] replace cache storage with sdb --- salt/modules/vagrant.py | 184 ++++++++++++++++++++++++----- tests/unit/modules/test_vagrant.py | 22 +++- 2 files changed, 171 insertions(+), 35 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index b8a135a4d8..2a61927104 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -2,18 +2,38 @@ ''' Work with virtual machines managed by vagrant +Mapping between a Salt node id and the Vagrant machine name +(and the path to the Vagrantfile where it is defined) +is stored in a Salt sdb database on the host (minion) machine. + + .. requirements: + - the VM host machine must have salt-minion, Vagrant and a vm provider + installed. + - the VM host must have a valid definition for `sdb://vagrant_sdb_data` + + Configuration example: + + .. code-block:: yaml + + # file /etc/salt/minion.d/vagrant_sdb.conf + vagrant_sdb_data: + driver: sqlite3 + database: /var/cache/salt/vagrant.sqlite + table: sdb + create_table: True + .. versionadded:: Oxygen + ''' # Import python libs from __future__ import absolute_import, print_function import logging +import os # Import salt libs -import salt.cache import salt.utils -import salt.utils.stringutils -from salt.exceptions import CommandExecutionError, SaltCacheError, SaltInvocationError +from salt.exceptions import CommandExecutionError, SaltInvocationError import salt.ext.six as six if six.PY3: @@ -25,6 +45,7 @@ log = logging.getLogger(__name__) __virtualname__ = 'vagrant' +VAGRANT_SDB_URL = 'sdb://vagrant_sdb_data/' def __virtual__(): ''' @@ -36,33 +57,102 @@ def __virtual__(): return __virtualname__ -def _update_cache(name, vm_): - vm_cache = salt.cache.Cache(__opts__, expire=13140000) # keep data for ten years - vm_cache.store('vagrant', name, vm_) +def _build_sdb_uri(key): + ''' + returns string used to fetch data for "key" from the sdb store. + + Salt node id's are used as the key for vm_ dicts. + + ''' + return '{}{}'.format(VAGRANT_SDB_URL, key) + + +def _build_machine_uri(machine, cwd): + ''' + returns string used to fetch id names from the sdb store. + + the cwd and machine name are concatenated with '?' which should + never collide with a Salt node id -- which is important since we + will be storing both in the same table. + ''' + key = '{}?{}'.format(machine, os.path.abspath(cwd)) + return _build_sdb_uri(key) + + +def _update_vm_info(name, vm_): + ''' store the vm_ information keyed by name ''' + __utils__['sdb.sdb_set'](_build_sdb_uri(name), vm_, __opts__) + + # store machine-to-name mapping, too + if vm_['machine']: + __utils__['sdb.sdb_set']( + _build_machine_uri(vm_['machine'], vm_.get('cwd', '.')), + name, + __opts__) def get_vm_info(name): - vm_cache = salt.cache.Cache(__opts__) + ''' + get the information for VM named `name` + :param name: the VM's info as we have it now + :return: dictionary of {'machine': x, 'cwd': y} etc. + ''' try: - vm_ = vm_cache.fetch('vagrant', name) - except SaltCacheError: - vm_ = {} - log.error('Trouble reading Salt cache for vagrant[%s]', name) - if 'machine' not in vm_: + vm_ = __utils__['sdb.sdb_get'](_build_sdb_uri(name), __opts__) + except KeyError: + raise SaltInvocationError( + 'Probable sdb driver not found. Check your configuration.') + if vm_ is None or 'machine' not in vm_: raise SaltInvocationError( 'No Vagrant machine defined for Salt-id {}'.format(name)) return vm_ -def _erase_cache(name): - vm_cache = salt.cache.Cache(__opts__) +def get_machine_id(machine, cwd): + ''' + returns the Salt node name of the Vagrant VM + + :param machine: the Vagrant machine name + :param cwd: the path to Vagrantfile + :return: Salt node name + ''' + name = __utils__['sdb.sdb_get'](_build_machine_uri(machine, cwd)) + if name is None: + raise SaltInvocationError( + 'No Salt name found for Vagrant machine {} in {}'.format( + machine, cwd)) + return name + + +def _erase_vm_info(name): + ''' + erase the information for a VM the we are destroying. + + some sdb drivers (such as the SQLite driver we expect to use) + do not have a `delete` method, so if the delete fails, we have + to replace the with a blank entry. + ''' try: - vm_cache.flush('vagrant', name) - except SaltCacheError: + # delete the machine record + vm_ = get_vm_info(name) + if vm_['machine']: + key = _build_machine_uri(vm_['machine'], vm_.get('cwd', '.')) + try: + __utils__['sdb.sdb_delete'](key, __opts__) + except KeyError: + # no delete method found -- load a blank value + __utils__['sdb.sdb_set'](key, '', __opts__) + except Exception: pass + + uri = _build_sdb_uri(name) try: - vm_cache.flush('vagrant_opts', name) - except SaltCacheError: + # delete the name record + __utils__['sdb.sdb_delete'](uri, __opts__) + except KeyError: + # no delete method found -- load an empty dictionary + __utils__['sdb.sdb_set'](uri, {}, __opts__) + except Exception: pass @@ -117,16 +207,20 @@ def list_domains(): up-to-date. ''' vms = [] - cmd = 'vagrant global-status {}' + cmd = 'vagrant global-status' reply = __salt__['cmd.shell'](cmd) + log.info('--->\n' + reply) for line in reply.split('\n'): # build a list of the text reply - print(line) tokens = line.strip().split() try: _ = int(tokens[0], 16) # valid id numbers are hexadecimal - vms.append(' '.join(tokens)) except (ValueError, IndexError): - pass # skip other lines + continue # skip lines without valid id numbers + machine = tokens[1] + cwd = tokens[-1] + name = get_machine_id(machine, cwd) + if name: + vms.append(name) return vms @@ -143,6 +237,7 @@ def list_active_vms(cwd=None): vms = [] cmd = 'vagrant status' reply = __salt__['cmd.shell'](cmd, cwd=cwd) + log.info('--->\n' + reply) for line in reply.split('\n'): # build a list of the text reply tokens = line.strip().split() if len(tokens) > 1: @@ -164,6 +259,7 @@ def list_inactive_vms(cwd=None): vms = [] cmd = 'vagrant status' reply = __salt__['cmd.shell'](cmd, cwd=cwd) + log.info('--->\n' + reply) for line in reply.split('\n'): # build a list of the text reply tokens = line.strip().split() if len(tokens) > 1 and tokens[-1].endswith(')'): @@ -177,7 +273,8 @@ def vm_state(name='', cwd=None): Return list of all the vms and their state. If you pass a VM name in as an argument then it will return info - for just the named VM, otherwise it will return all VMs. + for just the named VM, otherwise it will return all VMs defined by + the Vagrantfile in the `cwd` directory. CLI Example: @@ -186,16 +283,29 @@ def vm_state(name='', cwd=None): salt '*' vagrant.vm_state cwd=/projects/project_1 ''' - machine = get_vm_info(name)['machine'] if name else '' - info = {} + if name: + vm_ = get_vm_info(name) + machine = vm_['machine'] + cwd = vm_['cwd'] or cwd # usually ignore passed-in cwd + else: + if not cwd: + raise SaltInvocationError('Path to Vagranfile must be defined, but cwd=%s', cwd) + machine = '' + + info = [] cmd = 'vagrant status {}'.format(machine) reply = __salt__['cmd.shell'](cmd, cwd) + log.info('--->\n' + reply) for line in reply.split('\n'): # build a list of the text reply tokens = line.strip().split() if len(tokens) > 1 and tokens[-1].endswith(')'): try: - info[tokens[0]] = {'state': ' '.join(tokens[1:-1])} - info[tokens[0]]['provider'] = tokens[-1].lstrip('(').rstrip(')') + datum = {'machine': tokens[0], + 'state': ' '.join(tokens[1:-1]), + 'provider': tokens[-1].lstrip('(').rstrip(')'), + 'name': name or get_machine_id(tokens[0], cwd) + } + info.append(datum) except IndexError: pass return info @@ -228,7 +338,7 @@ def init(name, # Salt_id for created VM vm_['machine'] = machine or vm_.get('machine', machine) vm_['runas'] = runas or vm_.get('runas', runas) vm_['vagrant_provider'] = vagrant_provider or vm_.get('vagrant_provider', '') - _update_cache(name, vm_) + _update_vm_info(name, vm_) if start: log.debug('Starting VM {0}'.format(name)) @@ -263,9 +373,19 @@ def _start(name, vm_): # internal call name, because "start" is a keyword argum vagrant_provider = vm_.get('vagrant_provider', '') provider_ = '--provider={}'.format(vagrant_provider) if vagrant_provider else '' cmd = 'vagrant up {} {}'.format(machine, provider_) - ret = __salt__['cmd.retcode'](cmd, runes=vm_.get('runas'), cwd=vm_.get('cwd')) + ret = __salt__['cmd.run_all'](cmd, runas=vm_.get('runas'), cwd=vm_.get('cwd'), output_loglevel='info') - return ret == 0 + if machine == '': # we were called using the default machine + for line in ret['stdout'].split('\n'): # find its actual Vagrant name + if line.startswith('==>'): + machine = line.split()[1].rstrip(':') + vm_['machine'] = machine + _update_vm_info(name, vm_) # and remember the true name + break + + if ret['retcode'] == 0: + return 'Started "{}" using Vagrant machine "{}".'.format(name, machine) + return False def shutdown(name): @@ -364,7 +484,7 @@ def destroy(name): except (OSError, CommandExecutionError): ret = 1 finally: - _erase_cache(name) + _erase_vm_info(name) return ret == 0 @@ -427,7 +547,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): log.info('Trying ssh -p {Port} {User}@{HostName} ifconfig'.format(**ssh_config)) reply = __salt__['cmd.shell'](command) - + log.info('--->\n' + reply) target_network_range = ipaddress.ip_network(network_mask, strict=False) for line in reply.split('\n'): diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index e06c8b9ad5..1099aa2167 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -3,6 +3,7 @@ # Import python libs from __future__ import absolute_import +import os # Import Salt Testing libs from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import skipIf, TestCase @@ -11,29 +12,44 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON # Import salt libs import salt.modules.vagrant as vagrant import salt.modules.cmdmod as cmd +import salt.utils.sdb as sdb import salt.exceptions # Import third party libs from salt.ext import six +TEMP_DATABASE_FILE = '/tmp/test_vagrant.sqlite' @skipIf(NO_MOCK, NO_MOCK_REASON) class VagrantTestCase(TestCase, LoaderModuleMockMixin): ''' Unit TestCase for the salt.modules.vagrant module. ''' + @classmethod + def tearDownClass(cls): + os.unlink(TEMP_DATABASE_FILE) def setup_loader_modules(self): vagrant_globals = { '__opts__': { 'extension_modules': '', - 'cachedir': '/tmp/salt-tests-tmpdir/cache', - 'cache': 'localfs' + 'vagrant_sdb_data': { + 'driver': 'sqlite3', + 'database': TEMP_DATABASE_FILE, + 'table': 'sdb', + 'create_table': True + } }, '__salt__': { 'cmd.shell': cmd.shell, 'cmd.retcode': cmd.retcode, - }} + }, + '__utils__': { + 'sdb.sdb_set': sdb.sdb_set, + 'sdb.sdb_get': sdb.sdb_get, + 'sdb.sdb_delete': sdb.sdb_delete + } + } return {vagrant: vagrant_globals} def test_vagrant_get_vm_info(self): From 1f4451faaf31340f98dcd97fecbdbf75b470ebed Mon Sep 17 00:00:00 2001 From: vernoncole Date: Wed, 27 Sep 2017 13:06:08 -0600 Subject: [PATCH 347/633] lint and documentation cleanup for vagrant module --- salt/modules/vagrant.py | 115 ++++++++++++++++++++--------- tests/unit/modules/test_vagrant.py | 1 + 2 files changed, 81 insertions(+), 35 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 2a61927104..5b2d7b80d3 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -1,15 +1,19 @@ # -*- coding: utf-8 -*- ''' -Work with virtual machines managed by vagrant +Work with virtual machines managed by Vagrant. + +.. versionadded:: Oxygen Mapping between a Salt node id and the Vagrant machine name (and the path to the Vagrantfile where it is defined) -is stored in a Salt sdb database on the host (minion) machine. +is stored in a Salt sdb database on the Vagrant host (minion) machine. +In order to use this module, sdb must be configured. An SQLite +database is the recommended storage method. The URI used for +the sdb lookup is "sdb://vagrant_sdb_data". - .. requirements: - - the VM host machine must have salt-minion, Vagrant and a vm provider - installed. - - the VM host must have a valid definition for `sdb://vagrant_sdb_data` +requirements: + - the VM host machine must have salt-minion, Vagrant and a vm provider installed. + - the VM host must have a valid definition for `sdb://vagrant_sdb_data` Configuration example: @@ -22,8 +26,6 @@ is stored in a Salt sdb database on the host (minion) machine. table: sdb create_table: True - .. versionadded:: Oxygen - ''' # Import python libs @@ -47,6 +49,7 @@ __virtualname__ = 'vagrant' VAGRANT_SDB_URL = 'sdb://vagrant_sdb_data/' + def __virtual__(): ''' run Vagrant commands if possible @@ -93,9 +96,10 @@ def _update_vm_info(name, vm_): def get_vm_info(name): ''' - get the information for VM named `name` - :param name: the VM's info as we have it now - :return: dictionary of {'machine': x, 'cwd': y} etc. + get the information for a VM. + + :param name: salt_id name + :return: dictionary of {'machine': x, 'cwd': y, ...}. ''' try: vm_ = __utils__['sdb.sdb_get'](_build_sdb_uri(name), __opts__) @@ -104,17 +108,17 @@ def get_vm_info(name): 'Probable sdb driver not found. Check your configuration.') if vm_ is None or 'machine' not in vm_: raise SaltInvocationError( - 'No Vagrant machine defined for Salt-id {}'.format(name)) + 'No Vagrant machine defined for Salt_id {}'.format(name)) return vm_ def get_machine_id(machine, cwd): ''' - returns the Salt node name of the Vagrant VM + returns the salt_id name of the Vagrant VM :param machine: the Vagrant machine name :param cwd: the path to Vagrantfile - :return: Salt node name + :return: salt_id name ''' name = __utils__['sdb.sdb_get'](_build_machine_uri(machine, cwd)) if name is None: @@ -159,6 +163,7 @@ def _erase_vm_info(name): def _vagrant_ssh_config(vm_): ''' get the information for ssh communication from the new VM + :param vm_: the VM's info as we have it now :return: dictionary of ssh stuff ''' @@ -194,15 +199,16 @@ def version(): def list_domains(): ''' - Return a cached list of all available Vagrant VMs on host. + Return a list of the salt_id names of all available Vagrant VMs on + this host without regard to the path where they are defined. CLI Example: .. code-block:: bash - salt '*' vagrant.list_domains + salt '*' vagrant.list_domains --log-level=info - The above shows information about all known Vagrant environments + The log shows information about all known Vagrant environments on this machine. This data is cached and may not be completely up-to-date. ''' @@ -226,7 +232,8 @@ def list_domains(): def list_active_vms(cwd=None): ''' - Return a list of names for active virtual machine on the minion + Return a list of machine names for active virtual machine on the minion, + which are defined in the Vagrantfile at the indicated path. CLI Example: @@ -248,7 +255,9 @@ def list_active_vms(cwd=None): def list_inactive_vms(cwd=None): ''' - Return a list of names for inactive virtual machine on the minion + Return a list of machine names for inactive virtual machine on the minion, + which are defined in the Vagrantfile at the indicated path. + CLI Example: @@ -270,7 +279,7 @@ def list_inactive_vms(cwd=None): def vm_state(name='', cwd=None): ''' - Return list of all the vms and their state. + Return list of information for all the vms indicating their state. If you pass a VM name in as an argument then it will return info for just the named VM, otherwise it will return all VMs defined by @@ -281,6 +290,17 @@ def vm_state(name='', cwd=None): .. code-block:: bash salt '*' vagrant.vm_state cwd=/projects/project_1 + + returns a list of dictionaries with machine name, state, provider, + and salt_id name. + + .. code-block:: python + + datum = {'machine': _, # Vagrant machine name, + 'state': _, # string indicating machine state, like 'running' + 'provider': _, # the Vagrant VM provider + 'name': _} # salt_id name + ''' if name: @@ -289,7 +309,8 @@ def vm_state(name='', cwd=None): cwd = vm_['cwd'] or cwd # usually ignore passed-in cwd else: if not cwd: - raise SaltInvocationError('Path to Vagranfile must be defined, but cwd=%s', cwd) + raise SaltInvocationError( + 'Path to Vagranfile must be defined, but cwd={}'.format(cwd)) machine = '' info = [] @@ -320,7 +341,20 @@ def init(name, # Salt_id for created VM vm=None, # a dictionary of VM configuration settings ): ''' - Initialize a new Vagrant vm + Initialize a new Vagrant VM. + + This inputs all the information needed to start a Vagrant VM. These settings are stored in + a Salt sdb database on the Vagrant host minion and used to start, control, and query the + guest VMs. The salt_id assigned here is the key field for that database and must be unique. + + :param name: The salt_id name you will use to control this VM + :param cwd: The path to the directory where the Vagrantfile is located + :param machine: The machine name in the Vagrantfile. If blank, the primary machine will be used. + :param runas: The username on the host who owns the Vagrant work files. + :param start: (default: False) Start the virtual machine now. + :param vagrant_provider: The name of a Vagrant VM provider (if not the default). + :param vm: Optionally, all the above information may be supplied in this dictionary. + :return: A string indicating success, or False. CLI Example: @@ -350,7 +384,7 @@ def init(name, # Salt_id for created VM def start(name): ''' - Start (vagrant up) a defined virtual machine by salt_id name. + Start (vagrant up) a virtual machine defined by salt_id name. The machine must have been previously defined using "vagrant.init". CLI Example: @@ -368,7 +402,7 @@ def _start(name, vm_): # internal call name, because "start" is a keyword argum try: machine = vm_['machine'] except KeyError: - raise SaltInvocationError('No Vagrant machine defined for Salt-id {}'.format(name)) + raise SaltInvocationError('No Vagrant machine defined for Salt_id {}'.format(name)) vagrant_provider = vm_.get('vagrant_provider', '') provider_ = '--provider={}'.format(vagrant_provider) if vagrant_provider else '' @@ -390,8 +424,11 @@ def _start(name, vm_): # internal call name, because "start" is a keyword argum def shutdown(name): ''' - Send a soft shutdown signal to the named vm. - ( for Vagrant, alternate name for vagrant.stop ) + Send a soft shutdown (vagrant halt) signal to the named vm. + + This does the same thing as vagrant.stop. Other VM control + modules use "stop" and "shutdown" to differentiate between + hard and soft shutdowns. CLI Example: @@ -404,8 +441,7 @@ def shutdown(name): def stop(name): ''' - Hard shutdown the virtual machine. - ( Vagrant will attempt a soft shutdown first. ) + Hard shutdown the virtual machine. (vagrant halt) CLI Example: @@ -425,7 +461,7 @@ def stop(name): def pause(name): ''' - Pause (suspend) the named vm + Pause (vagrant suspend) the named VM. CLI Example: @@ -445,7 +481,7 @@ def pause(name): def reboot(name): ''' - Reboot a VM + Reboot a VM. (vagrant reload) CLI Example: @@ -465,7 +501,9 @@ def reboot(name): def destroy(name): ''' - Destroy and delete a virtual machine. + Destroy and delete a virtual machine. (vagrant destroy -f) + + This also removes the salt_id name defined by vagrant.init. CLI Example: @@ -492,6 +530,11 @@ def get_ssh_config(name, network_mask='', get_private_key=False): r''' Retrieve hints of how you might connect to a Vagrant VM. + :param name: the salt_id of the machine + :param network_mask: a CIDR mask to search for the VM's address + :param get_private_key: (default: False) return the key used for ssh login + :return: a dict of ssh login information for the VM + CLI Example: .. code-block:: bash @@ -499,7 +542,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): salt vagrant.get_ssh_config salt my_laptop vagrant.get_ssh_config quail1 network_mask=10.0.0.0/8 get_private_key=True - returns a dictionary containing: + The returned dictionary contains: - key_filename: the name of the private key file on the VM host computer - ssh_username: the username to be used to log in to the VM @@ -511,15 +554,17 @@ def get_ssh_config(name, network_mask='', get_private_key=False): About `network_mask`: Vagrant usually uses a redirected TCP port on its host computer to log in to a VM using ssh. + This redirected port and its IP address are "ssh_port" and "ssh_host". The ssh_host is + usually the localhost (127.0.0.1). This makes it impossible for a third machine (such as a salt-cloud master) to contact the VM unless the VM has another network interface defined. You will usually want a bridged network defined by having a `config.vm.network "public_network"` statement in your `Vagrantfile`. The IP address of the bridged adapter will typically be assigned by DHCP and unknown to you, but you should be able to determine what IP network the address will be chosen from. - If you enter a CIDR network mask, the module will attempt to find the VM's address for you. - It will send an `ifconfig` command to the VM (using ssh to `ssh_host`:`ssh_port`) and scan the - result, returning the IP address of the first interface it can find which matches your mask. + If you enter a CIDR network mask, Salt will attempt to find the VM's address for you. + The host machine will send an "ifconfig" command to the VM (using ssh to `ssh_host`:`ssh_port`) + and return the IP address of the first interface it can find which matches your mask. ''' vm_ = get_vm_info(name) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 1099aa2167..85421f887a 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -20,6 +20,7 @@ from salt.ext import six TEMP_DATABASE_FILE = '/tmp/test_vagrant.sqlite' + @skipIf(NO_MOCK, NO_MOCK_REASON) class VagrantTestCase(TestCase, LoaderModuleMockMixin): ''' From 7192332758fd45d472e90893620063aecdf8e036 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 27 Sep 2017 16:12:05 -0600 Subject: [PATCH 348/633] Fix `unit.modules.test_virt` for Windows Use os agnostic paths Get system root from salt config --- tests/unit/modules/test_virt.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index f616bb9391..43f90237aa 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -3,6 +3,7 @@ # Import python libs from __future__ import absolute_import import re +import os # Import Salt Testing libs from tests.support.mixins import LoaderModuleMockMixin @@ -13,6 +14,7 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch import salt.modules.virt as virt import salt.modules.config as config from salt._compat import ElementTree as ET +import salt.config import salt.utils # Import third party libs @@ -245,8 +247,9 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): disks = root.findall('.//disk') self.assertEqual(len(disks), 1) disk = disks[0] - self.assertTrue(disk.find('source').attrib['file'].startswith('/')) - self.assertTrue('hello/system' in disk.find('source').attrib['file']) + root_dir = salt.config.DEFAULT_MINION_OPTS.get('root_dir') + self.assertTrue(disk.find('source').attrib['file'].startswith(root_dir)) + self.assertTrue(os.path.join('hello', 'system') in disk.find('source').attrib['file']) self.assertEqual(disk.find('target').attrib['dev'], 'vda') self.assertEqual(disk.find('target').attrib['bus'], 'virtio') self.assertEqual(disk.find('driver').attrib['name'], 'qemu') @@ -284,7 +287,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(len(disks), 1) disk = disks[0] self.assertTrue('[0]' in disk.find('source').attrib['file']) - self.assertTrue('hello/system' in disk.find('source').attrib['file']) + self.assertTrue(os.path.join('hello', 'system') in disk.find('source').attrib['file']) self.assertEqual(disk.find('target').attrib['dev'], 'sda') self.assertEqual(disk.find('target').attrib['bus'], 'scsi') self.assertEqual(disk.find('address').attrib['unit'], '0') From 058e50e5302d5ee5925b9f7ea0527f421e312442 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 27 Sep 2017 16:37:12 -0600 Subject: [PATCH 349/633] Fix `unit.modules.test_win_service` Mock `cmd.run` --- tests/unit/modules/test_win_service.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/modules/test_win_service.py b/tests/unit/modules/test_win_service.py index 54c5c465bd..ae89c5ecd2 100644 --- a/tests/unit/modules/test_win_service.py +++ b/tests/unit/modules/test_win_service.py @@ -143,14 +143,14 @@ class WinServiceTestCase(TestCase, LoaderModuleMockMixin): {'Status': 'Stop Pending'}, {'Status': 'Stopped'}]) - with patch.object(win_service, 'status', mock_false): + with patch.dict(win_service.__salt__, {'cmd.run': MagicMock(return_value="service was stopped")}): self.assertTrue(win_service.stop('spongebob')) - with patch.object(win_service, 'status', mock_true): - with patch.object(win32serviceutil, 'StopService', mock_true): - with patch.object(win_service, 'info', mock_info): - with patch.object(win_service, 'status', mock_false): - self.assertTrue(win_service.stop('spongebob')) + with patch.dict(win_service.__salt__, {'cmd.run': MagicMock(return_value="service was stopped")}), \ + patch.object(win32serviceutil, 'StopService', mock_true), \ + patch.object(win_service, 'info', mock_info), \ + patch.object(win_service, 'status', mock_false): + self.assertTrue(win_service.stop('spongebob')) def test_restart(self): ''' From 228e74c8e373949e20ee7fedcb193d79e717fd90 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 27 Sep 2017 16:54:06 -0600 Subject: [PATCH 350/633] Fix `unit.modules.test_znc` for Windows Mock the signal object as it's missing SIGUSR1 and SIGHUP on Windows --- tests/unit/modules/test_znc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/test_znc.py b/tests/unit/modules/test_znc.py index d1e30179d5..8e781ece2a 100644 --- a/tests/unit/modules/test_znc.py +++ b/tests/unit/modules/test_znc.py @@ -54,7 +54,8 @@ class ZncTestCase(TestCase, LoaderModuleMockMixin): Tests write the active configuration state to config file ''' mock = MagicMock(return_value='SALT') - with patch.dict(znc.__salt__, {'ps.pkill': mock}): + with patch.dict(znc.__salt__, {'ps.pkill': mock}), \ + patch.object(znc, 'signal', MagicMock()): self.assertEqual(znc.dumpconf(), 'SALT') # 'rehashconf' function tests: 1 @@ -64,7 +65,8 @@ class ZncTestCase(TestCase, LoaderModuleMockMixin): Tests rehash the active configuration state from config file ''' mock = MagicMock(return_value='SALT') - with patch.dict(znc.__salt__, {'ps.pkill': mock}): + with patch.dict(znc.__salt__, {'ps.pkill': mock}), \ + patch.object(znc, 'signal', MagicMock()): self.assertEqual(znc.rehashconf(), 'SALT') # 'version' function tests: 1 From aafec7ab0e761f09058edf6ac3bf87a3fcd87a21 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 27 Sep 2017 16:59:36 -0600 Subject: [PATCH 351/633] Fix `unit.modules.test_zypper` for Windows Use `os.linesep` for newline in expected return --- tests/unit/modules/test_zypper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/test_zypper.py b/tests/unit/modules/test_zypper.py index f3403e6e1c..ac8621307a 100644 --- a/tests/unit/modules/test_zypper.py +++ b/tests/unit/modules/test_zypper.py @@ -189,7 +189,7 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): } with patch.dict('salt.modules.zypper.__salt__', {'cmd.run_all': MagicMock(return_value=ref_out)}): with self.assertRaisesRegex(CommandExecutionError, - "^Zypper command failure: Some handled zypper internal error\nAnother zypper internal error$"): + "^Zypper command failure: Some handled zypper internal error{0}Another zypper internal error$".format(os.linesep)): zypper.list_upgrades(refresh=False) # Test unhandled error From cfda0d54e21cb3e3e11993ad909acaf4d5af04b0 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Wed, 27 Sep 2017 17:07:29 -0600 Subject: [PATCH 352/633] test inside sandbox, fix two lines of docs --- salt/modules/vagrant.py | 4 ++-- tests/unit/modules/test_vagrant.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 5b2d7b80d3..a74dddf493 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -232,7 +232,7 @@ def list_domains(): def list_active_vms(cwd=None): ''' - Return a list of machine names for active virtual machine on the minion, + Return a list of machine names for active virtual machine on the host, which are defined in the Vagrantfile at the indicated path. CLI Example: @@ -255,7 +255,7 @@ def list_active_vms(cwd=None): def list_inactive_vms(cwd=None): ''' - Return a list of machine names for inactive virtual machine on the minion, + Return a list of machine names for inactive virtual machine on the host, which are defined in the Vagrantfile at the indicated path. diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 85421f887a..7cad1b1f5f 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -18,7 +18,7 @@ import salt.exceptions # Import third party libs from salt.ext import six -TEMP_DATABASE_FILE = '/tmp/test_vagrant.sqlite' +TEMP_DATABASE_FILE = '/tmp/salt-tests-tmpdir/test_vagrant.sqlite' @skipIf(NO_MOCK, NO_MOCK_REASON) From d59cc13ca4f62b0c34c642cb9b1bc3d4dc347a74 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Thu, 28 Sep 2017 00:59:20 -0600 Subject: [PATCH 353/633] try to pass unit tests on jenkins --- salt/modules/vagrant.py | 17 +++++++++-------- tests/unit/modules/test_vagrant.py | 6 ------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index a74dddf493..e1f2ef0a5e 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -35,6 +35,7 @@ import os # Import salt libs import salt.utils +import salt.utils.sdb from salt.exceptions import CommandExecutionError, SaltInvocationError import salt.ext.six as six @@ -84,11 +85,11 @@ def _build_machine_uri(machine, cwd): def _update_vm_info(name, vm_): ''' store the vm_ information keyed by name ''' - __utils__['sdb.sdb_set'](_build_sdb_uri(name), vm_, __opts__) + salt.utils.sdb.sdb_set(_build_sdb_uri(name), vm_, __opts__) # store machine-to-name mapping, too if vm_['machine']: - __utils__['sdb.sdb_set']( + salt.utils.sdb.sdb_set( _build_machine_uri(vm_['machine'], vm_.get('cwd', '.')), name, __opts__) @@ -102,7 +103,7 @@ def get_vm_info(name): :return: dictionary of {'machine': x, 'cwd': y, ...}. ''' try: - vm_ = __utils__['sdb.sdb_get'](_build_sdb_uri(name), __opts__) + vm_ = salt.utils.sdb.sdb_get(_build_sdb_uri(name), __opts__) except KeyError: raise SaltInvocationError( 'Probable sdb driver not found. Check your configuration.') @@ -120,7 +121,7 @@ def get_machine_id(machine, cwd): :param cwd: the path to Vagrantfile :return: salt_id name ''' - name = __utils__['sdb.sdb_get'](_build_machine_uri(machine, cwd)) + name = salt.utils.sdb.sdb_get(_build_machine_uri(machine, cwd)) if name is None: raise SaltInvocationError( 'No Salt name found for Vagrant machine {} in {}'.format( @@ -142,20 +143,20 @@ def _erase_vm_info(name): if vm_['machine']: key = _build_machine_uri(vm_['machine'], vm_.get('cwd', '.')) try: - __utils__['sdb.sdb_delete'](key, __opts__) + salt.utils.sdb.sdb_delete(key, __opts__) except KeyError: # no delete method found -- load a blank value - __utils__['sdb.sdb_set'](key, '', __opts__) + salt.utils.sdb.sdb_set(key, '', __opts__) except Exception: pass uri = _build_sdb_uri(name) try: # delete the name record - __utils__['sdb.sdb_delete'](uri, __opts__) + salt.utils.sdb.sdb_delete(uri, __opts__) except KeyError: # no delete method found -- load an empty dictionary - __utils__['sdb.sdb_set'](uri, {}, __opts__) + salt.utils.sdb.sdb_set(uri, {}, __opts__) except Exception: pass diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 7cad1b1f5f..c6ba35c23a 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -12,7 +12,6 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON # Import salt libs import salt.modules.vagrant as vagrant import salt.modules.cmdmod as cmd -import salt.utils.sdb as sdb import salt.exceptions # Import third party libs @@ -45,11 +44,6 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): 'cmd.shell': cmd.shell, 'cmd.retcode': cmd.retcode, }, - '__utils__': { - 'sdb.sdb_set': sdb.sdb_set, - 'sdb.sdb_get': sdb.sdb_get, - 'sdb.sdb_delete': sdb.sdb_delete - } } return {vagrant: vagrant_globals} From 19acf9b1496828fc4f5860cd090c1fa233a9d7b7 Mon Sep 17 00:00:00 2001 From: 3add3287 <3add3287@users.noreply.github.com> Date: Thu, 28 Sep 2017 09:12:24 +0200 Subject: [PATCH 354/633] Properly merge pillar data obtained from multiple nodegroups for cases where the minion belongs to more than one Fixes #43788 --- salt/pillar/file_tree.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/pillar/file_tree.py b/salt/pillar/file_tree.py index 323958e2f9..2af4560c49 100644 --- a/salt/pillar/file_tree.py +++ b/salt/pillar/file_tree.py @@ -343,14 +343,15 @@ def ext_pillar(minion_id, if minion_id in match: ngroup_dir = os.path.join( nodegroups_dir, str(nodegroup)) - ngroup_pillar.update( + ngroup_pillar = salt.utils.dictupdate.merge(ngroup_pillar, _construct_pillar(ngroup_dir, follow_dir_links, keep_newline, render_default, renderer_blacklist, renderer_whitelist, - template) + template), + strategy='recurse' ) else: if debug is True: From 6bd5c236459363afbf41de4b46d2eecb38fdd82e Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 21 Sep 2017 16:42:25 -0400 Subject: [PATCH 355/633] Added sspi mechanism support and __pillar__ and config merging to salt.proxy.esxi --- salt/proxy/esxi.py | 172 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 133 insertions(+), 39 deletions(-) diff --git a/salt/proxy/esxi.py b/salt/proxy/esxi.py index 4edd50ac31..f358a710da 100644 --- a/salt/proxy/esxi.py +++ b/salt/proxy/esxi.py @@ -273,13 +273,22 @@ for standing up an ESXi host from scratch. # Import Python Libs from __future__ import absolute_import import logging +import os # Import Salt Libs from salt.exceptions import SaltSystemExit +from salt.config.schemas.esxi import EsxiProxySchema +from salt.utils.dictupdate import merge # This must be present or the Salt loader won't load this module. __proxyenabled__ = ['esxi'] +# External libraries +try: + import jsonschema + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False # Variables are scoped to this module so we can have persistent data # across calls to fns in here. @@ -288,53 +297,122 @@ DETAILS = {} # Set up logging log = logging.getLogger(__file__) - # Define the module's virtual name __virtualname__ = 'esxi' - def __virtual__(): ''' Only load if the ESXi execution module is available. ''' - if 'vsphere.system_info' in __salt__: + if HAS_JSONSCHEMA: return __virtualname__ return False, 'The ESXi Proxy Minion module did not load.' - def init(opts): ''' This function gets called when the proxy starts up. For ESXi devices, the host, login credentials, and, if configured, the protocol and port are cached. ''' - if 'host' not in opts['proxy']: - log.critical('No \'host\' key found in pillar for this proxy.') - return False - if 'username' not in opts['proxy']: - log.critical('No \'username\' key found in pillar for this proxy.') - return False - if 'passwords' not in opts['proxy']: - log.critical('No \'passwords\' key found in pillar for this proxy.') - return False - - host = opts['proxy']['host'] - - # Get the correct login details + log.debug('Initting esxi proxy module in process \'{}\'' + ''.format(os.getpid())) + log.debug('Validating esxi proxy input') + schema = EsxiProxySchema.serialize() + log.trace('esxi_proxy_schema = {}'.format(schema)) + proxy_conf = merge(opts.get('proxy', {}), __pillar__.get('proxy', {})) + log.trace('proxy_conf = {0}'.format(proxy_conf)) try: - username, password = find_credentials(host) - except SaltSystemExit as err: - log.critical('Error: {0}'.format(err)) - return False + jsonschema.validate(proxy_conf, schema) + except jsonschema.exceptions.ValidationError as exc: + raise excs.InvalidProxyInputError(exc) - # Set configuration details - DETAILS['host'] = host - DETAILS['username'] = username - DETAILS['password'] = password - DETAILS['protocol'] = opts['proxy'].get('protocol', 'https') - DETAILS['port'] = opts['proxy'].get('port', '443') - DETAILS['credstore'] = opts['proxy'].get('credstore') + DETAILS['proxytype'] = proxy_conf['proxytype'] + if ('host' not in proxy_conf) and ('vcenter' not in proxy_conf): + log.critical('Neither \'host\' nor \'vcenter\' keys found in pillar ' + 'for this proxy.') + return False + if 'host' in proxy_conf: + # We have started the proxy by connecting directly to the host + if 'username' not in proxy_conf: + log.critical('No \'username\' key found in pillar for this proxy.') + return False + if 'passwords' not in proxy_conf: + log.critical('No \'passwords\' key found in pillar for this proxy.') + return False + host = proxy_conf['host'] + + # Get the correct login details + try: + username, password = find_credentials(host) + except excs.SaltSystemExit as err: + log.critical('Error: {0}'.format(err)) + return False + + # Set configuration details + DETAILS['host'] = host + DETAILS['username'] = username + DETAILS['password'] = password + DETAILS['protocol'] = proxy_conf.get('protocol') + DETAILS['port'] = proxy_conf.get('port') + return True + + if 'vcenter' in proxy_conf: + vcenter = proxy_conf['vcenter'] + if not proxy_conf.get('esxi_host'): + log.critical('No \'esxi_host\' key found in pillar for this proxy.') + DETAILS['esxi_host'] = proxy_conf['esxi_host'] + # We have started the proxy by connecting via the vCenter + if 'mechanism' not in proxy_conf: + log.critical('No \'mechanism\' key found in pillar for this proxy.') + return False + mechanism = proxy_conf['mechanism'] + # Save mandatory fields in cache + for key in ('vcenter', 'mechanism'): + DETAILS[key] = proxy_conf[key] + + if mechanism == 'userpass': + if 'username' not in proxy_conf: + log.critical('No \'username\' key found in pillar for this ' + 'proxy.') + return False + if not 'passwords' in proxy_conf and \ + len(proxy_conf['passwords']) > 0: + + log.critical('Mechanism is set to \'userpass\' , but no ' + '\'passwords\' key found in pillar for this ' + 'proxy.') + return False + for key in ('username', 'passwords'): + DETAILS[key] = proxy_conf[key] + elif mechanism == 'sspi': + if not 'domain' in proxy_conf: + log.critical('Mechanism is set to \'sspi\' , but no ' + '\'domain\' key found in pillar for this proxy.') + return False + if not 'principal' in proxy_conf: + log.critical('Mechanism is set to \'sspi\' , but no ' + '\'principal\' key found in pillar for this ' + 'proxy.') + return False + for key in ('domain', 'principal'): + DETAILS[key] = proxy_conf[key] + + if mechanism == 'userpass': + # Get the correct login details + log.debug('Retrieving credentials and testing vCenter connection' + ' for mehchanism \'userpass\'') + try: + username, password = find_credentials() + DETAILS['password'] = password + except excs.SaltSystemExit as err: + log.critical('Error: {0}'.format(err)) + return False + + # Save optional + DETAILS['protocol'] = proxy_conf.get('protocol', 'https') + DETAILS['port'] = proxy_conf.get('port', '443') + DETAILS['credstore'] = proxy_conf.get('credstore') def grains(): @@ -358,8 +436,9 @@ def grains_refresh(): def ping(): ''' - Check to see if the host is responding. Returns False if the host didn't - respond, True otherwise. + Returns True if connection is to be done via a vCenter (no connection is attempted). + Check to see if the host is responding when connecting directly via an ESXi + host. CLI Example: @@ -367,15 +446,19 @@ def ping(): salt esxi-host test.ping ''' - # find_credentials(DETAILS['host']) - try: - __salt__['vsphere.system_info'](host=DETAILS['host'], - username=DETAILS['username'], - password=DETAILS['password']) - except SaltSystemExit as err: - log.warning(err) - return False - + if DETAILS.get('esxi_host'): + return True + else: + # TODO Check connection if mechanism is SSPI + if DETAILS['mechanism'] == 'userpass': + find_credentials(DETAILS['host']) + try: + __salt__['vsphere.system_info'](host=DETAILS['host'], + username=DETAILS['username'], + password=DETAILS['password']) + except excs.SaltSystemExit as err: + log.warning(err) + return False return True @@ -461,3 +544,14 @@ def _grains(host, protocol=None, port=None): port=port) GRAINS_CACHE.update(ret) return GRAINS_CACHE + + +def is_connected_via_vcenter(): + return True if 'vcenter' in DETAILS else False + + +def get_details(): + ''' + Return the proxy details + ''' + return DETAILS From 3369a3def79e41f0abcf84ba387ae319fdc279cb Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 21 Sep 2017 16:48:15 -0400 Subject: [PATCH 356/633] Added the EsxiProxySchema JSON schema --- salt/config/schemas/esxi.py | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 salt/config/schemas/esxi.py diff --git a/salt/config/schemas/esxi.py b/salt/config/schemas/esxi.py new file mode 100644 index 0000000000..affd14be59 --- /dev/null +++ b/salt/config/schemas/esxi.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu (alexandru.bleotu@morganstanley.com)` + + + salt.config.schemas.esxi + ~~~~~~~~~~~~~~~~~~~~~~~~ + + ESXi host configuration schemas +''' + +# Import Python libs +from __future__ import absolute_import + +# Import Salt libs +from salt.utils.schema import (Schema, + ArrayItem, + IntegerItem, + StringItem) + + +class EsxiProxySchema(Schema): + ''' + Schema of the esxi proxy input + ''' + + title = 'Esxi Proxy Schema' + description = 'Esxi proxy schema' + additional_properties = False + proxytype = StringItem(required=True, + enum=['esxi']) + host = StringItem(pattern=r'[^\s]+') # Used when connecting directly + vcenter = StringItem(pattern=r'[^\s]+') # Used when connecting via a vCenter + esxi_host = StringItem() + username = StringItem() + passwords = ArrayItem(min_items=1, + items=StringItem(), + unique_items=True) + mechanism = StringItem(enum=['userpass', 'sspi']) + # TODO Should be changed when anyOf is supported for schemas + domain = StringItem() + principal = StringItem() + protocol = StringItem() + port = IntegerItem(minimum=1) From 434d88b9a4cf5053b2f8ecdf819eba911d7e025d Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 21 Sep 2017 16:52:03 -0400 Subject: [PATCH 357/633] Added salt.modules.esxi.get_details that returns the proxy details --- salt/modules/esxi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/salt/modules/esxi.py b/salt/modules/esxi.py index a4c1f8ddcc..ee1f981022 100644 --- a/salt/modules/esxi.py +++ b/salt/modules/esxi.py @@ -56,3 +56,7 @@ def cmd(command, *args, **kwargs): proxy_cmd = proxy_prefix + '.ch_config' return __proxy__[proxy_cmd](command, *args, **kwargs) + + +def get_details(): + return __proxy__['esxi.get_details']() From 5c795129048a5400cecba82253eddcb05d921863 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 21 Sep 2017 17:06:36 -0400 Subject: [PATCH 358/633] Added salt.modules.vsphere._get_esxi_proxy_details --- salt/modules/vsphere.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 0c92385804..87088cfb3b 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -6495,3 +6495,19 @@ def _get_esxcluster_proxy_details(): det.get('protocol'), det.get('port'), det.get('mechanism'), \ det.get('principal'), det.get('domain'), det.get('datacenter'), \ det.get('cluster') + + +def _get_esxi_proxy_details(): + ''' + Returns the running esxi's proxy details + ''' + det = __proxy__['esxi.get_details']() + host = det.get('host') + if det.get('vcenter'): + host = det['vcenter'] + esxi_hosts = None + if det.get('esxi_host'): + esxi_hosts = [det['esxi_host']] + return host, det.get('username'), det.get('password'), \ + det.get('protocol'), det.get('port'), det.get('mechanism'), \ + det.get('principal'), det.get('domain'), esxi_hosts From ffbab2ce896d29103804aac624d87df49db325b9 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 07:56:27 -0400 Subject: [PATCH 359/633] Added esxi proxy support and retrieval of esxi reference in salt.modules.vsphere._get_proxy_target --- salt/modules/vsphere.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 87088cfb3b..efde0b6d07 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -6441,7 +6441,7 @@ def add_host_to_dvs(host, username, password, vmknic_name, vmnic_name, @depends(HAS_PYVMOMI) -@supports_proxies('esxcluster', 'esxdatacenter', 'vcenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter', 'vcenter') def _get_proxy_target(service_instance): ''' Returns the target object of a proxy. @@ -6472,6 +6472,18 @@ def _get_proxy_target(service_instance): elif proxy_type == 'vcenter': # vcenter proxy - the target is the root folder reference = salt.utils.vmware.get_root_folder(service_instance) + elif proxy_type == 'esxi': + # esxi proxy + details = __proxy__['esxi.get_details']() + if 'vcenter' not in details: + raise InvalidEntityError('Proxies connected directly to ESXi ' + 'hosts are not supported') + references = salt.utils.vmware.get_hosts( + service_instance, host_names=details['esxi_host']) + if not references: + raise VMwareObjectRetrievalError( + 'ESXi host \'{0}\' was not found'.format(details['esxi_host'])) + reference = references[0] log.trace('reference = {0}'.format(reference)) return reference From 230c17e7043f99c5946dc2bb7b7ce5f6e56f90dd Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sat, 23 Sep 2017 07:33:57 -0400 Subject: [PATCH 360/633] Added salt.utils.vsan.get_vsan_disk_management_system --- salt/utils/vsan.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/salt/utils/vsan.py b/salt/utils/vsan.py index 8ad713cd3e..a411d4ec97 100644 --- a/salt/utils/vsan.py +++ b/salt/utils/vsan.py @@ -129,6 +129,30 @@ def get_vsan_cluster_config_system(service_instance): return vc_mos['vsan-cluster-config-system'] +def get_vsan_disk_management_system(service_instance): + ''' + Returns a vim.VimClusterVsanVcDiskManagementSystem object + + service_instance + Service instance to the host or vCenter + ''' + + #TODO Replace when better connection mechanism is available + + #For python 2.7.9 and later, the defaul SSL conext has more strict + #connection handshaking rule. We may need turn of the hostname checking + #and client side cert verification + context = None + if sys.version_info[:3] > (2, 7, 8): + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + stub = service_instance._stub + vc_mos = vsanapiutils.GetVsanVcMos(stub, context=context) + return vc_mos['vsan-disk-management-system'] + + def get_cluster_vsan_info(cluster_ref): ''' Returns the extended cluster vsan configuration object From 67afc2f84110cc14ab71cc71ac6fb37be2da4fb2 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sat, 23 Sep 2017 07:36:10 -0400 Subject: [PATCH 361/633] Added salt.utils.vsan.get_host_vsan_system --- salt/utils/vsan.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/salt/utils/vsan.py b/salt/utils/vsan.py index a411d4ec97..c79b95155a 100644 --- a/salt/utils/vsan.py +++ b/salt/utils/vsan.py @@ -153,6 +153,35 @@ def get_vsan_disk_management_system(service_instance): return vc_mos['vsan-disk-management-system'] +def get_host_vsan_system(service_instance, host_ref, hostname=None): + ''' + Returns a host's vsan system + + service_instance + Service instance to the host or vCenter + + host_ref + Refernce to ESXi host + + hostname + Name of ESXi host. Default value is None. + ''' + if not hostname: + hostname = salt.utils.vmware.get_managed_object_name(host_ref) + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='configManager.vsanSystem', + type=vim.HostSystem, + skip=False) + objs = salt.utils.vmware.get_mors_with_properties( + service_instance, vim.HostVsanSystem, property_list=['config.enabled'], + container_ref=host_ref, traversal_spec=traversal_spec) + if not objs: + raise VMwareObjectRetrievalError('Host\'s \'{0}\' VSAN system was ' + 'not retrieved'.format(hostname)) + log.trace('[{0}] Retrieved VSAN system'.format(hostname)) + return objs[0]['object'] + + def get_cluster_vsan_info(cluster_ref): ''' Returns the extended cluster vsan configuration object From 6a31c437dfaf681c170cffbf8f972dad6881d0a6 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sat, 23 Sep 2017 07:37:45 -0400 Subject: [PATCH 362/633] Added salt.utils.vsan.create_diskgroup --- salt/utils/vsan.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/salt/utils/vsan.py b/salt/utils/vsan.py index c79b95155a..20b6f954d4 100644 --- a/salt/utils/vsan.py +++ b/salt/utils/vsan.py @@ -182,6 +182,63 @@ def get_host_vsan_system(service_instance, host_ref, hostname=None): return objs[0]['object'] +def create_diskgroup(service_instance, vsan_disk_mgmt_system, + host_ref, cache_disk, capacity_disks): + ''' + Creates a disk group + + service_instance + Service instance to the host or vCenter + + vsan_disk_mgmt_system + vim.VimClusterVsanVcDiskManagemenetSystem representing the vSan disk + management system retrieved from the vsan endpoint. + + host_ref + vim.HostSystem object representing the target host the disk group will + be created on + + cache_disk + The vim.HostScsidisk to be used as a cache disk. It must be an ssd disk. + + capacity_disks + List of vim.HostScsiDisk objects representing of disks to be used as + capacity disks. Can be either ssd or non-ssd. There must be a minimum + of 1 capacity disk in the list. + ''' + hostname = salt.utils.vmware.get_managed_object_name(host_ref) + cache_disk_id = cache_disk.canonicalName + log.debug('Creating a new disk group with cache disk \'{0}\' on host ' + '\'{1}\''.format(cache_disk_id, hostname)) + log.trace('capacity_disk_ids = {0}'.format([c.canonicalName for c in + capacity_disks])) + spec = vim.VimVsanHostDiskMappingCreationSpec() + spec.cacheDisks = [cache_disk] + spec.capacityDisks = capacity_disks + # All capacity disks must be either ssd or non-ssd (mixed disks are not + # supported) + spec.creationType = 'allFlash' if getattr(capacity_disks[0], 'ssd') \ + else 'hybrid' + spec.host = host_ref + try: + task = vsan_disk_mgmt_system.InitializeDiskMappings(spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.fault.MethodNotFound as exc: + log.exception(exc) + raise VMwareRuntimeError('Method \'{0}\' not found'.format(exc.method)) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + _wait_for_tasks([task], service_instance) + return True + + def get_cluster_vsan_info(cluster_ref): ''' Returns the extended cluster vsan configuration object From d637b074b9a2b1f9f1cac0c0edd43e00845ca9ef Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 09:03:41 -0400 Subject: [PATCH 363/633] Added salt.utils.vsan.add_capacity_to_diskgroup --- salt/utils/vsan.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/salt/utils/vsan.py b/salt/utils/vsan.py index 20b6f954d4..a9dfe3775c 100644 --- a/salt/utils/vsan.py +++ b/salt/utils/vsan.py @@ -239,6 +239,65 @@ def create_diskgroup(service_instance, vsan_disk_mgmt_system, return True +def add_capacity_to_diskgroup(service_instance, vsan_disk_mgmt_system, + host_ref, diskgroup, new_capacity_disks): + ''' + Adds capacity disk(s) to a disk group. + + service_instance + Service instance to the host or vCenter + + vsan_disk_mgmt_system + vim.VimClusterVsanVcDiskManagemenetSystem representing the vSan disk + management system retrieved from the vsan endpoint. + + host_ref + vim.HostSystem object representing the target host the disk group will + be created on + + diskgroup + The vsan.HostDiskMapping object representing the host's diskgroup where + the additional capacity needs to be added + + new_capacity_disks + List of vim.HostScsiDisk objects representing the disks to be added as + capacity disks. Can be either ssd or non-ssd. There must be a minimum + of 1 new capacity disk in the list. + ''' + hostname = salt.utils.vmware.get_managed_object_name(host_ref) + cache_disk = diskgroup.ssd + cache_disk_id = cache_disk.canonicalName + log.debug('Adding capacity to disk group with cache disk \'{0}\' on host ' + '\'{1}\''.format(cache_disk_id, hostname)) + log.trace('new_capacity_disk_ids = {0}'.format([c.canonicalName for c in + new_capacity_disks])) + spec = vim.VimVsanHostDiskMappingCreationSpec() + spec.cacheDisks = [cache_disk] + spec.capacityDisks = new_capacity_disks + # All new capacity disks must be either ssd or non-ssd (mixed disks are not + # supported); also they need to match the type of the existing capacity + # disks; we assume disks are already validated + spec.creationType = 'allFlash' if getattr(new_capacity_disks[0], 'ssd') \ + else 'hybrid' + spec.host = host_ref + try: + task = vsan_disk_mgmt_system.InitializeDiskMappings(spec) + except fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.fault.MethodNotFound as exc: + log.exception(exc) + raise VMwareRuntimeError('Method \'{0}\' not found'.format(exc.method)) + except vmodl.RuntimeFault as exc: + raise VMwareRuntimeError(exc.msg) + _wait_for_tasks([task], service_instance) + return True + + def get_cluster_vsan_info(cluster_ref): ''' Returns the extended cluster vsan configuration object From d8a2724c428885248c6b4c4a10a0a929599bfa06 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 09:17:50 -0400 Subject: [PATCH 364/633] Added salt.utils.vsan.remove_capacity_from_diskgroup --- salt/utils/vsan.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/salt/utils/vsan.py b/salt/utils/vsan.py index a9dfe3775c..5e6093dfff 100644 --- a/salt/utils/vsan.py +++ b/salt/utils/vsan.py @@ -298,6 +298,78 @@ def add_capacity_to_diskgroup(service_instance, vsan_disk_mgmt_system, return True +def remove_capacity_from_diskgroup(service_instance, host_ref, diskgroup, + capacity_disks, data_evacuation=True, + hostname=None, + host_vsan_system=None): + ''' + Removes capacity disk(s) from a disk group. + + service_instance + Service instance to the host or vCenter + + host_vsan_system + ESXi host's VSAN system + + host_ref + Reference to the ESXi host + + diskgroup + The vsan.HostDiskMapping object representing the host's diskgroup from + where the capacity needs to be removed + + capacity_disks + List of vim.HostScsiDisk objects representing the capacity disks to be + removed. Can be either ssd or non-ssd. There must be a minimum + of 1 capacity disk in the list. + + data_evacuation + Specifies whether to gracefully evacuate the data on the capacity disks + before removing them from the disk group. Default value is True. + + hostname + Name of ESXi host. Default value is None. + + host_vsan_system + ESXi host's VSAN system. Default value is None. + ''' + if not hostname: + hostname = salt.utils.vmware.get_managed_object_name(host_ref) + cache_disk = diskgroup.ssd + cache_disk_id = cache_disk.canonicalName + log.debug('Removing capacity from disk group with cache disk \'{0}\' on ' + 'host \'{1}\''.format(cache_disk_id, hostname)) + log.trace('capacity_disk_ids = {0}'.format([c.canonicalName for c in + capacity_disks])) + if not host_vsan_system: + host_vsan_system = get_host_vsan_system(service_instance, + host_ref, hostname) + # Set to evacuate all data before removing the disks + maint_spec = vim.HostMaintenanceSpec() + maint_spec.vsanMode = vim.VsanHostDecommissionMode() + if data_evacuation: + maint_spec.vsanMode.objectAction = \ + vim.VsanHostDecommissionModeObjectAction.evacuateAllData + else: + maint_spec.vsanMode.objectAction = \ + vim.VsanHostDecommissionModeObjectAction.noAction + try: + task = host_vsan_system.RemoveDisk_Task(disk=capacity_disks, + maintenanceSpec=maint_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + salt.utils.vmware.wait_for_task(task, hostname, 'remove_capacity') + return True + + def get_cluster_vsan_info(cluster_ref): ''' Returns the extended cluster vsan configuration object From a8406fb3b2f8695f7c4f638753bf125d9161f89f Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sat, 23 Sep 2017 07:42:09 -0400 Subject: [PATCH 365/633] Added salt.utils.vsan.remove_diskgroup --- salt/utils/vsan.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/salt/utils/vsan.py b/salt/utils/vsan.py index 5e6093dfff..b2ec11f80d 100644 --- a/salt/utils/vsan.py +++ b/salt/utils/vsan.py @@ -370,6 +370,67 @@ def remove_capacity_from_diskgroup(service_instance, host_ref, diskgroup, return True +def remove_diskgroup(service_instance, host_ref, diskgroup, hostname=None, + host_vsan_system=None, erase_disk_partitions=False, + data_accessibility=True): + ''' + Removes a disk group. + + service_instance + Service instance to the host or vCenter + + host_ref + Reference to the ESXi host + + diskgroup + The vsan.HostDiskMapping object representing the host's diskgroup from + where the capacity needs to be removed + + hostname + Name of ESXi host. Default value is None. + + host_vsan_system + ESXi host's VSAN system. Default value is None. + + data_accessibility + Specifies whether to ensure data accessibility. Default value is True. + ''' + if not hostname: + hostname = salt.utils.vmware.get_managed_object_name(host_ref) + cache_disk_id = diskgroup.ssd.canonicalName + log.debug('Removing disk group with cache disk \'{0}\' on ' + 'host \'{1}\''.format(cache_disk_id, hostname)) + if not host_vsan_system: + host_vsan_system = get_host_vsan_system( + service_instance, host_ref, hostname) + # Set to evacuate all data before removing the disks + maint_spec = vim.HostMaintenanceSpec() + maint_spec.vsanMode = vim.VsanHostDecommissionMode() + object_action = vim.VsanHostDecommissionModeObjectAction + if data_accessibility: + maint_spec.vsanMode.objectAction = \ + object_action.ensureObjectAccessibility + else: + maint_spec.vsanMode.objectAction = object_action.noAction + try: + task = host_vsan_system.RemoveDiskMapping_Task( + mapping=[diskgroup], maintenanceSpec=maint_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + salt.utils.vmware.wait_for_task(task, hostname, 'remove_diskgroup') + log.debug('Removed disk group with cache disk \'{0}\' ' + 'on host \'{1}\''.format(cache_disk_id, hostname)) + return True + + def get_cluster_vsan_info(cluster_ref): ''' Returns the extended cluster vsan configuration object From 273afc10159866676492a343c004433d5f4a2f37 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 09:51:13 -0400 Subject: [PATCH 366/633] Added salt.exceptions.VMwareObjectNotFoundError --- salt/exceptions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/salt/exceptions.py b/salt/exceptions.py index 1a253dff04..db93362c0f 100644 --- a/salt/exceptions.py +++ b/salt/exceptions.py @@ -442,6 +442,12 @@ class VMwareObjectRetrievalError(VMwareSaltError): ''' +class VMwareObjectNotFoundError(VMwareSaltError): + ''' + Used when a VMware object was not found + ''' + + class VMwareApiError(VMwareSaltError): ''' Used when representing a generic VMware API error From 4d6eb4197a99625bf3d917d31dcfc0923acba82d Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 09:46:58 -0400 Subject: [PATCH 367/633] Added salt.utils.vmware._get_partition_info --- salt/utils/vmware.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index b0552996e3..d3d6b33649 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2048,6 +2048,30 @@ def get_storage_system(service_instance, host_ref, hostname=None): return objs[0]['object'] +def _get_partition_info(storage_system, device_path): + ''' + Returns partition informations for a device path, of type + vim.HostDiskPartitionInfo + ''' + try: + partition_infos = \ + storage_system.RetrieveDiskPartitionInfo( + devicePath=[device_path]) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + log.trace('partition_info = {0}'.format(partition_infos[0])) + return partition_infos[0] + + def get_hosts(service_instance, datacenter_name=None, host_names=None, cluster_name=None, get_all_hosts=False): ''' From cd4d2963d4cd6cf9ced57981b8297f500e6960bc Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 09:47:59 -0400 Subject: [PATCH 368/633] Added salt.utils.vmware._get_new_computed_partition_spec --- salt/utils/vmware.py | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index d3d6b33649..3e22ec3a1f 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2072,6 +2072,69 @@ def _get_partition_info(storage_system, device_path): return partition_infos[0] +def _get_new_computed_partition_spec(hostname, storage_system, device_path, + partition_info): + ''' + Computes the new disk partition info when adding a new vmfs partition that + uses up the remainder of the disk; returns a tuple + (new_partition_number, vim.HostDiskPartitionSpec + ''' + log.trace('Adding a partition at the end of the disk and getting the new ' + 'computed partition spec') + #TODO implement support for multiple partitions + # We support adding a partition add the end of the disk with partitions + free_partitions = [p for p in partition_info.layout.partition + if p.type == 'none'] + if not free_partitions: + raise salt.exceptions.VMwareObjectNotFoundError( + 'Free partition was not found on device \'{0}\'' + ''.format(partition_info.deviceName)) + free_partition = free_partitions[0] + + # Create a layout object that copies the existing one + layout = vim.HostDiskPartitionLayout( + total=partition_info.layout.total, + partition=partition_info.layout.partition) + # Create a partition with the free space on the disk + # Change the free partition type to vmfs + free_partition.type = 'vmfs' + try: + computed_partition_info = storage_system.ComputeDiskPartitionInfo( + devicePath=device_path, + partitionFormat=vim.HostDiskPartitionInfoPartitionFormat.gpt, + layout=layout) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + log.trace('computed partition info = {0}' + ''.format(computed_partition_info)) + log.trace('Retrieving new partition number') + partition_numbers = [p.partition for p in + computed_partition_info.layout.partition + if (p.start.block == free_partition.start.block or + # XXX If the entire disk is free (i.e. the free + # disk partition starts at block 0) the newily + # created partition is created from block 1 + (free_partition.start.block == 0 and + p.start.block == 1)) and + p.end.block == free_partition.end.block and + p.type == 'vmfs'] + if not partition_numbers: + raise salt.exceptions.VMwareNotFoundError( + 'New partition was not found in computed partitions of device ' + '\'{0}\''.format(partition_info.deviceName)) + log.trace('new partition number = {0}'.format(partition_numbers[0])) + return (partition_numbers[0], computed_partition_info.spec) + + def get_hosts(service_instance, datacenter_name=None, host_names=None, cluster_name=None, get_all_hosts=False): ''' From 8e1eb19e4beb7104cd42f5d210f89e37ce2a5d06 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 09:48:57 -0400 Subject: [PATCH 369/633] Added salt.utils.vmware.create_vmfs_datastore --- salt/utils/vmware.py | 61 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 3e22ec3a1f..23918857b1 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2135,6 +2135,67 @@ def _get_new_computed_partition_spec(hostname, storage_system, device_path, return (partition_numbers[0], computed_partition_info.spec) +def create_vmfs_datastore(host_ref, datastore_name, disk_ref, + vmfs_major_version, storage_system=None): + ''' + Creates a VMFS datastore from a disk_id + + host_ref + vim.HostSystem object referencing a host to create the datastore on + + datastore_name + Name of the datastore + + disk_ref + vim.HostScsiDislk on which the datastore is created + + vmfs_major_version + VMFS major version to use + ''' + # TODO Support variable sized partitions + hostname = get_managed_object_name(host_ref) + disk_id = disk_ref.canonicalName + log.debug('Creating datastore \'{0}\' on host \'{1}\', scsi disk \'{2}\', ' + 'vmfs v{3}'.format(datastore_name, hostname, disk_id, + vmfs_major_version)) + if not storage_system: + si = get_service_instance_from_managed_object(host_ref, name=hostname) + storage_system = get_storage_system(si, host_ref, hostname) + + target_disk = disk_ref + partition_info = _get_partition_info(storage_system, + target_disk.devicePath) + log.trace('partition_info = {0}'.format(partition_info)) + new_partition_number, partition_spec = _get_new_computed_partition_spec( + hostname, storage_system, target_disk.devicePath, partition_info) + spec = vim.VmfsDatastoreCreateSpec( + vmfs=vim.HostVmfsSpec( + majorVersion=vmfs_major_version, + volumeName=datastore_name, + extent=vim.HostScsiDiskPartition( + diskName=disk_id, + partition=new_partition_number)), + diskUuid=target_disk.uuid, + partition=partition_spec) + try: + ds_ref = \ + host_ref.configManager.datastoreSystem.CreateVmfsDatastore(spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + log.debug('Created datastore \'{0}\' on host ' + '\'{1}\''.format(datastore_name, hostname)) + return ds_ref + + def get_hosts(service_instance, datacenter_name=None, host_names=None, cluster_name=None, get_all_hosts=False): ''' From 9831a5df77abfe1ec7f4338a12ff10bd5950a7fa Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 09:49:56 -0400 Subject: [PATCH 370/633] Added salt.utils.vmware.get_host_datastore_system --- salt/utils/vmware.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 23918857b1..70a1062040 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2196,6 +2196,37 @@ def create_vmfs_datastore(host_ref, datastore_name, disk_ref, return ds_ref +def get_host_datastore_system(host_ref, hostname=None): + ''' + Returns a host's datastore system + + host_ref + Reference to the ESXi host + + hostname + Name of the host. This argument is optional. + ''' + + if not hostname: + hostname = get_managed_object_name(host_ref) + service_instance = get_service_instance_from_managed_object(host_ref) + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='configManager.datastoreSystem', + type=vim.HostSystem, + skip=False) + objs = get_mors_with_properties(service_instance, + vim.HostDatastoreSystem, + property_list=['datastore'], + container_ref=host_ref, + traversal_spec=traversal_spec) + if not objs: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Host\'s \'{0}\' datastore system was not retrieved' + ''.format(hostname)) + log.trace('[{0}] Retrieved datastore system'.format(hostname)) + return objs[0]['object'] + + def get_hosts(service_instance, datacenter_name=None, host_names=None, cluster_name=None, get_all_hosts=False): ''' From a34cf1215b84cb31dd38f21c60e2ed680fae9d66 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 09:50:15 -0400 Subject: [PATCH 371/633] Added salt.utils.vmware.remove_datastore --- salt/utils/vmware.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 70a1062040..518be5ccfb 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2227,6 +2227,45 @@ def get_host_datastore_system(host_ref, hostname=None): return objs[0]['object'] +def remove_datastore(service_instance, datastore_ref): + ''' + Creates a VMFS datastore from a disk_id + + service_instance + The Service Instance Object containing the datastore + + datastore_ref + The reference to the datastore to remove + ''' + ds_props = get_properties_of_managed_object( + datastore_ref, ['host', 'info', 'name']) + ds_name = ds_props['name'] + log.debug('Removing datastore \'{}\''.format(ds_name)) + ds_info = ds_props['info'] + ds_hosts = ds_props.get('host') + if not ds_hosts: + raise salt.exceptions.VMwareApiError( + 'Datastore \'{0}\' can\'t be removed. No ' + 'attached hosts found'.format(ds_name)) + hostname = get_managed_object_name(ds_hosts[0].key) + host_ds_system = get_host_datastore_system(ds_hosts[0].key, + hostname=hostname) + try: + host_ds_system.RemoveDatastore(datastore_ref) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + log.trace('[{0}] Removed datastore \'{1}\''.format(hostname, ds_name)) + + def get_hosts(service_instance, datacenter_name=None, host_names=None, cluster_name=None, get_all_hosts=False): ''' From 783a75a57c9a8ac864c1157f4abb730f08e561fb Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 09:22:56 -0400 Subject: [PATCH 372/633] Improved logic to filter hosts based on parent in salt.utils.vmware.get_hosts --- salt/utils/vmware.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 518be5ccfb..d579867437 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2292,39 +2292,42 @@ def get_hosts(service_instance, datacenter_name=None, host_names=None, properties = ['name'] if not host_names: host_names = [] - if cluster_name: - properties.append('parent') - if datacenter_name: - start_point = get_datacenter(service_instance, datacenter_name) - if cluster_name: - # Retrieval to test if cluster exists. Cluster existence only makes - # sense if the cluster has been specified - cluster = get_cluster(start_point, cluster_name) - else: + if get_all_hosts or not datacenter_name: # Assume the root folder is the starting point start_point = get_root_folder(service_instance) + else: + if cluster_name: + properties.append('parent') + if datacenter_name: + start_point = get_datacenter(service_instance, datacenter_name) + if cluster_name: + # Retrieval to test if cluster exists. Cluster existence only makes + # sense if the cluster has been specified + cluster = get_cluster(start_point, cluster_name) # Search for the objects hosts = get_mors_with_properties(service_instance, vim.HostSystem, container_ref=start_point, property_list=properties) + log.trace('Retrieved hosts: {0}'.format(h['name'] for h in hosts)) filtered_hosts = [] for h in hosts: # Complex conditions checking if a host should be added to the # filtered list (either due to its name and/or cluster membership) - name_condition = get_all_hosts or (h['name'] in host_names) - # the datacenter_name needs to be set in order for the cluster - # condition membership to be checked, otherwise the condition is - # ignored - cluster_condition = \ - (not datacenter_name or not cluster_name or - (isinstance(h['parent'], vim.ClusterComputeResource) and - h['parent'].name == cluster_name)) - if name_condition and cluster_condition: + if get_all_hosts: filtered_hosts.append(h['object']) + continue + if cluster_name: + if not isinstance(h['parent'], vim.ClusterComputeResource): + continue + parent_name = get_managed_object_name(h['parent']) + if parent_name != cluster_name: + continue + if h['name'] in host_names: + filtered_hosts.append(h['object']) return filtered_hosts From 27cd7cf8e72c8cdfdafaaa847192ef8a1b7abf2b Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 09:30:09 -0400 Subject: [PATCH 373/633] Added salt.utils.vmware._get_scsi_address_to_lun_key_map --- salt/utils/vmware.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index d579867437..18d18859be 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2331,6 +2331,70 @@ def get_hosts(service_instance, datacenter_name=None, host_names=None, return filtered_hosts +def _get_scsi_address_to_lun_key_map(service_instance, + host_ref, + storage_system=None, + hostname=None): + ''' + Returns a map between the scsi addresses and the keys of all luns on an ESXi + host. + map[] = + + service_instance + The Service Instance Object from which to obtain the hosts + + host_ref + The vim.HostSystem object representing the host that contains the + requested disks. + + storage_system + The host's storage system. Default is None. + + hostname + Name of the host. Default is None. + ''' + map = {} + if not hostname: + hostname = get_managed_object_name(host_ref) + if not storage_system: + storage_system = get_storage_system(service_instance, host_ref, + hostname) + try: + device_info = storage_system.storageDeviceInfo + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + if not device_info: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Host\'s \'{0}\' storage device ' + 'info was not retrieved'.format(hostname)) + multipath_info = device_info.multipathInfo + if not multipath_info: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Host\'s \'{0}\' multipath info was not retrieved' + ''.format(hostname)) + if multipath_info.lun is None: + raise salt.exceptions.VMwareObjectRetrievalError( + 'No luns were retrieved from host \'{0}\''.format(hostname)) + lun_key_by_scsi_addr = {} + for l in multipath_info.lun: + # The vmware scsi_address may have multiple comma separated values + # The first one is the actual scsi address + lun_key_by_scsi_addr.update({p.name.split(',')[0]: l.lun + for p in l.path}) + log.trace('Scsi address to lun id map on host \'{0}\': ' + '{1}'.format(hostname, lun_key_by_scsi_addr)) + return lun_key_by_scsi_addr + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From cc21f382d5cd9247ea630f4d980a796fde8eb3f1 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 09:34:09 -0400 Subject: [PATCH 374/633] Added salt.utils.vmware.get_all_luns --- salt/utils/vmware.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 18d18859be..4a99edb17d 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2395,6 +2395,56 @@ def _get_scsi_address_to_lun_key_map(service_instance, return lun_key_by_scsi_addr +def get_all_luns(host_ref, storage_system=None, hostname=None): + ''' + Returns a list of all vim.HostScsiDisk objects in a disk + + host_ref + The vim.HostSystem object representing the host that contains the + requested disks. + + storage_system + The host's storage system. Default is None. + + hostname + Name of the host. This argument is optional. + ''' + if not hostname: + hostname = get_managed_object_name(host_ref) + if not storage_system: + si = get_service_instance_from_managed_object(host_ref, name=hostname) + storage_system = get_storage_system(si, host_ref, hostname) + if not storage_system: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Host\'s \'{0}\' storage system was not retrieved' + ''.format(hostname)) + try: + device_info = storage_system.storageDeviceInfo + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + if not device_info: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Host\'s \'{0}\' storage device info was not retrieved' + ''.format(hostname)) + + scsi_luns = device_info.scsiLun + if scsi_luns: + log.trace('Retrieved scsi luns in host \'{0}\': {1}' + ''.format(hostname, [l.canonicalName for l in scsi_luns])) + return scsi_luns + log.trace('Retrieved no scsi_luns in host \'{0}\''.format(hostname)) + return [] + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From 5a8cc2f19f96508a7b167ea0d6fd03f919f65062 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 10:27:31 -0400 Subject: [PATCH 375/633] Added salt.utils.vmware.get_scsi_address_to_lun_map --- salt/utils/vmware.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 4a99edb17d..42116941ad 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2445,6 +2445,35 @@ def get_all_luns(host_ref, storage_system=None, hostname=None): return [] +def get_scsi_address_to_lun_map(host_ref, storage_system=None, hostname=None): + ''' + Returns a map of all vim.ScsiLun objects on a ESXi host keyed by their + scsi address + + host_ref + The vim.HostSystem object representing the host that contains the + requested disks. + + storage_system + The host's storage system. Default is None. + + hostname + Name of the host. This argument is optional. + ''' + if not hostname: + hostname = get_managed_object_name(host_ref) + si = get_service_instance_from_managed_object(host_ref, name=hostname) + if not storage_system: + storage_system = get_storage_system(si, host_ref, hostname) + lun_ids_to_scsi_addr_map = \ + _get_scsi_address_to_lun_key_map(si, host_ref, storage_system, + hostname) + luns_to_key_map = {d.key: d for d in + get_all_luns(host_ref, storage_system, hostname)} + return {scsi_addr: luns_to_key_map[lun_key] for scsi_addr, lun_key in + lun_ids_to_scsi_addr_map.iteritems()} + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From 8b7af00e275954cc448eaafc3c0a438e96b487db Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 10:36:28 -0400 Subject: [PATCH 376/633] Added salt.utils.vmware.get_disks --- salt/utils/vmware.py | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 42116941ad..bb6628973c 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2474,6 +2474,62 @@ def get_scsi_address_to_lun_map(host_ref, storage_system=None, hostname=None): lun_ids_to_scsi_addr_map.iteritems()} +def get_disks(host_ref, disk_ids=None, scsi_addresses=None, + get_all_disks=False): + ''' + Returns a list of vim.HostScsiDisk objects representing disks + in a ESXi host, filtered by their cannonical names and scsi_addresses + + host_ref + The vim.HostSystem object representing the host that contains the + requested disks. + + disk_ids + The list of canonical names of the disks to be retrieved. Default value + is None + + scsi_addresses + The list of scsi addresses of the disks to be retrieved. Default value + is None + + get_all_disks + Specifies whether to retrieve all disks in the host. + Default value is False. + ''' + hostname = get_managed_object_name(host_ref) + if get_all_disks: + log.trace('Retrieving all disks in host \'{0}\''.format(hostname)) + else: + log.trace('Retrieving disks in host \'{0}\': ids = ({1}); scsi ' + 'addresses = ({2})'.format(hostname, disk_ids, + scsi_addresses)) + if not (disk_ids or scsi_addresses): + return [] + si = get_service_instance_from_managed_object(host_ref, name=hostname) + storage_system = get_storage_system(si, host_ref, hostname) + disk_keys = [] + if scsi_addresses: + # convert the scsi addresses to disk keys + lun_key_by_scsi_addr = _get_scsi_address_to_lun_key_map(si, host_ref, + storage_system, + hostname) + disk_keys = [key for scsi_addr, key in lun_key_by_scsi_addr.iteritems() + if scsi_addr in scsi_addresses] + log.trace('disk_keys based on scsi_addresses = {0}'.format(disk_keys)) + + scsi_luns = get_all_luns(host_ref, storage_system) + scsi_disks = [disk for disk in scsi_luns + if isinstance(disk, vim.HostScsiDisk) and ( + get_all_disks or + # Filter by canonical name + (disk_ids and (disk.canonicalName in disk_ids)) or + # Filter by disk keys from scsi addresses + (disk.key in disk_keys))] + log.trace('Retrieved disks in host \'{0}\': {1}' + ''.format(hostname, [d.canonicalName for d in scsi_disks])) + return scsi_disks + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From 200159d76d4dae4e40d784db8e8b5056515386a6 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 10:38:55 -0400 Subject: [PATCH 377/633] Added salt.utils.vmware.get_disk_partition_info --- salt/utils/vmware.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index bb6628973c..959b0b8ecf 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2530,6 +2530,48 @@ def get_disks(host_ref, disk_ids=None, scsi_addresses=None, return scsi_disks +def get_disk_partition_info(host_ref, disk_id, storage_system=None): + ''' + Returns all partitions on a disk + + host_ref + The reference of the ESXi host containing the disk + + disk_id + The canonical name of the disk whose partitions are to be removed + + storage_system + The ESXi host's storage system. Default is None. + ''' + hostname = get_managed_object_name(host_ref) + service_instance = get_service_instance_from_managed_object(host_ref) + if not storage_system: + storage_system = get_storage_system(service_instance, host_ref, + hostname) + + props = get_properties_of_managed_object(storage_system, + ['storageDeviceInfo.scsiLun']) + if not props.get('storageDeviceInfo.scsiLun'): + raise salt.exceptions.VMwareObjectRetrievalError( + 'No devices were retrieved in host \'{0}\''.format(hostname)) + log.trace('[{0}] Retrieved {1} devices: {2}'.format( + hostname, len(props['storageDeviceInfo.scsiLun']), + ', '.join([l.canonicalName + for l in props['storageDeviceInfo.scsiLun']]))) + disks = [l for l in props['storageDeviceInfo.scsiLun'] + if isinstance(l, vim.HostScsiDisk) and + l.canonicalName == disk_id] + if not disks: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Disk \'{0}\' was not found in host \'{1}\'' + ''.format(disk_id, hostname)) + log.trace('[{0}] device_path = {1}'.format(hostname, disks[0].devicePath)) + partition_info = _get_partition_info(storage_system, disks[0].devicePath) + log.trace('[{0}] Retrieved {1} partition(s) on disk \'{2}\'' + ''.format(hostname, len(partition_info.spec.partition), disk_id)) + return partition_info + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From c386612c0769dcbdea5c1ef4b20f28a93a799074 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 10:41:33 -0400 Subject: [PATCH 378/633] Added salt.utils.vmware.erase_disk_partitions --- salt/utils/vmware.py | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 959b0b8ecf..bc3e87da3e 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2572,6 +2572,78 @@ def get_disk_partition_info(host_ref, disk_id, storage_system=None): return partition_info +def erase_disk_partitions(service_instance, host_ref, disk_id, + hostname=None, storage_system=None): + ''' + Erases all partitions on a disk + + in a vcenter filtered by their names and/or datacenter, cluster membership + + service_instance + The Service Instance Object from which to obtain all information + + host_ref + The reference of the ESXi host containing the disk + + disk_id + The canonical name of the disk whose partitions are to be removed + + hostname + The ESXi hostname. Default is None. + + storage_system + The ESXi host's storage system. Default is None. + ''' + + if not hostname: + hostname = get_managed_object_name(host_ref) + if not storage_system: + storage_system = get_storage_system(service_instance, host_ref, + hostname) + + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='configManager.storageSystem', + type=vim.HostSystem, + skip=False) + results = get_mors_with_properties(service_instance, + vim.HostStorageSystem, + ['storageDeviceInfo.scsiLun'], + container_ref=host_ref, + traversal_spec=traversal_spec) + if not results: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Host\'s \'{0}\' devices were not retrieved'.format(hostname)) + log.trace('[{0}] Retrieved {1} devices: {2}'.format( + hostname, len(results[0].get('storageDeviceInfo.scsiLun', [])), + ', '.join([l.canonicalName for l in + results[0].get('storageDeviceInfo.scsiLun', [])]))) + disks = [l for l in results[0].get('storageDeviceInfo.scsiLun', []) + if isinstance(l, vim.HostScsiDisk) and + l.canonicalName == disk_id] + if not disks: + raise salt.exceptions.VMwareObjectRetrievalError( + 'Disk \'{0}\' was not found in host \'{1}\'' + ''.format(disk_id, hostname)) + log.trace('[{0}] device_path = {1}'.format(hostname, disks[0].devicePath)) + # Erase the partitions by setting an empty partition spec + try: + storage_system.UpdateDiskPartitions(disks[0].devicePath, + vim.HostDiskPartitionSpec()) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + log.trace('[{0}] Erased partitions on disk \'{1}\'' + ''.format(hostname, disk_id)) + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From 3d0383694f38466535ad7df7e067b1e24cf7c9fb Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 10:44:39 -0400 Subject: [PATCH 379/633] Added salt.utils.get_diskgroups --- salt/utils/vmware.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index bc3e87da3e..52e8838fa4 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2644,6 +2644,66 @@ def erase_disk_partitions(service_instance, host_ref, disk_id, ''.format(hostname, disk_id)) +def get_diskgroups(host_ref, cache_disk_ids=None, get_all_disk_groups=False): + ''' + Returns a list of vim.VsanHostDiskMapping objects representing disks + in a ESXi host, filtered by their cannonical names. + + host_ref + The vim.HostSystem object representing the host that contains the + requested disks. + + cache_disk_ids + The list of cannonical names of the cache disks to be retrieved. The + canonical name of the cache disk is enough to identify the disk group + because it is guaranteed to have one and only one cache disk. + Default is None. + + get_all_disk_groups + Specifies whether to retrieve all disks groups in the host. + Default value is False. + ''' + hostname = get_managed_object_name(host_ref) + if get_all_disk_groups: + log.trace('Retrieving all disk groups on host \'{0}\'' + ''.format(hostname)) + else: + log.trace('Retrieving disk groups from host \'{0}\', with cache disk ' + 'ids : ({1})'.format(hostname, cache_disk_ids)) + if not cache_disk_ids: + return [] + try: + vsan_host_config = host_ref.config.vsanHostConfig + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + if not vsan_host_config: + raise salt.exceptions.VMwareObjectRetrievalError( + 'No host config found on host \'{0}\''.format(hostname)) + vsan_storage_info = vsan_host_config.storageInfo + if not vsan_storage_info: + raise salt.exceptions.VMwareObjectRetrievalError( + 'No vsan storage info found on host \'{0}\''.format(hostname)) + vsan_disk_mappings = vsan_storage_info.diskMapping + if not vsan_disk_mappings: + return [] + disk_groups = [dm for dm in vsan_disk_mappings if \ + (get_all_disk_groups or \ + (dm.ssd.canonicalName in cache_disk_ids))] + log.trace('Retrieved disk groups on host \'{0}\', with cache disk ids : ' + '{1}'.format(hostname, + [d.ssd.canonicalName for d in disk_groups])) + return disk_groups + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From 13e8bad397cfb3cbb5c9182b819093e3949174b7 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 10:46:14 -0400 Subject: [PATCH 380/633] Added salt.utils._check_disks_in_diskgroup --- salt/utils/vmware.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 52e8838fa4..66ca37ed61 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2704,6 +2704,27 @@ def get_diskgroups(host_ref, cache_disk_ids=None, get_all_disk_groups=False): return disk_groups +def _check_disks_in_diskgroup(disk_group, cache_disk_id, capacity_disk_ids): + ''' + Checks that the disks in a disk group are as expected and raises + CheckError exceptions if the check fails + ''' + if not disk_group.ssd.canonicalName == cache_disk_id: + raise salt.exceptions.ArgumentValueError( + 'Incorrect diskgroup cache disk; got id: \'{0}\'; expected id: ' + '\'{1}\''.format(disk_group.ssd.canonicalName, cache_disk_id)) + if sorted([d.canonicalName for d in disk_group.nonSsd]) != \ + sorted(capacity_disk_ids): + + raise salt.exceptions.ArgumentValueError( + 'Incorrect capacity disks; got ids: \'{0}\'; expected ids: \'{1}\'' + ''.format(sorted([d.canonicalName for d in disk_group.nonSsd]), + sorted(capacity_disk_ids))) + log.trace('Checked disks in diskgroup with cache disk id \'{0}\'' + ''.format(cache_disk_id)) + return True + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From beb2b615889351338a36ba1eb785de119de9e020 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 16:06:20 -0400 Subject: [PATCH 381/633] Added salt.utils.vmware.get_host_cache --- salt/utils/vmware.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 66ca37ed61..eb6132a148 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2725,6 +2725,47 @@ def _check_disks_in_diskgroup(disk_group, cache_disk_id, capacity_disk_ids): return True +#TODO Support host caches on multiple datastores +def get_host_cache(host_ref, host_cache_manager=None): + ''' + Returns a vim.HostScsiDisk if the host cache is configured on the specified + host, other wise returns None + + host_ref + The vim.HostSystem object representing the host that contains the + requested disks. + + host_cache_manager + The vim.HostCacheConfigurationManager object representing the cache + configuration manager on the specified host. Default is None. If None, + it will be retrieved in the method + ''' + hostname = get_managed_object_name(host_ref) + service_instance = get_service_instance_from_managed_object(host_ref) + log.trace('Retrieving the host cache on host \'{0}\''.format(hostname)) + if not host_cache_manager: + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( + path='configManager.cacheConfigurationManager', + type=vim.HostSystem, + skip=False) + results = get_mors_with_properties(service_instance, + vim.HostCacheConfigurationManager, + ['cacheConfigurationInfo'], + container_ref=host_ref, + traversal_spec=traversal_spec) + if not results or not results[0].get('cacheConfigurationInfo'): + log.trace('Host \'{0}\' has no host cache'.format(hostname)) + return None + return results[0]['cacheConfigurationInfo'][0] + else: + results = get_properties_of_managed_object(host_cache_manager, + ['cacheConfigurationInfo']) + if not results: + log.trace('Host \'{0}\' has no host cache'.format(hostname)) + return None + return results['cacheConfigurationInfo'][0] + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From f84c55bf83ccd0a4f3fe9817622085fd970561d3 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 16:11:58 -0400 Subject: [PATCH 382/633] Added salt.utils.vmware.configure_host_cache --- salt/utils/vmware.py | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index eb6132a148..1c226e9cc0 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2766,6 +2766,62 @@ def get_host_cache(host_ref, host_cache_manager=None): return results['cacheConfigurationInfo'][0] +#TODO Support host caches on multiple datastores +def configure_host_cache(host_ref, datastore_ref, swap_size_MiB, + host_cache_manager=None): + ''' + Configures the host cahe of the specified host + + host_ref + The vim.HostSystem object representing the host that contains the + requested disks. + + datastore_ref + The vim.Datastore opject representing the datastore the host cache will + be configured on. + + swap_size_MiB + The size in Mibibytes of the swap. + + host_cache_manager + The vim.HostCacheConfigurationManager object representing the cache + configuration manager on the specified host. Default is None. If None, + it will be retrieved in the method + ''' + hostname = get_managed_object_name(host_ref) + if not host_cache_manager: + props = get_properties_of_managed_object( + host_ref, ['configManager.cacheConfigurationManager']) + if not props.get('configManager.cacheConfigurationManager'): + raise salt.exceptions.VMwareObjectRetrievalError( + 'Host \'{0}\' has no host cache'.format(hostname)) + host_cache_manager = props['configManager.cacheConfigurationManager'] + log.trace('Configuring the host cache on host \'{0}\', datastore \'{1}\', ' + 'swap size={2} MiB'.format(hostname, datastore_ref.name, + swap_size_MiB)) + + spec = vim.HostCacheConfigurationSpec( + datastore=datastore_ref, + swapSize=swap_size_MiB) + log.trace('host_cache_spec={0}'.format(spec)) + try: + task = host_cache_manager.ConfigureHostCache_Task(spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + 'Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + wait_for_task(task, hostname, 'HostCacheConfigurationTask') + log.trace('Configured host cache on host \'{0}\''.format(hostname)) + return True + + def list_hosts(service_instance): ''' Returns a list of hosts associated with a given service instance. From 6ad97b01e4f1393f2a8b1a28838cd2c4f3a5d1bc Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 15:43:42 -0400 Subject: [PATCH 383/633] Change debug logs to trace logs in salt.utils.vmware.get_datastores --- salt/utils/vmware.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 1c226e9cc0..3c861c27c0 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -1909,7 +1909,7 @@ def get_datastores(service_instance, reference, datastore_names=None, 'is set'.format(reference.__class__.__name__)) if (not get_all_datastores) and backing_disk_ids: # At this point we know the reference is a vim.HostSystem - log.debug('Filtering datastores with backing disk ids: {}' + log.trace('Filtering datastores with backing disk ids: {}' ''.format(backing_disk_ids)) storage_system = get_storage_system(service_instance, reference, obj_name) @@ -1925,11 +1925,11 @@ def get_datastores(service_instance, reference, datastore_names=None, # Skip volume if it doesn't contain an extent with a # canonical name of interest continue - log.debug('Found datastore \'{0}\' for disk id(s) \'{1}\'' + log.trace('Found datastore \'{0}\' for disk id(s) \'{1}\'' ''.format(vol.name, [e.diskName for e in vol.extent])) disk_datastores.append(vol.name) - log.debug('Datastore found for disk filter: {}' + log.trace('Datastore found for disk filter: {}' ''.format(disk_datastores)) if datastore_names: datastore_names.extend(disk_datastores) From 0186045169fd98bfe49cadba8ab846247314f15f Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 15:44:20 -0400 Subject: [PATCH 384/633] Change debug logs to trace logs in salt.utils.vmware.rename_datastore --- salt/utils/vmware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 3c861c27c0..f2fbf43f59 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2006,7 +2006,7 @@ def rename_datastore(datastore_ref, new_datastore_name): New datastore name ''' ds_name = get_managed_object_name(datastore_ref) - log.debug('Renaming datastore \'{0}\' to ' + log.trace('Renaming datastore \'{0}\' to ' '\'{1}\''.format(ds_name, new_datastore_name)) try: datastore_ref.RenameDatastore(new_datastore_name) From e9890106160b0571d3d0ab840fa862de3f17b7dd Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 19:51:11 -0400 Subject: [PATCH 385/633] Added salt.modules.list_hosts_via_proxy --- salt/modules/vsphere.py | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index efde0b6d07..15343fb260 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -5813,6 +5813,57 @@ def assign_license(license_key, license_name, entity, entity_display_name, entity_name=entity_display_name) +@depends(HAS_PYVMOMI) +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def list_hosts_via_proxy(hostnames=None, datacenter=None, + cluster=None, service_instance=None): + ''' + Returns a list of hosts for the the specified VMware environment. The list + of hosts can be filtered by datacenter name and/or cluster name + + hostnames + Hostnames to filter on. + + datacenter_name + Name of datacenter. Only hosts in this datacenter will be retrieved. + Default is None. + + cluster_name + Name of cluster. Only hosts in this cluster will be retrieved. If a + datacenter is not specified the first cluster with this name will be + considerred. Default is None. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + CLI Example: + + .. code-block:: bash + + salt '*' vsphere.list_hosts_via_proxy + + salt '*' vsphere.list_hosts_via_proxy hostnames=[esxi1.example.com] + + salt '*' vsphere.list_hosts_via_proxy datacenter=dc1 cluster=cluster1 + ''' + if cluster: + if not datacenter: + raise salt.exceptions.ArgumentValueError( + 'Datacenter is required when cluster is specified') + get_all_hosts = False + if not hostnames and not datacenter and not cluster: + get_all_hosts = True + hosts = salt.utils.vmware.get_hosts(service_instance, + datacenter_name=datacenter, + host_names=hostnames, + cluster_name=cluster, + get_all_hosts=get_all_hosts) + return [salt.utils.vmware.get_managed_object_name(h) for h in hosts] + + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From 4230224fe4c04af87abbda1d86b97d94af808917 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 19:54:04 -0400 Subject: [PATCH 386/633] Added salt.modules.vsphere.list_disks --- salt/modules/vsphere.py | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 15343fb260..07983b74b9 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -5863,6 +5863,55 @@ def list_hosts_via_proxy(hostnames=None, datacenter=None, return [salt.utils.vmware.get_managed_object_name(h) for h in hosts] +@depends(HAS_PYVMOMI) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def list_disks(disk_ids=None, scsi_addresses=None, service_instance=None): + ''' + Returns a list of dict representations of the disks in an ESXi host. + The list of disks can be filtered by disk canonical names or + scsi addresses. + + disk_ids: + List of disk canonical names to be retrieved. Default is None. + + scsi_addresses + List of scsi addresses of disks to be retrieved. Default is None + + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.list_disks + + salt '*' vsphere.list_disks disk_ids='[naa.00, naa.001]' + + salt '*' vsphere.list_disks + scsi_addresses='[vmhba0:C0:T0:L0, vmhba1:C0:T0:L0]' + ''' + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + log.trace('Retrieving disks if host \'{0}\''.format(hostname)) + log.trace('disk ids = {0}'.format(disk_ids)) + log.trace('scsi_addresses = {0}'.format(scsi_addresses)) + # Default to getting all disks if no filtering is done + get_all_disks = True if not (disk_ids or scsi_addresses) else False + ret_list = [] + scsi_address_to_lun = salt.utils.vmware.get_scsi_address_to_lun_map( + host_ref, hostname=hostname) + canonical_name_to_scsi_address = { + lun.canonicalName: scsi_addr + for scsi_addr, lun in scsi_address_to_lun.iteritems()} + for d in salt.utils.vmware.get_disks(host_ref, disk_ids, scsi_addresses, + get_all_disks): + ret_list.append({'id': d.canonicalName, + 'scsi_address': + canonical_name_to_scsi_address[d.canonicalName]}) + return ret_list + def _check_hosts(service_instance, host, host_names): ''' From 5d89f7b7430ae4e26dbaa547a40765e8e9d63a9e Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 19:56:10 -0400 Subject: [PATCH 387/633] Added salt.modules.vsphere.erase_disk_partitions --- salt/modules/vsphere.py | 55 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 07983b74b9..ae33157967 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -5913,6 +5913,61 @@ def list_disks(disk_ids=None, scsi_addresses=None, service_instance=None): return ret_list +@depends(HAS_PYVMOMI) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def erase_disk_partitions(disk_id=None, scsi_address=None, + service_instance=None): + ''' + Erases the partitions on a disk. + The disk can be specified either by the canonical name, or by the + scsi_address. + + disk_id + Canonical name of the disk. + Either ``disk_id`` or ``scsi_address`` needs to be specified + (``disk_id`` supersedes ``scsi_address``. + + scsi_address` + Scsi address of the disk. + ``disk_id`` or ``scsi_address`` needs to be specified + (``disk_id`` supersedes ``scsi_address``. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.erase_disk_partitions scsi_address='vmhaba0:C0:T0:L0' + + salt '*' vsphere.erase_disk_partitions disk_id='naa.000000000000001' + ''' + if not disk_id and not scsi_address: + raise ArgumentValueError('Either \'disk_id\' or \'scsi_address\' ' + 'needs to be specified') + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + if not disk_id: + scsi_address_to_lun = \ + salt.utils.vmware.get_scsi_address_to_lun_map(host_ref) + if scsi_address not in scsi_address_to_lun: + raise VMwareObjectRetrievalError( + 'Scsi lun with address \'{0}\' was not found on host \'{1}\'' + ''.format(scsi_address, hostname)) + disk_id = scsi_address_to_lun[scsi_address].canonicalName + log.trace('[{0}] Got disk id \'{1}\' for scsi address \'{2}\'' + ''.format(hostname, disk_id, scsi_address)) + log.trace('Erasing disk partitions on disk \'{0}\' in host \'{1}\'' + ''.format(disk_id, hostname)) + salt.utils.vmware.erase_disk_partitions(service_instance, + host_ref, disk_id, + hostname=hostname) + log.info('Erased disk partitions on disk \'{0}\' on host \'{1}\'' + ''.format(disk_id, esxi_host)) + return True + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From f76115fc67063986158936af173642679c87b644 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 19:57:43 -0400 Subject: [PATCH 388/633] Added salt.modules.vsphere.list_disk_partitions --- salt/modules/vsphere.py | 69 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index ae33157967..a7b36c4a9c 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -5968,6 +5968,75 @@ def erase_disk_partitions(disk_id=None, scsi_address=None, return True +@depends(HAS_PYVMOMI) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def list_disk_partitions(disk_id=None, scsi_address=None, + service_instance=None): + ''' + Lists the partitions on a disk. + The disk can be specified either by the canonical name, or by the + scsi_address. + + disk_id + Canonical name of the disk. + Either ``disk_id`` or ``scsi_address`` needs to be specified + (``disk_id`` supersedes ``scsi_address``. + + scsi_address` + Scsi address of the disk. + ``disk_id`` or ``scsi_address`` needs to be specified + (``disk_id`` supersedes ``scsi_address``. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.list_disk_partitions scsi_address='vmhaba0:C0:T0:L0' + + salt '*' vsphere.list_disk_partitions disk_id='naa.000000000000001' + ''' + if not disk_id and not scsi_address: + raise ArgumentValueError('Either \'disk_id\' or \'scsi_address\' ' + 'needs to be specified') + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + if not disk_id: + scsi_address_to_lun = \ + salt.utils.vmware.get_scsi_address_to_lun_map(host_ref) + if scsi_address not in scsi_address_to_lun: + raise VMwareObjectRetrievalError( + 'Scsi lun with address \'{0}\' was not found on host \'{1}\'' + ''.format(scsi_address, hostname)) + disk_id = scsi_address_to_lun[scsi_address].canonicalName + log.trace('[{0}] Got disk id \'{1}\' for scsi address \'{2}\'' + ''.format(hostname, disk_id, scsi_address)) + log.trace('Listing disk partitions on disk \'{0}\' in host \'{1}\'' + ''.format(disk_id, hostname)) + partition_info = \ + salt.utils.vmware.get_disk_partition_info(host_ref, disk_id) + ret_list = [] + # NOTE: 1. The layout view has an extra 'None' partition for free space + # 2. The orders in the layout/partition views are not the same + for part_spec in partition_info.spec.partition: + part_layout = [p for p in partition_info.layout.partition + if p.partition == part_spec.partition][0] + part_dict = {'hostname': hostname, + 'device': disk_id, + 'format': partition_info.spec.partitionFormat, + 'partition': part_spec.partition, + 'type': part_spec.type, + 'sectors': + part_spec.endSector - part_spec.startSector + 1, + 'size_KB': + (part_layout.end.block - part_layout.start.block + 1) * + part_layout.start.blockSize / 1024} + ret_list.append(part_dict) + return ret_list + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From 23fbb26f31b1f84eab4cdce6973adb2d2f5586bc Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 19:58:41 -0400 Subject: [PATCH 389/633] Added salt.modules.vsphere.list_diskgroups --- salt/modules/vsphere.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index a7b36c4a9c..917e1ae07f 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -6037,6 +6037,46 @@ def list_disk_partitions(disk_id=None, scsi_address=None, return ret_list +@depends(HAS_PYVMOMI) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def list_diskgroups(cache_disk_ids=None, service_instance=None): + ''' + Returns a list of disk group dict representation on an ESXi host. + The list of disk groups can be filtered by the cache disks + canonical names. If no filtering is applied, all disk groups are returned. + + cache_disk_ids: + List of cache disk canonical names of the disk groups to be retrieved. + Default is None. + + use_proxy_details + Specify whether to use the proxy minion's details instead of the + arguments + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.list_diskgroups + + salt '*' vsphere.list_diskgroups cache_disk_ids='[naa.000000000000001]' + ''' + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + log.trace('Listing diskgroups in \'{0}\''.format(hostname)) + get_all_diskgroups = True if not cache_disk_ids else False + ret_list = [] + for dg in salt.utils.vmware.get_diskgroups(host_ref, cache_disk_ids, + get_all_diskgroups): + ret_list.append( + {'cache_disk': dg.ssd.canonicalName, + 'capacity_disks': [d.canonicalName for d in dg.nonSsd]}) + return ret_list + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From 40589adc0cb0d8b144d41a0e900cee9a288373a6 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 06:05:43 -0400 Subject: [PATCH 390/633] Added DiskGroupsDiskIdSchema JSON schema and DiskGroupDiskIdItem complex schema item --- salt/config/schemas/esxi.py | 40 ++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/salt/config/schemas/esxi.py b/salt/config/schemas/esxi.py index affd14be59..2a89418861 100644 --- a/salt/config/schemas/esxi.py +++ b/salt/config/schemas/esxi.py @@ -13,12 +13,50 @@ from __future__ import absolute_import # Import Salt libs -from salt.utils.schema import (Schema, +from salt.utils.schema import (DefinitionsSchema, + Schema, + ComplexSchemaItem, ArrayItem, IntegerItem, StringItem) +class DiskGroupDiskIdItem(ComplexSchemaItem): + ''' + Schema item of a ESXi host disk group containg disk ids + ''' + + title = 'Diskgroup Disk Id Item' + description = 'ESXi host diskgroup item containing disk ids' + + + cache_id = StringItem( + title='Cache Disk Id', + description='Specifies the id of the cache disk', + pattern=r'[^\s]+') + + capacity_ids = ArrayItem( + title='Capacity Disk Ids', + description='Array with the ids of the capacity disks', + items=StringItem(pattern=r'[^\s]+'), + min_items=1) + + +class DiskGroupsDiskIdSchema(DefinitionsSchema): + ''' + Schema of ESXi host diskgroups containing disk ids + ''' + + title = 'Diskgroups Disk Id Schema' + description = 'ESXi host diskgroup schema containing disk ids' + diskgroups = ArrayItem( + title='DiskGroups', + description='List of disk groups in an ESXi host', + min_items = 1, + items=DiskGroupDiskIdItem(), + required=True) + + class EsxiProxySchema(Schema): ''' Schema of the esxi proxy input From 7532c286903e33ec1e9dca82ed1bf90095937145 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Sun, 24 Sep 2017 20:04:24 -0400 Subject: [PATCH 391/633] Added salt.modules.vsphere.create_diskgroup --- salt/modules/vsphere.py | 70 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 917e1ae07f..19573c1e89 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -185,6 +185,7 @@ from salt.utils.decorators import depends, ignores_kwargs from salt.config.schemas.esxcluster import ESXClusterConfigSchema, \ ESXClusterEntitySchema from salt.config.schemas.vcenter import VCenterEntitySchema +from salt.config.schemas.esxi import DiskGroupsDiskIdSchema # Import Third Party Libs try: @@ -6077,6 +6078,75 @@ def list_diskgroups(cache_disk_ids=None, service_instance=None): return ret_list +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def create_diskgroup(cache_disk_id, capacity_disk_ids, safety_checks=True, + service_instance=None): + ''' + Creates disk group on an ESXi host with the specified cache and + capacity disks. + + cache_disk_id + The canonical name of the disk to be used as a cache. The disk must be + ssd. + + capacity_disk_ids + A list containing canonical names of the capacity disks. Must contain at + least one id. Default is True. + + safety_checks + Specify whether to perform safety check or to skip the checks and try + performing the required task. Default value is True. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.create_diskgroup cache_disk_id='naa.000000000000001' + capacity_disk_ids='[naa.000000000000002, naa.000000000000003]' + ''' + log.trace('Validating diskgroup input') + schema = DiskGroupsDiskIdSchema.serialize() + try: + jsonschema.validate( + {'diskgroups': [{'cache_id': cache_disk_id, + 'capacity_ids': capacity_disk_ids}]}, + schema) + except jsonschema.exceptions.ValidationError as exc: + raise ArgumentValueError(exc) + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + if safety_checks: + diskgroups = \ + salt.utils.vmware.get_diskgroups(host_ref, [cache_disk_id]) + if diskgroups: + raise VMwareObjectExistsError( + 'Diskgroup with cache disk id \'{0}\' already exists ESXi ' + 'host \'{1}\''.format(cache_disk_id, hostname)) + disk_ids = capacity_disk_ids[:] + disk_ids.insert(0, cache_disk_id) + disks = salt.utils.vmware.get_disks(host_ref, disk_ids=disk_ids) + for id in disk_ids: + if not [d for d in disks if d.canonicalName == id]: + raise VMwareObjectRetrievalError( + 'No disk with id \'{0}\' was found in ESXi host \'{0}\'' + ''.format(id, hostname)) + cache_disk = [d for d in disks if d.canonicalName == cache_disk_id][0] + capacity_disks = [d for d in disks if d.canonicalName in capacity_disk_ids] + vsan_disk_mgmt_system = \ + salt.utils.vsan.get_vsan_disk_management_system(service_instance) + dg = salt.utils.vsan.create_diskgroup(service_instance, + vsan_disk_mgmt_system, + host_ref, + cache_disk, + capacity_disks) + return True + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From 41d3846c110a84833a0c0ff2865ea289e359b909 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 06:07:12 -0400 Subject: [PATCH 392/633] Added salt.modules.vsphere.add_capacity_to_diskgroup --- salt/modules/vsphere.py | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 19573c1e89..e32a506767 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -6147,6 +6147,70 @@ def create_diskgroup(cache_disk_id, capacity_disk_ids, safety_checks=True, return True +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def add_capacity_to_diskgroup(cache_disk_id, capacity_disk_ids, + safety_checks=True, service_instance=None): + ''' + Adds capacity disks to the disk group with the specified cache disk. + + cache_disk_id + The canonical name of the cache disk. + + capacity_disk_ids + A list containing canonical names of the capacity disks to add. + + safety_checks + Specify whether to perform safety check or to skip the checks and try + performing the required task. Default value is True. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.add_capacity_to_diskgroup + cache_disk_id='naa.000000000000001' + capacity_disk_ids='[naa.000000000000002, naa.000000000000003]' + ''' + log.trace('Validating diskgroup input') + schema = DiskGroupsDiskIdSchema.serialize() + try: + jsonschema.validate( + {'diskgroups': [{'cache_id': cache_disk_id, + 'capacity_ids': capacity_disk_ids}]}, + schema) + except jsonschema.exceptions.ValidationError as exc: + raise ArgumentValueError(exc) + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + disks = salt.utils.vmware.get_disks(host_ref, disk_ids=capacity_disk_ids) + if safety_checks: + for id in capacity_disk_ids: + if not [d for d in disks if d.canonicalName == id]: + raise VMwareObjectRetrievalError( + 'No disk with id \'{0}\' was found in ESXi host \'{1}\'' + ''.format(id, hostname)) + diskgroups = \ + salt.utils.vmware.get_diskgroups( + host_ref, cache_disk_ids=[cache_disk_id]) + if not diskgroups: + raise VMwareObjectRetrievalError( + 'No diskgroup with cache disk id \'{0}\' was found in ESXi ' + 'host \'{1}\''.format(cache_disk_id, esxi_host)) + vsan_disk_mgmt_system = \ + salt.utils.vsan.get_vsan_disk_management_system(service_instance) + salt.utils.vsan.add_capacity_to_diskgroup(service_instance, + vsan_disk_mgmt_system, + host_ref, + disk_groups[0], + disks) + return True + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From 790472673672442bc2c00affcac9b6f3f46e1bb6 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 06:08:34 -0400 Subject: [PATCH 393/633] Added salt.modules.vsphere.remove_capacity_from_diskgroup --- salt/modules/vsphere.py | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index e32a506767..5a25c4bed3 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -6211,6 +6211,74 @@ def add_capacity_to_diskgroup(cache_disk_id, capacity_disk_ids, return True +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def remove_capacity_from_diskgroup(cache_disk_id, capacity_disk_ids, + data_evacuation=True, safety_checks=True, + service_instance=None): + ''' + Remove capacity disks from the disk group with the specified cache disk. + + cache_disk_id + The canonical name of the cache disk. + + capacity_disk_ids + A list containing canonical names of the capacity disks to add. + + data_evacuation + Specifies whether to gracefully evacuate the data on the capacity disks + before removing them from the disk group. Default value is True. + + safety_checks + Specify whether to perform safety check or to skip the checks and try + performing the required task. Default value is True. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.remove_capacity_from_diskgroup + cache_disk_id='naa.000000000000001' + capacity_disk_ids='[naa.000000000000002, naa.000000000000003]' + ''' + log.trace('Validating diskgroup input') + schema = DiskGroupsDiskIdSchema.serialize() + try: + jsonschema.validate( + {'diskgroups': [{'cache_id': cache_disk_id, + 'capacity_ids': capacity_disk_ids}]}, + schema) + except jsonschema.exceptions.ValidationError as exc: + raise ArgumentValueError(exc) + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + disks = salt.utils.vmware.get_disks(host_ref, disk_ids=capacity_disk_ids) + if safety_checks: + for id in capacity_disk_ids: + if not [d for d in disks if d.canonicalName == id]: + raise VMwareObjectRetrievalError( + 'No disk with id \'{0}\' was found in ESXi host \'{1}\'' + ''.format(id, hostname)) + diskgroups = \ + salt.utils.vmware.get_diskgroups(host_ref, + cache_disk_ids=[cache_disk_id]) + if not diskgroups: + raise VMwareObjectRetrievalError( + 'No diskgroup with cache disk id \'{0}\' was found in ESXi ' + 'host \'{1}\''.format(cache_disk_id, hostname)) + log.trace('data_evacuation = {0}'.format(data_evacuation)) + salt.utils.vsan.remove_capacity_from_diskgroup( + service_instance, host_ref, diskgroups[0], + capacity_disks=[d for d in disks + if d.canonicalName in capacity_disk_ids], + data_evacuation=data_evacuation) + return True + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From b3909ee4cc35eaec9d746ba7bd4b049dd086915c Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 06:13:57 -0400 Subject: [PATCH 394/633] Added salt.modules.vsphere.remove_diskgroup --- salt/modules/vsphere.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 5a25c4bed3..43c4884eec 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -6279,6 +6279,47 @@ def remove_capacity_from_diskgroup(cache_disk_id, capacity_disk_ids, return True +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def remove_diskgroup(cache_disk_id, data_accessibility=True, + service_instance=None): + ''' + Remove the diskgroup with the specified cache disk. + + cache_disk_id + The canonical name of the cache disk. + + data_accessibility + Specifies whether to ensure data accessibility. Default value is True. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.remove_diskgroup cache_disk_id='naa.000000000000001' + ''' + log.trace('Validating diskgroup input') + schema = DiskGroupsDiskIdSchema.serialize() + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + diskgroups = \ + salt.utils.vmware.get_diskgroups(host_ref, + cache_disk_ids=[cache_disk_id]) + if not diskgroups: + raise VMwareObjectRetrievalError( + 'No diskgroup with cache disk id \'{0}\' was found in ESXi ' + 'host \'{1}\''.format(cache_disk_id, hostname)) + log.trace('data accessibility = {0}'.format(data_accessibility)) + salt.utils.vsan.remove_diskgroup( + service_instance, host_ref, diskgroups[0], + data_accessibility=data_accessibility) + return True + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From 8bd7993e973c26e1528565784e07dd1b54710350 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 06:12:09 -0400 Subject: [PATCH 395/633] Added SimpleHostCacheSchema JSON schema --- salt/config/schemas/esxi.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/salt/config/schemas/esxi.py b/salt/config/schemas/esxi.py index 2a89418861..f2ad765f0c 100644 --- a/salt/config/schemas/esxi.py +++ b/salt/config/schemas/esxi.py @@ -18,6 +18,7 @@ from salt.utils.schema import (DefinitionsSchema, ComplexSchemaItem, ArrayItem, IntegerItem, + BooleanItem, StringItem) @@ -57,6 +58,22 @@ class DiskGroupsDiskIdSchema(DefinitionsSchema): required=True) +class SimpleHostCacheSchema(Schema): + ''' + Simplified Schema of ESXi host cache + ''' + + title = 'Simple Host Cache Schema' + description = 'Simplified schema of the ESXi host cache' + enabled = BooleanItem( + title='Enabled', + required=True) + datastore_name = StringItem(title='Datastore Name', + required=True) + swap_size_MiB = IntegerItem(title='Host cache swap size in MiB', + minimum=1) + + class EsxiProxySchema(Schema): ''' Schema of the esxi proxy input From 85388847044637de313fb5611486d66278bb528e Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 06:14:55 -0400 Subject: [PATCH 396/633] Added salt.modules.vsphere.get_host_cache --- salt/modules/vsphere.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 43c4884eec..631cf355fd 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -6320,6 +6320,37 @@ def remove_diskgroup(cache_disk_id, data_accessibility=True, return True +@depends(HAS_PYVMOMI) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def get_host_cache(service_instance=None): + ''' + Returns the host cache configuration on the proxy host. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.get_host_cache + ''' + # Default to getting all disks if no filtering is done + ret_dict = {} + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + hci = salt.utils.vmware.get_host_cache(host_ref) + if not hci: + log.debug('Host cache not configured on host \'{0}\''.format(hostname)) + ret_dict['enabled'] = False + return ret_dict + + # TODO Support multiple host cache info objects (on multiple datastores) + return {'enabled': True, + 'datastore': {'name': hci.key.name}, + 'swap_size': '{}MiB'.format(hci.swapSize)} + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From ea637743532243ab8c28e4761c73f39fd0ed7d0a Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 06:15:16 -0400 Subject: [PATCH 397/633] Added salt.modules.vsphere.configure_host_cache --- salt/modules/vsphere.py | 61 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 631cf355fd..00b78043a0 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -185,7 +185,8 @@ from salt.utils.decorators import depends, ignores_kwargs from salt.config.schemas.esxcluster import ESXClusterConfigSchema, \ ESXClusterEntitySchema from salt.config.schemas.vcenter import VCenterEntitySchema -from salt.config.schemas.esxi import DiskGroupsDiskIdSchema +from salt.config.schemas.esxi import DiskGroupsDiskIdSchema, \ + VmfsDatastoreSchema, SimpleHostCacheSchema # Import Third Party Libs try: @@ -6351,6 +6352,64 @@ def get_host_cache(service_instance=None): 'swap_size': '{}MiB'.format(hci.swapSize)} +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def configure_host_cache(enabled, datastore=None, swap_size_MiB=None, + service_instance=None): + ''' + Configures the host cache on the selected host. + + enabled + Boolean flag specifying whether the host cache is enabled. + + datastore + Name of the datastore that contains the host cache. Must be set if + enabled is ``true``. + + swap_size_MiB + Swap size in Mibibytes. Needs to be set if enabled is ``true``. Must be + smaller thant the datastore size. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.configure_host_cache enabled=False + + salt '*' vsphere.configure_host_cache enabled=True datastore=ds1 + swap_size_MiB=1024 + ''' + log.debug('Validating host cache input') + schema = SimpleHostCacheSchema.serialize() + try: + jsonschema.validate({'enabled': enabled, + 'datastore_name': datastore, + 'swap_size_MiB': swap_size_MiB}, + schema) + except jsonschema.exceptions.ValidationError as exc: + raise ArgumentValueError(exc) + if not enabled: + raise ArgumentValueError('Disabling the host cache is not supported') + ret_dict = {'enabled': False} + + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + if datastore: + ds_refs = salt.utils.vmware.get_datastores( + service_instance, host_ref, datastore_names=[datastore]) + if not ds_refs: + raise VMwareObjectRetrievalError( + 'Datastore \'{0}\' was not found on host ' + '\'{1}\''.format(datastore_name, hostname)) + ds_ref = ds_refs[0] + salt.utils.vmware.configure_host_cache(host_ref, ds_ref, swap_size_MiB) + return True + + def _check_hosts(service_instance, host, host_names): ''' Helper function that checks to see if the host provided is a vCenter Server or From 55e5a6ed21575991f2a390815a34614d6db7aca0 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 06:19:06 -0400 Subject: [PATCH 398/633] Added DiskGroupsDiskScsiAddressSchema JSON schema --- salt/config/schemas/esxi.py | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/salt/config/schemas/esxi.py b/salt/config/schemas/esxi.py index f2ad765f0c..8bea76f406 100644 --- a/salt/config/schemas/esxi.py +++ b/salt/config/schemas/esxi.py @@ -17,9 +17,36 @@ from salt.utils.schema import (DefinitionsSchema, Schema, ComplexSchemaItem, ArrayItem, + DictItem, IntegerItem, BooleanItem, - StringItem) + StringItem, + OneOfItem) + + +class VMwareScsiAddressItem(StringItem): + pattern = r'vmhba\d+:C\d+:T\d+:L\d+' + + +class DiskGroupDiskScsiAddressItem(ComplexSchemaItem): + ''' + Schema item of a ESXi host disk group containing disk SCSI addresses + ''' + + title = 'Diskgroup Disk Scsi Address Item' + description = 'ESXi host diskgroup item containing disk SCSI addresses' + + + cache_scsi_addr = VMwareScsiAddressItem( + title='Cache Disk Scsi Address', + description='Specifies the SCSI address of the cache disk', + required=True) + + capacity_scsi_addrs = ArrayItem( + title='Capacity Scsi Addresses', + description='Array with the SCSI addresses of the capacity disks', + items=VMwareScsiAddressItem(), + min_items=1) class DiskGroupDiskIdItem(ComplexSchemaItem): @@ -43,6 +70,24 @@ class DiskGroupDiskIdItem(ComplexSchemaItem): min_items=1) +class DiskGroupsDiskScsiAddressSchema(DefinitionsSchema): + ''' + Schema of ESXi host diskgroups containing disk SCSI addresses + ''' + + title = 'Diskgroups Disk Scsi Address Schema' + description = 'ESXi host diskgroup schema containing disk SCSI addresses' + disk_groups = ArrayItem( + title='Diskgroups', + description='List of diskgroups in an ESXi host', + min_items = 1, + items=DiskGroupDiskScsiAddressItem(), + required=True) + erase_disks = BooleanItem( + title='Erase Diskgroup Disks', + required=True) + + class DiskGroupsDiskIdSchema(DefinitionsSchema): ''' Schema of ESXi host diskgroups containing disk ids From 23e2fd3aefaa7cbd24026c227964eb2826a826dd Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 06:20:50 -0400 Subject: [PATCH 399/633] Added VmfsDatastoreSchema and HostCacheSchema JSON schemas used in host cache state functions --- salt/config/schemas/esxi.py | 80 ++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/salt/config/schemas/esxi.py b/salt/config/schemas/esxi.py index 8bea76f406..5c8b7596c3 100644 --- a/salt/config/schemas/esxi.py +++ b/salt/config/schemas/esxi.py @@ -77,7 +77,7 @@ class DiskGroupsDiskScsiAddressSchema(DefinitionsSchema): title = 'Diskgroups Disk Scsi Address Schema' description = 'ESXi host diskgroup schema containing disk SCSI addresses' - disk_groups = ArrayItem( + diskgroups = ArrayItem( title='Diskgroups', description='List of diskgroups in an ESXi host', min_items = 1, @@ -103,6 +103,84 @@ class DiskGroupsDiskIdSchema(DefinitionsSchema): required=True) +class VmfsDatastoreDiskIdItem(ComplexSchemaItem): + ''' + Schema item of a VMFS datastore referencing a backing disk id + ''' + + title = 'VMFS Datastore Disk Id Item' + description = 'VMFS datastore item referencing a backing disk id' + name = StringItem( + title='Name', + description='Specifies the name of the VMFS datastore', + required=True) + backing_disk_id = StringItem( + title='Backing Disk Id', + description=('Specifies the id of the disk backing the VMFS ' + 'datastore'), + pattern=r'[^\s]+', + required=True) + vmfs_version = IntegerItem( + title='VMFS Version', + description='VMFS version', + enum=[1, 2, 3, 5]) + + +class VmfsDatastoreDiskScsiAddressItem(ComplexSchemaItem): + ''' + Schema item of a VMFS datastore referencing a backing disk SCSI address + ''' + + title = 'VMFS Datastore Disk Scsi Address Item' + description = 'VMFS datastore item referencing a backing disk SCSI address' + name = StringItem( + title='Name', + description='Specifies the name of the VMFS datastore', + required=True) + backing_disk_scsi_addr = VMwareScsiAddressItem( + title='Backing Disk Scsi Address', + description=('Specifies the SCSI address of the disk backing the VMFS ' + 'datastore'), + required=True) + vmfs_version = IntegerItem( + title='VMFS Version', + description='VMFS version', + enum=[1, 2, 3, 5]) + + +class VmfsDatastoreSchema(DefinitionsSchema): + ''' + Schema of a VMFS datastore + ''' + + title = 'VMFS Datastore Schema' + description = 'Schema of a VMFS datastore' + datastore = OneOfItem( + items=[VmfsDatastoreDiskScsiAddressItem(), + VmfsDatastoreDiskIdItem()], + required=True) + + +class HostCacheSchema(DefinitionsSchema): + ''' + Schema of ESXi host cache + ''' + + title = 'Host Cache Schema' + description = 'Schema of the ESXi host cache' + enabled = BooleanItem( + title='Enabled', + required=True) + datastore = VmfsDatastoreDiskScsiAddressItem(required=True) + swap_size = StringItem( + title='Host cache swap size (in GB or %)', + pattern=r'(\d+GiB)|(([0-9]|([1-9][0-9])|100)%)', + required=True) + erase_backing_disk = BooleanItem( + title='Erase Backup Disk', + required=True) + + class SimpleHostCacheSchema(Schema): ''' Simplified Schema of ESXi host cache From 8e58f72964839029347582f95cca08ed82ffb486 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 11:02:04 -0400 Subject: [PATCH 400/633] Added salt.modules.vsphere.create_vmfs_datastore --- salt/modules/vsphere.py | 56 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 00b78043a0..ce29c9e1a1 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -5563,6 +5563,60 @@ def list_datastores_via_proxy(datastore_names=None, backing_disk_ids=None, return ret_dict +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +@supports_proxies('esxi') +@gets_service_instance_via_proxy +def create_vmfs_datastore(datastore_name, disk_id, vmfs_major_version, + safety_checks=True, service_instance=None): + ''' + Creates a ESXi host disk group with the specified cache and capacity disks. + + datastore_name + The name of the datastore to be created. + + disk_id + The disk id (canonical name) on which the datastore is created. + + vmfs_major_version + The VMFS major version. + + safety_checks + Specify whether to perform safety check or to skip the checks and try + performing the required task. Default is True. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.create_vmfs_datastore datastore_name=ds1 disk_id= + vmfs_major_version=5 + ''' + log.debug('Validating vmfs datastore input') + schema = VmfsDatastoreSchema.serialize() + try: + jsonschema.validate( + {'datastore': {'name': datastore_name, + 'backing_disk_id': disk_id, + 'vmfs_version': vmfs_major_version}}, + schema) + except jsonschema.exceptions.ValidationError as exc: + raise ArgumentValueError(exc) + host_ref = _get_proxy_target(service_instance) + hostname = __proxy__['esxi.get_details']()['esxi_host'] + if safety_checks: + disks = salt.utils.vmware.get_disks(host_ref, disk_ids=[disk_id]) + if not disks: + raise VMwareObjectRetrievalError( + 'Disk \'{0}\' was not found in host \'{1}\''.format(disk_id, + hostname)) + ds_ref = salt.utils.vmware.create_vmfs_datastore( + host_ref, datastore_name, disks[0], vmfs_major_version) + return True + + @depends(HAS_PYVMOMI) @supports_proxies('esxi', 'esxcluster', 'esxdatacenter') @gets_service_instance_via_proxy @@ -6207,7 +6261,7 @@ def add_capacity_to_diskgroup(cache_disk_id, capacity_disk_ids, salt.utils.vsan.add_capacity_to_diskgroup(service_instance, vsan_disk_mgmt_system, host_ref, - disk_groups[0], + diskgroups[0], disks) return True From 29b59a62569c27df36ce09e16991c8abd805b72a Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 11:02:41 -0400 Subject: [PATCH 401/633] Comment fix in salt.modules.vsphere.rename_datastore --- salt/modules/vsphere.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index ce29c9e1a1..97449b4693 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -5984,7 +5984,7 @@ def erase_disk_partitions(disk_id=None, scsi_address=None, Either ``disk_id`` or ``scsi_address`` needs to be specified (``disk_id`` supersedes ``scsi_address``. - scsi_address` + scsi_address Scsi address of the disk. ``disk_id`` or ``scsi_address`` needs to be specified (``disk_id`` supersedes ``scsi_address``. From fa6460d578463259fbfe7a8631b0e267a73b389f Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 26 Sep 2017 07:59:08 -0400 Subject: [PATCH 402/633] Added salt.modules.vsphere.remove_datastore --- salt/modules/vsphere.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 97449b4693..0bbf1936b9 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -5655,6 +5655,41 @@ def rename_datastore(datastore_name, new_datastore_name, return True +@depends(HAS_PYVMOMI) +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') +@gets_service_instance_via_proxy +def remove_datastore(datastore, service_instance=None): + ''' + Removes a datastore. If multiple datastores an error is raised. + + datastore + Datastore name + + service_instance + Service instance (vim.ServiceInstance) of the vCenter/ESXi host. + Default is None. + + .. code-block:: bash + + salt '*' vsphere.remove_datastore ds_name + ''' + log.trace('Removing datastore \'{0}\''.format(datastore)) + target = _get_proxy_target(service_instance) + taget_name = target.name + datastores = salt.utils.vmware.get_datastores( + service_instance, + reference=target, + datastore_names=[datastore]) + if not datastores: + raise VMwareObjectRetrievalError( + 'Datastore \'{0}\' was not found'.format(datastore)) + if len(datastores) > 1: + raise VMwareObjectRetrievalError( + 'Multiple datastores \'{0}\' were found'.format(datastore)) + salt.utils.vmware.remove_datastore(service_instance, datastores[0]) + return True + + @depends(HAS_PYVMOMI) @supports_proxies('esxcluster', 'esxdatacenter') @gets_service_instance_via_proxy From 7d70a014f274eedc4dc905ea8a75e44612d4b252 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 18:07:30 -0400 Subject: [PATCH 403/633] Moved pyVmomi/python incompatibility check from __virtual__ to pyVmomi import as some functions do not use pyVmomi --- salt/modules/vsphere.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 0bbf1936b9..21b5426445 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -188,6 +188,8 @@ from salt.config.schemas.vcenter import VCenterEntitySchema from salt.config.schemas.esxi import DiskGroupsDiskIdSchema, \ VmfsDatastoreSchema, SimpleHostCacheSchema +log = logging.getLogger(__name__) + # Import Third Party Libs try: import jsonschema @@ -197,6 +199,14 @@ except ImportError: try: from pyVmomi import vim, vmodl, pbm, VmomiSupport + + # We check the supported vim versions to infer the pyVmomi version + if 'vim25/6.0' in VmomiSupport.versionMap and \ + sys.version_info > (2, 7) and sys.version_info < (2, 7, 9): + + log.error('pyVmomi not loaded: Incompatible versions ' + 'of Python. See Issue #29537.') + raise ImportError() HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False @@ -207,24 +217,11 @@ if esx_cli: else: HAS_ESX_CLI = False -log = logging.getLogger(__name__) - __virtualname__ = 'vsphere' __proxyenabled__ = ['esxi', 'esxcluster', 'esxdatacenter', 'vcenter'] def __virtual__(): - if not HAS_JSONSCHEMA: - return False, 'Execution module did not load: jsonschema not found' - if not HAS_PYVMOMI: - return False, 'Execution module did not load: pyVmomi not found' - - # We check the supported vim versions to infer the pyVmomi version - if 'vim25/6.0' in VmomiSupport.versionMap and \ - sys.version_info > (2, 7) and sys.version_info < (2, 7, 9): - - return False, ('Execution module did not load: Incompatible versions ' - 'of Python and pyVmomi present. See Issue #29537.') return __virtualname__ From 152ce0b691bf54d2e52a125488379a5cf8f04677 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 18:08:43 -0400 Subject: [PATCH 404/633] Added salt.states.esxi additional imports and pyVmomi/python compatibility check --- salt/states/esxi.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/salt/states/esxi.py b/salt/states/esxi.py index 12240422e4..fa6ba13df3 100644 --- a/salt/states/esxi.py +++ b/salt/states/esxi.py @@ -90,20 +90,46 @@ ESXi Proxy Minion, please refer to the configuration examples, dependency installation instructions, how to run remote execution functions against ESXi hosts via a Salt Proxy Minion, and a larger state example. - ''' # Import Python Libs from __future__ import absolute_import import logging +import sys +import re # Import Salt Libs from salt.ext import six import salt.utils.files -from salt.exceptions import CommandExecutionError +from salt.exceptions import CommandExecutionError, InvalidConfigError, \ + VMwareObjectRetrievalError, VMwareSaltError, VMwareApiError +from salt.utils.decorators import depends +from salt.config.schemas.esxi import DiskGroupsDiskScsiAddressSchema, \ + HostCacheSchema + +# External libraries +try: + import jsonschema + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False # Get Logging Started log = logging.getLogger(__name__) +try: + from pyVmomi import vim, vmodl, VmomiSupport + + # We check the supported vim versions to infer the pyVmomi version + if 'vim25/6.0' in VmomiSupport.versionMap and \ + sys.version_info > (2, 7) and sys.version_info < (2, 7, 9): + + log.error('pyVmomi not loaded: Incompatible versions ' + 'of Python. See Issue #29537.') + raise ImportError() + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + def __virtual__(): return 'esxi.cmd' in __salt__ From 1423d9dfb999cf480a5976056a790267ce7e4e35 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 18:10:38 -0400 Subject: [PATCH 405/633] Added diskgroups_configured state that configures VSAN diskgroups on ESXi hosts --- salt/states/esxi.py | 276 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/salt/states/esxi.py b/salt/states/esxi.py index fa6ba13df3..208af27a8f 100644 --- a/salt/states/esxi.py +++ b/salt/states/esxi.py @@ -1024,6 +1024,282 @@ def syslog_configured(name, return ret +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +def diskgroups_configured(name, diskgroups, erase_disks=False): + ''' + Configures the disk groups to use for vsan. + + It will do the following: + (1) checks for if all disks in the diskgroup spec exist and errors if they + don't + (2) creates diskgroups with the correct disk configurations if diskgroup + (identified by the cache disk canonical name) doesn't exist + (3) adds extra capacity disks to the existing diskgroup + + State input example + ------------------- + + .. code:: python + + { + 'cache_scsi_addr': 'vmhba1:C0:T0:L0', + 'capacity_scsi_addrs': [ + 'vmhba2:C0:T0:L0', + 'vmhba3:C0:T0:L0', + 'vmhba4:C0:T0:L0', + ] + } + + name + Mandatory state name. + + diskgroups + Disk group representation containing scsi disk addresses. + Scsi addresses are expected for disks in the diskgroup: + + erase_disks + Specifies whether to erase all partitions on all disks member of the + disk group before the disk group is created. Default vaule is False. + ''' + proxy_details = __salt__['esxi.get_details']() + hostname = proxy_details['host'] if not proxy_details.get('vcenter') \ + else proxy_details['esxi_host'] + log.info('Running state {0} for host \'{1}\''.format(name, hostname)) + # Variable used to return the result of the invocation + ret = {'name': name, 'result': None, 'changes': {}, + 'pchanges': {}, 'comments': None} + # Signals if errors have been encountered + errors = False + # Signals if changes are required + changes = False + comments = [] + diskgroup_changes = {} + si = None + try: + log.trace('Validating diskgroups_configured input') + schema = DiskGroupsDiskScsiAddressSchema.serialize() + try: + jsonschema.validate({'diskgroups': diskgroups, + 'erase_disks': erase_disks}, schema) + except jsonschema.exceptions.ValidationError as exc: + raise InvalidConfigError(exc) + si = __salt__['vsphere.get_service_instance_via_proxy']() + host_disks = __salt__['vsphere.list_disks'](service_instance=si) + if not host_disks: + raise VMwareObjectRetrievalError( + 'No disks retrieved from host \'{0}\''.format(hostname)) + scsi_addr_to_disk_map = {d['scsi_address']: d for d in host_disks} + log.trace('scsi_addr_to_disk_map = {0}'.format(scsi_addr_to_disk_map)) + existing_diskgroups = \ + __salt__['vsphere.list_diskgroups'](service_instance=si) + cache_disk_to_existing_diskgroup_map = \ + {dg['cache_disk']: dg for dg in existing_diskgroups} + except CommandExecutionError as err: + log.error('Error: {0}'.format(err)) + if si: + __salt__['vsphere.disconnect'](si) + ret.update({ + 'result': False if not __opts__['test'] else None, + 'comment': str(err)}) + return ret + + # Iterate through all of the disk groups + for idx, dg in enumerate(diskgroups): + # Check for cache disk + if not dg['cache_scsi_addr'] in scsi_addr_to_disk_map: + comments.append('No cache disk with scsi address \'{0}\' was ' + 'found.'.format(dg['cache_scsi_addr'])) + log.error(comments[-1]) + errors = True + continue + + # Check for capacity disks + cache_disk_id = scsi_addr_to_disk_map[dg['cache_scsi_addr']]['id'] + cache_disk_display = '{0} (id:{1})'.format(dg['cache_scsi_addr'], + cache_disk_id) + bad_scsi_addrs = [] + capacity_disk_ids = [] + capacity_disk_displays = [] + for scsi_addr in dg['capacity_scsi_addrs']: + if not scsi_addr in scsi_addr_to_disk_map: + bad_scsi_addrs.append(scsi_addr) + continue + capacity_disk_ids.append(scsi_addr_to_disk_map[scsi_addr]['id']) + capacity_disk_displays.append( + '{0} (id:{1})'.format(scsi_addr, capacity_disk_ids[-1])) + if bad_scsi_addrs: + comments.append('Error in diskgroup #{0}: capacity disks with ' + 'scsi addresses {1} were not found.' + ''.format(idx, + ', '.join(['\'{0}\''.format(a) + for a in bad_scsi_addrs]))) + log.error(comments[-1]) + errors = True + continue + + if not cache_disk_to_existing_diskgroup_map.get(cache_disk_id): + # A new diskgroup needs to be created + log.trace('erase_disks = {0}'.format(erase_disks)) + if erase_disks: + if __opts__['test']: + comments.append('State {0} will ' + 'erase all disks of disk group #{1}; ' + 'cache disk: \'{2}\', ' + 'capacity disk(s): {3}.' + ''.format(name, idx, cache_disk_display, + ', '.join( + ['\'{}\''.format(a) for a in + capacity_disk_displays]))) + else: + # Erase disk group disks + for disk_id in ([cache_disk_id] + capacity_disk_ids): + __salt__['vsphere.erase_disk_partitions']( + disk_id=disk_id, service_instance=si) + comments.append('Erased disks of diskgroup #{0}; ' + 'cache disk: \'{1}\', capacity disk(s): ' + '{2}'.format( + idx, cache_disk_display, + ', '.join(['\'{0}\''.format(a) for a in + capacity_disk_displays]))) + log.info(comments[-1]) + + if __opts__['test']: + comments.append('State {0} will create ' + 'the disk group #{1}; cache disk: \'{2}\', ' + 'capacity disk(s): {3}.' + .format(name, idx, cache_disk_display, + ', '.join(['\'{0}\''.format(a) for a in + capacity_disk_displays]))) + log.info(comments[-1]) + changes = True + continue + try: + __salt__['vsphere.create_diskgroup'](cache_disk_id, + capacity_disk_ids, + safety_checks=False, + service_instance=si) + except VMwareSaltError as err: + comments.append('Error creating disk group #{0}: ' + '{1}.'.format(idx, err)) + log.error(comments[-1]) + errors = True + continue + + comments.append('Created disk group #\'{0}\'.'.format(idx)) + log.info(comments[-1]) + diskgroup_changes[str(idx)] = \ + {'new': {'cache': cache_disk_display, + 'capacity': capacity_disk_displays}} + changes = True + continue + + # The diskgroup exists; checking the capacity disks + log.debug('Disk group #{0} exists. Checking capacity disks: ' + '{1}.'.format(idx, capacity_disk_displays)) + existing_diskgroup = \ + cache_disk_to_existing_diskgroup_map.get(cache_disk_id) + existing_capacity_disk_displays = \ + ['{0} (id:{1})'.format([d['scsi_address'] for d in host_disks + if d['id'] == disk_id][0], disk_id) + for disk_id in existing_diskgroup['capacity_disks']] + # Populate added disks and removed disks and their displays + added_capacity_disk_ids = [] + added_capacity_disk_displays = [] + removed_capacity_disk_ids = [] + removed_capacity_disk_displays = [] + for disk_id in capacity_disk_ids: + if disk_id not in existing_diskgroup['capacity_disks']: + disk_scsi_addr = [d['scsi_address'] for d in host_disks + if d['id'] == disk_id][0] + added_capacity_disk_ids.append(disk_id) + added_capacity_disk_displays.append( + '{0} (id:{1})'.format(disk_scsi_addr, disk_id)) + for disk_id in existing_diskgroup['capacity_disks']: + if disk_id not in capacity_disk_ids: + disk_scsi_addr = [d['scsi_address'] for d in host_disks + if d['id'] == disk_id][0] + removed_capacity_disk_ids.append(disk_id) + removed_capacity_disk_displays.append( + '{0} (id:{1})'.format(disk_scsi_addr, disk_id)) + + log.debug('Disk group #{0}: existing capacity disk ids: {1}; added ' + 'capacity disk ids: {2}; removed capacity disk ids: {3}' + ''.format(idx, existing_capacity_disk_displays, + added_capacity_disk_displays, + removed_capacity_disk_displays)) + + #TODO revisit this when removing capacity disks is supported + if removed_capacity_disk_ids: + comments.append( + 'Error removing capacity disk(s) {0} from disk group #{1}; ' + 'operation is not supported.' + ''.format(', '.join(['\'{0}\''.format(id) for id in + removed_capacity_disk_displays]), idx)) + log.error(comments[-1]) + errors = True + continue + + if added_capacity_disk_ids: + # Capacity disks need to be added to disk group + + # Building a string representation of the capacity disks + # that need to be added + s = ', '.join(['\'{0}\''.format(id) for id in + added_capacity_disk_displays]) + if __opts__['test']: + comments.append('State {0} will add ' + 'capacity disk(s) {1} to disk group #{2}.' + ''.format(name, s, idx)) + log.info(comments[-1]) + changes = True + continue + try: + __salt__['vsphere.add_capacity_to_diskgroup']( + cache_disk_id, + added_capacity_disk_ids, + safety_checks=False, + service_instance=si) + except VMwareSaltError as err: + comments.append('Error adding capacity disk(s) {0} to ' + 'disk group #{1}: {2}.'.format(s, idx, err)) + log.error(comments[-1]) + errors = True + continue + + com = ('Added capacity disk(s) {0} to disk group #{1}' + ''.format(s, idx)) + log.info(com) + comments.append(com) + diskgroup_changes[str(idx)] = \ + {'new': {'cache': cache_disk_display, + 'capacity': capacity_disk_displays}, + 'old': {'cache': cache_disk_display, + 'capacity': existing_capacity_disk_displays}} + changes = True + continue + + # No capacity needs to be added + s = ('Disk group #{0} is correctly configured. Nothing to be done.' + ''.format(idx)) + log.info(s) + comments.append(s) + __salt__['vsphere.disconnect'](si) + + #Build the final return message + result = (True if not (changes or errors) else # no changes/errors + None if __opts__['test'] else # running in test mode + False if errors else True) # found errors; defaults to True + ret.update({'result': result, + 'comment': '\n'.join(comments)}) + if changes: + if __opts__['test']: + ret['pchanges'] = diskgroup_changes + elif changes: + ret['changes'] = diskgroup_changes + return ret + + def _lookup_syslog_config(config): ''' Helper function that looks up syslog_config keys available from From c1d36f53c2594d88dbb7efcc24c4ffb1d16d5d63 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Mon, 25 Sep 2017 18:11:47 -0400 Subject: [PATCH 406/633] Added host_cache_configured state that configures the host cache on ESXi hosts --- salt/states/esxi.py | 298 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) diff --git a/salt/states/esxi.py b/salt/states/esxi.py index 208af27a8f..c94daef37b 100644 --- a/salt/states/esxi.py +++ b/salt/states/esxi.py @@ -1300,6 +1300,304 @@ def diskgroups_configured(name, diskgroups, erase_disks=False): return ret +@depends(HAS_PYVMOMI) +@depends(HAS_JSONSCHEMA) +def host_cache_configured(name, enabled, datastore, swap_size='100%', + dedicated_backing_disk=False, + erase_backing_disk=False): + ''' + Configures the host cache used for swapping. + + It will do the following: + (1) checks if backing disk exists + (2) creates the VMFS datastore if doesn't exist (datastore partition will + be created and use the entire disk + (3) raises an error if dedicated_backing_disk is True and partitions + already exist on the backing disk + (4) configures host_cache to use a portion of the datastore for caching + (either a specific size or a percentage of the datastore) + + State input examples + -------------------- + + Percentage swap size (can't be 100%) + + .. code:: python + + { + 'enabled': true, + 'datastore': { + 'backing_disk_scsi_addr': 'vmhba0:C0:T0:L0', + 'vmfs_version': 5, + 'name': 'hostcache' + } + 'dedicated_backing_disk': false + 'swap_size': '98%', + } + + + .. code:: python + + Fixed sized swap size + + { + 'enabled': true, + 'datastore': { + 'backing_disk_scsi_addr': 'vmhba0:C0:T0:L0', + 'vmfs_version': 5, + 'name': 'hostcache' + } + 'dedicated_backing_disk': true + 'swap_size': '10GiB', + } + + name + Mandatory state name. + + enabled + Specifies whether the host cache is enabled. + + datastore + Specifies the host cache datastore. + + swap_size + Specifies the size of the host cache swap. Can be a percentage or a + value in GiB. Default value is ``100%``. + + dedicated_backing_disk + Specifies whether the backing disk is dedicated to the host cache which + means it must have no other partitions. Default is False + + erase_backing_disk + Specifies whether to erase all partitions on the backing disk before + the datastore is created. Default vaule is False. + ''' + log.trace('enabled = {0}'.format(enabled)) + log.trace('datastore = {0}'.format(datastore)) + log.trace('swap_size = {0}'.format(swap_size)) + log.trace('erase_backing_disk = {0}'.format(erase_backing_disk)) + # Variable used to return the result of the invocation + proxy_details = __salt__['esxi.get_details']() + hostname = proxy_details['host'] if not proxy_details.get('vcenter') \ + else proxy_details['esxi_host'] + log.trace('hostname = {0}'.format(hostname)) + log.info('Running host_cache_swap_configured for host ' + '\'{0}\''.format(hostname)) + ret = {'name': hostname, 'comment': 'Default comments', + 'result': None, 'changes': {}, 'pchanges': {}} + result = None if __opts__['test'] else True #We assume success + needs_setting = False + comments = [] + changes = {} + si = None + try: + log.debug('Validating host_cache_configured input') + schema = HostCacheSchema.serialize() + try: + jsonschema.validate({'enabled': enabled, + 'datastore': datastore, + 'swap_size': swap_size, + 'erase_backing_disk': erase_backing_disk}, + schema) + except jsonschema.exceptions.ValidationError as exc: + raise InvalidConfigError(exc) + m = re.match(r'(\d+)(%|GiB)', swap_size) + swap_size_value = int(m.group(1)) + swap_type = m.group(2) + log.trace('swap_size_value = {0}; swap_type = {1}'.format( + swap_size_value, swap_type)) + si = __salt__['vsphere.get_service_instance_via_proxy']() + host_cache = __salt__['vsphere.get_host_cache'](service_instance=si) + + # Check enabled + if host_cache['enabled'] != enabled: + changes.update({'enabled': {'old': host_cache['enabled'], + 'new': enabled}}) + needs_setting = True + + + # Check datastores + existing_datastores = None + if host_cache.get('datastore'): + existing_datastores = \ + __salt__['vsphere.list_datastores_via_proxy']( + datastore_names=[datastore['name']], + service_instance=si) + # Retrieve backing disks + existing_disks = __salt__['vsphere.list_disks']( + scsi_addresses=[datastore['backing_disk_scsi_addr']], + service_instance=si) + if not existing_disks: + raise VMwareObjectRetrievalError( + 'Disk with scsi address \'{0}\' was not found in host \'{1}\'' + ''.format(datastore['backing_disk_scsi_addr'], hostname)) + backing_disk = existing_disks[0] + backing_disk_display = '{0} (id:{1})'.format( + backing_disk['scsi_address'], backing_disk['id']) + log.trace('backing_disk = {0}'.format(backing_disk_display)) + + existing_datastore = None + if not existing_datastores: + # Check if disk needs to be erased + if erase_backing_disk: + if __opts__['test']: + comments.append('State {0} will erase ' + 'the backing disk \'{1}\' on host \'{2}\'.' + ''.format(name, backing_disk_display, + hostname)) + log.info(comments[-1]) + else: + # Erase disk + __salt__['vsphere.erase_disk_partitions']( + disk_id=backing_disk['id'], service_instance=si) + comments.append('Erased backing disk \'{0}\' on host ' + '\'{1}\'.'.format(backing_disk_display, + hostname)) + log.info(comments[-1]) + # Create the datastore + if __opts__['test']: + comments.append('State {0} will create ' + 'the datastore \'{1}\', with backing disk ' + '\'{2}\', on host \'{3}\'.' + ''.format(name, datastore['name'], + backing_disk_display, hostname)) + log.info(comments[-1]) + else: + if dedicated_backing_disk: + # Check backing disk doesn't already have partitions + partitions = __salt__['vsphere.list_disk_partitions']( + disk_id=backing_disk['id'], service_instance=si) + log.trace('partitions = {0}'.format(partitions)) + # We will ignore the mbr partitions + non_mbr_partitions = [p for p in partitions + if p['format'] != 'mbr'] + if len(non_mbr_partitions) > 0: + raise VMwareApiError( + 'Backing disk \'{0}\' has unexpected partitions' + ''.format(backing_disk_display)) + __salt__['vsphere.create_vmfs_datastore']( + datastore['name'], existing_disks[0]['id'], + datastore['vmfs_version'], service_instance=si) + comments.append('Created vmfs datastore \'{0}\', backed by ' + 'disk \'{1}\', on host \'{2}\'.' + ''.format(datastore['name'], + backing_disk_display, hostname)) + log.info(comments[-1]) + changes.update( + {'datastore': + {'new': {'name': datastore['name'], + 'backing_disk': backing_disk_display}}}) + existing_datastore = \ + __salt__['vsphere.list_datastores_via_proxy']( + datastore_names=[datastore['name']], + service_instance=si)[0] + needs_setting = True + else: + # Check datastore is backed by the correct disk + if not existing_datastores[0].get('backing_disk_ids'): + raise VMwareSaltError('Datastore \'{0}\' doesn\'t have a ' + 'backing disk' + ''.format(datastore['name'])) + if backing_disk['id'] not in \ + existing_datastores[0]['backing_disk_ids']: + + raise VMwareSaltError( + 'Datastore \'{0}\' is not backed by the correct disk: ' + 'expected \'{1}\'; got {2}' + ''.format( + datastore['name'], backing_disk['id'], + ', '.join( + ['\'{0}\''.format(disk) for disk in + existing_datastores[0]['backing_disk_ids']]))) + + comments.append('Datastore \'{0}\' already exists on host \'{1}\' ' + 'and is backed by disk \'{2}\'. Nothing to be ' + 'done.'.format(datastore['name'], hostname, + backing_disk_display)) + existing_datastore = existing_datastores[0] + log.trace('existing_datastore = {0}'.format(existing_datastore)) + log.info(comments[-1]) + + + if existing_datastore: + # The following comparisons can be done if the existing_datastore + # is set; it may not be set if running in test mode + # + # We support percent, as well as MiB, we will convert the size + # to MiB, multiples of 1024 (VMware SDK limitation) + if swap_type == '%': + # Percentage swap size + # Convert from bytes to MiB + raw_size_MiB = (swap_size_value/100.0) * \ + (existing_datastore['capacity']/1024/1024) + else: + raw_size_MiB = swap_size_value * 1024 + log.trace('raw_size = {0}MiB'.format(raw_size_MiB)) + swap_size_MiB= int(raw_size_MiB/1024)*1024 + log.trace('adjusted swap_size = {0}MiB'.format(swap_size_MiB)) + existing_swap_size_MiB = 0 + m = re.match('(\d+)MiB', host_cache.get('swap_size')) if \ + host_cache.get('swap_size') else None + if m: + # if swap_size from the host is set and has an expected value + # we are going to parse it to get the number of MiBs + existing_swap_size_MiB = int(m.group(1)) + if not (existing_swap_size_MiB == swap_size_MiB): + needs_setting = True + changes.update( + {'swap_size': + {'old': '{}GiB'.format(existing_swap_size_MiB/1024), + 'new': '{}GiB'.format(swap_size_MiB/1024)}}) + + + if needs_setting: + if __opts__['test']: + comments.append('State {0} will configure ' + 'the host cache on host \'{1}\' to: {2}.' + ''.format(name, hostname, + {'enabled': enabled, + 'datastore_name': datastore['name'], + 'swap_size': swap_size})) + else: + if (existing_datastore['capacity'] / 1024.0**2) < \ + swap_size_MiB: + + raise ArgumentValueError( + 'Capacity of host cache datastore \'{0}\' ({1} MiB) is ' + 'smaller than the required swap size ({2} MiB)' + ''.format(existing_datastore['name'], + existing_datastore['capacity'] / 1024.0**2, + swap_size_MiB)) + __salt__['vsphere.configure_host_cache']( + enabled, + datastore['name'], + swap_size_MiB=swap_size_MiB, + service_instance=si) + comments.append('Host cache configured on host ' + '\'{0}\'.'.format(hostname)) + else: + comments.append('Host cache on host \'{0}\' is already correctly ' + 'configured. Nothing to be done.'.format(hostname)) + result = True + __salt__['vsphere.disconnect'](si) + log.info(comments[-1]) + ret.update({'comment': '\n'.join(comments), + 'result': result}) + if __opts__['test']: + ret['pchanges'] = changes + else: + ret['changes'] = changes + return ret + except CommandExecutionError as err: + log.error('Error: {0}.'.format(err)) + if si: + __salt__['vsphere.disconnect'](si) + ret.update({ + 'result': False if not __opts__['test'] else None, + 'comment': '{}.'.format(err)}) + return ret + + def _lookup_syslog_config(config): ''' Helper function that looks up syslog_config keys available from From ac3a3bdda50435267bb0e8c84af0e7ec3315145a Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 26 Sep 2017 17:25:29 -0400 Subject: [PATCH 407/633] Added VMwareObjectExistsError exception --- salt/exceptions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/salt/exceptions.py b/salt/exceptions.py index db93362c0f..7215112ea3 100644 --- a/salt/exceptions.py +++ b/salt/exceptions.py @@ -442,6 +442,12 @@ class VMwareObjectRetrievalError(VMwareSaltError): ''' +class VMwareObjectExistsError(VMwareSaltError): + ''' + Used when a VMware object exists + ''' + + class VMwareObjectNotFoundError(VMwareSaltError): ''' Used when a VMware object was not found From 951d43e0a9e6d7663563d0de7283305a6e165fef Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 26 Sep 2017 17:28:06 -0400 Subject: [PATCH 408/633] pylint --- salt/config/schemas/esxi.py | 11 ++++------- salt/modules/vsphere.py | 12 ++++++------ salt/proxy/esxi.py | 20 +++++++++++--------- salt/states/esxi.py | 22 ++++++++++------------ salt/utils/vmware.py | 11 ++++++----- salt/utils/vsan.py | 5 +++-- 6 files changed, 40 insertions(+), 41 deletions(-) diff --git a/salt/config/schemas/esxi.py b/salt/config/schemas/esxi.py index 5c8b7596c3..4520321c36 100644 --- a/salt/config/schemas/esxi.py +++ b/salt/config/schemas/esxi.py @@ -17,7 +17,6 @@ from salt.utils.schema import (DefinitionsSchema, Schema, ComplexSchemaItem, ArrayItem, - DictItem, IntegerItem, BooleanItem, StringItem, @@ -36,7 +35,6 @@ class DiskGroupDiskScsiAddressItem(ComplexSchemaItem): title = 'Diskgroup Disk Scsi Address Item' description = 'ESXi host diskgroup item containing disk SCSI addresses' - cache_scsi_addr = VMwareScsiAddressItem( title='Cache Disk Scsi Address', description='Specifies the SCSI address of the cache disk', @@ -57,7 +55,6 @@ class DiskGroupDiskIdItem(ComplexSchemaItem): title = 'Diskgroup Disk Id Item' description = 'ESXi host diskgroup item containing disk ids' - cache_id = StringItem( title='Cache Disk Id', description='Specifies the id of the cache disk', @@ -80,7 +77,7 @@ class DiskGroupsDiskScsiAddressSchema(DefinitionsSchema): diskgroups = ArrayItem( title='Diskgroups', description='List of diskgroups in an ESXi host', - min_items = 1, + min_items=1, items=DiskGroupDiskScsiAddressItem(), required=True) erase_disks = BooleanItem( @@ -98,7 +95,7 @@ class DiskGroupsDiskIdSchema(DefinitionsSchema): diskgroups = ArrayItem( title='DiskGroups', description='List of disk groups in an ESXi host', - min_items = 1, + min_items=1, items=DiskGroupDiskIdItem(), required=True) @@ -207,8 +204,8 @@ class EsxiProxySchema(Schema): additional_properties = False proxytype = StringItem(required=True, enum=['esxi']) - host = StringItem(pattern=r'[^\s]+') # Used when connecting directly - vcenter = StringItem(pattern=r'[^\s]+') # Used when connecting via a vCenter + host = StringItem(pattern=r'[^\s]+') # Used when connecting directly + vcenter = StringItem(pattern=r'[^\s]+') # Used when connecting via a vCenter esxi_host = StringItem() username = StringItem() passwords = ArrayItem(min_items=1, diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index 21b5426445..aad667d124 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -180,7 +180,7 @@ import salt.utils.vsan import salt.utils.pbm from salt.exceptions import CommandExecutionError, VMwareSaltError, \ ArgumentValueError, InvalidConfigError, VMwareObjectRetrievalError, \ - VMwareApiError, InvalidEntityError + VMwareApiError, InvalidEntityError, VMwareObjectExistsError from salt.utils.decorators import depends, ignores_kwargs from salt.config.schemas.esxcluster import ESXClusterConfigSchema, \ ESXClusterEntitySchema @@ -5992,7 +5992,7 @@ def list_disks(disk_ids=None, scsi_addresses=None, service_instance=None): host_ref, hostname=hostname) canonical_name_to_scsi_address = { lun.canonicalName: scsi_addr - for scsi_addr, lun in scsi_address_to_lun.iteritems()} + for scsi_addr, lun in six.iteritems(scsi_address_to_lun)} for d in salt.utils.vmware.get_disks(host_ref, disk_ids, scsi_addresses, get_all_disks): ret_list.append({'id': d.canonicalName, @@ -6052,7 +6052,7 @@ def erase_disk_partitions(disk_id=None, scsi_address=None, host_ref, disk_id, hostname=hostname) log.info('Erased disk partitions on disk \'{0}\' on host \'{1}\'' - ''.format(disk_id, esxi_host)) + ''.format(disk_id, hostname)) return True @@ -6220,7 +6220,7 @@ def create_diskgroup(cache_disk_id, capacity_disk_ids, safety_checks=True, for id in disk_ids: if not [d for d in disks if d.canonicalName == id]: raise VMwareObjectRetrievalError( - 'No disk with id \'{0}\' was found in ESXi host \'{0}\'' + 'No disk with id \'{0}\' was found in ESXi host \'{1}\'' ''.format(id, hostname)) cache_disk = [d for d in disks if d.canonicalName == cache_disk_id][0] capacity_disks = [d for d in disks if d.canonicalName in capacity_disk_ids] @@ -6287,7 +6287,7 @@ def add_capacity_to_diskgroup(cache_disk_id, capacity_disk_ids, if not diskgroups: raise VMwareObjectRetrievalError( 'No diskgroup with cache disk id \'{0}\' was found in ESXi ' - 'host \'{1}\''.format(cache_disk_id, esxi_host)) + 'host \'{1}\''.format(cache_disk_id, hostname)) vsan_disk_mgmt_system = \ salt.utils.vsan.get_vsan_disk_management_system(service_instance) salt.utils.vsan.add_capacity_to_diskgroup(service_instance, @@ -6490,7 +6490,7 @@ def configure_host_cache(enabled, datastore=None, swap_size_MiB=None, if not ds_refs: raise VMwareObjectRetrievalError( 'Datastore \'{0}\' was not found on host ' - '\'{1}\''.format(datastore_name, hostname)) + '\'{1}\''.format(datastore, hostname)) ds_ref = ds_refs[0] salt.utils.vmware.configure_host_cache(host_ref, ds_ref, swap_size_MiB) return True diff --git a/salt/proxy/esxi.py b/salt/proxy/esxi.py index f358a710da..c1131d4dfd 100644 --- a/salt/proxy/esxi.py +++ b/salt/proxy/esxi.py @@ -276,7 +276,7 @@ import logging import os # Import Salt Libs -from salt.exceptions import SaltSystemExit +from salt.exceptions import SaltSystemExit, InvalidConfigError from salt.config.schemas.esxi import EsxiProxySchema from salt.utils.dictupdate import merge @@ -300,6 +300,7 @@ log = logging.getLogger(__file__) # Define the module's virtual name __virtualname__ = 'esxi' + def __virtual__(): ''' Only load if the ESXi execution module is available. @@ -309,6 +310,7 @@ def __virtual__(): return False, 'The ESXi Proxy Minion module did not load.' + def init(opts): ''' This function gets called when the proxy starts up. For @@ -325,7 +327,7 @@ def init(opts): try: jsonschema.validate(proxy_conf, schema) except jsonschema.exceptions.ValidationError as exc: - raise excs.InvalidProxyInputError(exc) + raise InvalidConfigError(exc) DETAILS['proxytype'] = proxy_conf['proxytype'] if ('host' not in proxy_conf) and ('vcenter' not in proxy_conf): @@ -345,7 +347,7 @@ def init(opts): # Get the correct login details try: username, password = find_credentials(host) - except excs.SaltSystemExit as err: + except SaltSystemExit as err: log.critical('Error: {0}'.format(err)) return False @@ -366,7 +368,7 @@ def init(opts): if 'mechanism' not in proxy_conf: log.critical('No \'mechanism\' key found in pillar for this proxy.') return False - mechanism = proxy_conf['mechanism'] + mechanism = proxy_conf['mechanism'] # Save mandatory fields in cache for key in ('vcenter', 'mechanism'): DETAILS[key] = proxy_conf[key] @@ -376,7 +378,7 @@ def init(opts): log.critical('No \'username\' key found in pillar for this ' 'proxy.') return False - if not 'passwords' in proxy_conf and \ + if 'passwords' not in proxy_conf and \ len(proxy_conf['passwords']) > 0: log.critical('Mechanism is set to \'userpass\' , but no ' @@ -386,11 +388,11 @@ def init(opts): for key in ('username', 'passwords'): DETAILS[key] = proxy_conf[key] elif mechanism == 'sspi': - if not 'domain' in proxy_conf: + if 'domain' not in proxy_conf: log.critical('Mechanism is set to \'sspi\' , but no ' '\'domain\' key found in pillar for this proxy.') return False - if not 'principal' in proxy_conf: + if 'principal' not in proxy_conf: log.critical('Mechanism is set to \'sspi\' , but no ' '\'principal\' key found in pillar for this ' 'proxy.') @@ -405,7 +407,7 @@ def init(opts): try: username, password = find_credentials() DETAILS['password'] = password - except excs.SaltSystemExit as err: + except SaltSystemExit as err: log.critical('Error: {0}'.format(err)) return False @@ -456,7 +458,7 @@ def ping(): __salt__['vsphere.system_info'](host=DETAILS['host'], username=DETAILS['username'], password=DETAILS['password']) - except excs.SaltSystemExit as err: + except SaltSystemExit as err: log.warning(err) return False return True diff --git a/salt/states/esxi.py b/salt/states/esxi.py index c94daef37b..337ef1d7f5 100644 --- a/salt/states/esxi.py +++ b/salt/states/esxi.py @@ -117,7 +117,7 @@ except ImportError: log = logging.getLogger(__name__) try: - from pyVmomi import vim, vmodl, VmomiSupport + from pyVmomi import VmomiSupport # We check the supported vim versions to infer the pyVmomi version if 'vim25/6.0' in VmomiSupport.versionMap and \ @@ -1122,7 +1122,7 @@ def diskgroups_configured(name, diskgroups, erase_disks=False): capacity_disk_ids = [] capacity_disk_displays = [] for scsi_addr in dg['capacity_scsi_addrs']: - if not scsi_addr in scsi_addr_to_disk_map: + if scsi_addr not in scsi_addr_to_disk_map: bad_scsi_addrs.append(scsi_addr) continue capacity_disk_ids.append(scsi_addr_to_disk_map[scsi_addr]['id']) @@ -1153,7 +1153,7 @@ def diskgroups_configured(name, diskgroups, erase_disks=False): capacity_disk_displays]))) else: # Erase disk group disks - for disk_id in ([cache_disk_id] + capacity_disk_ids): + for disk_id in [cache_disk_id] + capacity_disk_ids: __salt__['vsphere.erase_disk_partitions']( disk_id=disk_id, service_instance=si) comments.append('Erased disks of diskgroup #{0}; ' @@ -1287,9 +1287,9 @@ def diskgroups_configured(name, diskgroups, erase_disks=False): __salt__['vsphere.disconnect'](si) #Build the final return message - result = (True if not (changes or errors) else # no changes/errors - None if __opts__['test'] else # running in test mode - False if errors else True) # found errors; defaults to True + result = (True if not (changes or errors) else # no changes/errors + None if __opts__['test'] else # running in test mode + False if errors else True) # found errors; defaults to True ret.update({'result': result, 'comment': '\n'.join(comments)}) if changes: @@ -1385,7 +1385,7 @@ def host_cache_configured(name, enabled, datastore, swap_size='100%', '\'{0}\''.format(hostname)) ret = {'name': hostname, 'comment': 'Default comments', 'result': None, 'changes': {}, 'pchanges': {}} - result = None if __opts__['test'] else True #We assume success + result = None if __opts__['test'] else True # We assume success needs_setting = False comments = [] changes = {} @@ -1518,7 +1518,6 @@ def host_cache_configured(name, enabled, datastore, swap_size='100%', log.trace('existing_datastore = {0}'.format(existing_datastore)) log.info(comments[-1]) - if existing_datastore: # The following comparisons can be done if the existing_datastore # is set; it may not be set if running in test mode @@ -1533,23 +1532,22 @@ def host_cache_configured(name, enabled, datastore, swap_size='100%', else: raw_size_MiB = swap_size_value * 1024 log.trace('raw_size = {0}MiB'.format(raw_size_MiB)) - swap_size_MiB= int(raw_size_MiB/1024)*1024 + swap_size_MiB = int(raw_size_MiB/1024)*1024 log.trace('adjusted swap_size = {0}MiB'.format(swap_size_MiB)) existing_swap_size_MiB = 0 - m = re.match('(\d+)MiB', host_cache.get('swap_size')) if \ + m = re.match(r'(\d+)MiB', host_cache.get('swap_size')) if \ host_cache.get('swap_size') else None if m: # if swap_size from the host is set and has an expected value # we are going to parse it to get the number of MiBs existing_swap_size_MiB = int(m.group(1)) - if not (existing_swap_size_MiB == swap_size_MiB): + if not existing_swap_size_MiB == swap_size_MiB: needs_setting = True changes.update( {'swap_size': {'old': '{}GiB'.format(existing_swap_size_MiB/1024), 'new': '{}GiB'.format(swap_size_MiB/1024)}}) - if needs_setting: if __opts__['test']: comments.append('State {0} will configure ' diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index f2fbf43f59..45dd46a4ac 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2471,7 +2471,7 @@ def get_scsi_address_to_lun_map(host_ref, storage_system=None, hostname=None): luns_to_key_map = {d.key: d for d in get_all_luns(host_ref, storage_system, hostname)} return {scsi_addr: luns_to_key_map[lun_key] for scsi_addr, lun_key in - lun_ids_to_scsi_addr_map.iteritems()} + lun_ids_to_six.iteritems(scsi_addr_map)} def get_disks(host_ref, disk_ids=None, scsi_addresses=None, @@ -2513,8 +2513,9 @@ def get_disks(host_ref, disk_ids=None, scsi_addresses=None, lun_key_by_scsi_addr = _get_scsi_address_to_lun_key_map(si, host_ref, storage_system, hostname) - disk_keys = [key for scsi_addr, key in lun_key_by_scsi_addr.iteritems() - if scsi_addr in scsi_addresses] + disk_keys = [key for scsi_addr, key + in six.iteritems(lun_key_by_scsi_addr) + if scsi_addr in scsi_addresses] log.trace('disk_keys based on scsi_addresses = {0}'.format(disk_keys)) scsi_luns = get_all_luns(host_ref, storage_system) @@ -2695,8 +2696,8 @@ def get_diskgroups(host_ref, cache_disk_ids=None, get_all_disk_groups=False): vsan_disk_mappings = vsan_storage_info.diskMapping if not vsan_disk_mappings: return [] - disk_groups = [dm for dm in vsan_disk_mappings if \ - (get_all_disk_groups or \ + disk_groups = [dm for dm in vsan_disk_mappings if + (get_all_disk_groups or (dm.ssd.canonicalName in cache_disk_ids))] log.trace('Retrieved disk groups on host \'{0}\', with cache disk ids : ' '{1}'.format(hostname, diff --git a/salt/utils/vsan.py b/salt/utils/vsan.py index b2ec11f80d..4e124c9f6f 100644 --- a/salt/utils/vsan.py +++ b/salt/utils/vsan.py @@ -49,7 +49,8 @@ import logging import ssl # Import Salt Libs -from salt.exceptions import VMwareApiError, VMwareRuntimeError +from salt.exceptions import VMwareApiError, VMwareRuntimeError, \ + VMwareObjectRetrievalError import salt.utils.vmware try: @@ -282,7 +283,7 @@ def add_capacity_to_diskgroup(service_instance, vsan_disk_mgmt_system, spec.host = host_ref try: task = vsan_disk_mgmt_system.InitializeDiskMappings(spec) - except fault.NoPermission as exc: + except vim.fault.NoPermission as exc: log.exception(exc) raise VMwareApiError('Not enough permissions. Required privilege: ' '{0}'.format(exc.privilegeId)) From 2e2b01e57cdfbf0a561b20a7383750d78126cefb Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 26 Sep 2017 17:29:01 -0400 Subject: [PATCH 409/633] Fix logic to list hosts --- salt/modules/vsphere.py | 2 +- salt/utils/vmware.py | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index aad667d124..c88485d555 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -5941,7 +5941,7 @@ def list_hosts_via_proxy(hostnames=None, datacenter=None, raise salt.exceptions.ArgumentValueError( 'Datacenter is required when cluster is specified') get_all_hosts = False - if not hostnames and not datacenter and not cluster: + if not hostnames: get_all_hosts = True hosts = salt.utils.vmware.get_hosts(service_instance, datacenter_name=datacenter, diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 45dd46a4ac..532312b59b 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2290,20 +2290,21 @@ def get_hosts(service_instance, datacenter_name=None, host_names=None, Default value is False. ''' properties = ['name'] + if cluster_name and not datacenter_name: + raise salt.exceptions.ArgumentValueError( + 'Must specify the datacenter when specifying the cluster') if not host_names: host_names = [] - if get_all_hosts or not datacenter_name: + if not datacenter_name: # Assume the root folder is the starting point start_point = get_root_folder(service_instance) else: + start_point = get_datacenter(service_instance, datacenter_name) if cluster_name: + # Retrieval to test if cluster exists. Cluster existence only makes + # sense if the datacenter has been specified + cluster = get_cluster(start_point, cluster_name) properties.append('parent') - if datacenter_name: - start_point = get_datacenter(service_instance, datacenter_name) - if cluster_name: - # Retrieval to test if cluster exists. Cluster existence only makes - # sense if the cluster has been specified - cluster = get_cluster(start_point, cluster_name) # Search for the objects hosts = get_mors_with_properties(service_instance, @@ -2316,16 +2317,17 @@ def get_hosts(service_instance, datacenter_name=None, host_names=None, # Complex conditions checking if a host should be added to the # filtered list (either due to its name and/or cluster membership) - if get_all_hosts: - filtered_hosts.append(h['object']) - continue - if cluster_name: if not isinstance(h['parent'], vim.ClusterComputeResource): continue parent_name = get_managed_object_name(h['parent']) if parent_name != cluster_name: continue + + if get_all_hosts: + filtered_hosts.append(h['object']) + continue + if h['name'] in host_names: filtered_hosts.append(h['object']) return filtered_hosts From adfa462c05018648bf4077e98dc07c2cb00297f4 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 26 Sep 2017 17:29:36 -0400 Subject: [PATCH 410/633] Fixed tests for salt.utils.vmware.get_hosts --- tests/unit/utils/vmware/test_host.py | 43 ++++++++++++++++------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/tests/unit/utils/vmware/test_host.py b/tests/unit/utils/vmware/test_host.py index bd28c70f61..5a6319e6c8 100644 --- a/tests/unit/utils/vmware/test_host.py +++ b/tests/unit/utils/vmware/test_host.py @@ -14,6 +14,7 @@ from tests.support.unit import TestCase, skipIf from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock # Import Salt libraries +from salt.exceptions import ArgumentValueError import salt.utils.vmware # Import Third Party Libs try: @@ -46,14 +47,23 @@ class GetHostsTestCase(TestCase): self.mock_host1, self.mock_host2, self.mock_host3 = MagicMock(), \ MagicMock(), MagicMock() self.mock_prop_host1 = {'name': 'fake_hostname1', - 'object': self.mock_host1} + 'object': self.mock_host1} self.mock_prop_host2 = {'name': 'fake_hostname2', - 'object': self.mock_host2} + 'object': self.mock_host2} self.mock_prop_host3 = {'name': 'fake_hostname3', - 'object': self.mock_host3} + 'object': self.mock_host3} self.mock_prop_hosts = [self.mock_prop_host1, self.mock_prop_host2, self.mock_prop_host3] + def test_cluster_no_datacenter(self): + with self.assertRaises(ArgumentValueError) as excinfo: + salt.utils.vmware.get_hosts(self.mock_si, + cluster_name='fake_cluster') + self.assertEqual(excinfo.exception.strerror, + 'Must specify the datacenter when specifying the ' + 'cluster') + + def test_get_si_no_datacenter_no_cluster(self): mock_get_mors = MagicMock() mock_get_root_folder = MagicMock(return_value=self.mock_root_folder) @@ -124,23 +134,20 @@ class GetHostsTestCase(TestCase): self.assertEqual(res, []) def test_filter_cluster(self): - cluster1 = vim.ClusterComputeResource('fake_good_cluster') - cluster2 = vim.ClusterComputeResource('fake_bad_cluster') - # Mock cluster1.name and cluster2.name - cluster1._stub = MagicMock(InvokeAccessor=MagicMock( - return_value='fake_good_cluster')) - cluster2._stub = MagicMock(InvokeAccessor=MagicMock( - return_value='fake_bad_cluster')) - self.mock_prop_host1['parent'] = cluster2 - self.mock_prop_host2['parent'] = cluster1 - self.mock_prop_host3['parent'] = cluster1 + self.mock_prop_host1['parent'] = vim.ClusterComputeResource('cluster') + self.mock_prop_host2['parent'] = vim.ClusterComputeResource('cluster') + self.mock_prop_host3['parent'] = vim.Datacenter('dc') + mock_get_cl_name = MagicMock( + side_effect=['fake_bad_cluster', 'fake_good_cluster']) with patch('salt.utils.vmware.get_mors_with_properties', MagicMock(return_value=self.mock_prop_hosts)): - res = salt.utils.vmware.get_hosts(self.mock_si, - datacenter_name='fake_datacenter', - cluster_name='fake_good_cluster', - get_all_hosts=True) - self.assertEqual(res, [self.mock_host2, self.mock_host3]) + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_cl_name): + res = salt.utils.vmware.get_hosts( + self.mock_si, datacenter_name='fake_datacenter', + cluster_name='fake_good_cluster', get_all_hosts=True) + self.assertEqual(mock_get_cl_name.call_count, 2) + self.assertEqual(res, [self.mock_host2]) def test_no_hosts(self): with patch('salt.utils.vmware.get_mors_with_properties', From 90a174c915fd04ae391c83042b3772059e57e435 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Tue, 26 Sep 2017 20:22:26 -0400 Subject: [PATCH 411/633] more pylint --- salt/proxy/esxi.py | 2 +- salt/states/esxi.py | 4 ++-- salt/utils/vmware.py | 8 ++++---- tests/unit/utils/vmware/test_host.py | 1 - 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/salt/proxy/esxi.py b/salt/proxy/esxi.py index c1131d4dfd..1599c381c6 100644 --- a/salt/proxy/esxi.py +++ b/salt/proxy/esxi.py @@ -405,7 +405,7 @@ def init(opts): log.debug('Retrieving credentials and testing vCenter connection' ' for mehchanism \'userpass\'') try: - username, password = find_credentials() + username, password = find_credentials(DETAILS['vcenter']) DETAILS['password'] = password except SaltSystemExit as err: log.critical('Error: {0}'.format(err)) diff --git a/salt/states/esxi.py b/salt/states/esxi.py index 337ef1d7f5..3d723abce1 100644 --- a/salt/states/esxi.py +++ b/salt/states/esxi.py @@ -101,7 +101,8 @@ import re from salt.ext import six import salt.utils.files from salt.exceptions import CommandExecutionError, InvalidConfigError, \ - VMwareObjectRetrievalError, VMwareSaltError, VMwareApiError + VMwareObjectRetrievalError, VMwareSaltError, VMwareApiError, \ + ArgumentValueError from salt.utils.decorators import depends from salt.config.schemas.esxi import DiskGroupsDiskScsiAddressSchema, \ HostCacheSchema @@ -1415,7 +1416,6 @@ def host_cache_configured(name, enabled, datastore, swap_size='100%', 'new': enabled}}) needs_setting = True - # Check datastores existing_datastores = None if host_cache.get('datastore'): diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index 532312b59b..68ff6ca722 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -2473,7 +2473,7 @@ def get_scsi_address_to_lun_map(host_ref, storage_system=None, hostname=None): luns_to_key_map = {d.key: d for d in get_all_luns(host_ref, storage_system, hostname)} return {scsi_addr: luns_to_key_map[lun_key] for scsi_addr, lun_key in - lun_ids_to_six.iteritems(scsi_addr_map)} + six.iteritems(lun_ids_to_scsi_addr_map)} def get_disks(host_ref, disk_ids=None, scsi_addresses=None, @@ -2698,9 +2698,9 @@ def get_diskgroups(host_ref, cache_disk_ids=None, get_all_disk_groups=False): vsan_disk_mappings = vsan_storage_info.diskMapping if not vsan_disk_mappings: return [] - disk_groups = [dm for dm in vsan_disk_mappings if - (get_all_disk_groups or - (dm.ssd.canonicalName in cache_disk_ids))] + disk_groups = [dm for dm in vsan_disk_mappings if + (get_all_disk_groups or + (dm.ssd.canonicalName in cache_disk_ids))] log.trace('Retrieved disk groups on host \'{0}\', with cache disk ids : ' '{1}'.format(hostname, [d.ssd.canonicalName for d in disk_groups])) diff --git a/tests/unit/utils/vmware/test_host.py b/tests/unit/utils/vmware/test_host.py index 5a6319e6c8..0f6965fb7c 100644 --- a/tests/unit/utils/vmware/test_host.py +++ b/tests/unit/utils/vmware/test_host.py @@ -63,7 +63,6 @@ class GetHostsTestCase(TestCase): 'Must specify the datacenter when specifying the ' 'cluster') - def test_get_si_no_datacenter_no_cluster(self): mock_get_mors = MagicMock() mock_get_root_folder = MagicMock(return_value=self.mock_root_folder) From 1a12d5cb30efb89bacd455fa052aa41aad1581b6 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 28 Sep 2017 06:29:55 -0400 Subject: [PATCH 412/633] Added salt.pillar.extra_minion_data_in_pillar that adds any extra minion data into the pillar --- salt/pillar/extra_minion_data_in_pillar.py | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 salt/pillar/extra_minion_data_in_pillar.py diff --git a/salt/pillar/extra_minion_data_in_pillar.py b/salt/pillar/extra_minion_data_in_pillar.py new file mode 100644 index 0000000000..2dad741c66 --- /dev/null +++ b/salt/pillar/extra_minion_data_in_pillar.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +''' +Add all extra minion data to the pillar. + +:codeauthor: Alexandru.Bleotu@morganstanley.ms.com + +One can filter on the keys to include in the pillar by using the ``include`` +parameter. For subkeys the ':' notation is supported (i.e. 'key:subkey') +The keyword ```` includes all keys. + +Complete example in etc/salt/master +===================================== + +.. code-block:: yaml + + ext_pillar: + - extra_minion_data_in_pillar: + include: + + ext_pillar: + - extra_minion_data_in_pillar: + include: + - key1 + - key2:subkey2 +''' + + +from __future__ import absolute_import +import os +import logging + + +# Set up logging +log = logging.getLogger(__name__) + + +__virtualname__ = 'extra_minion_data_in_pillar' + +def __virtual__(): + return __virtualname__ + + +def ext_pillar(minion_id, pillar, include, extra_minion_data=None): + + def get_subtree(key, source_dict): + ''' + Returns a subtree corresponfing to the specified key. + + key + Key. Supports the ':' notation (e.g. 'key:subkey') + + source_dict + Source dictionary + ''' + ret_dict = aux_dict = {} + subtree = source_dict + subkeys = key.split(':') + # Build an empty intermediate subtree following the subkeys + for subkey in subkeys[:-1]: + # The result will be built in aux_dict + aux_dict[subkey] = {} + aux_dict = aux_dict[subkey] + if not subkey in subtree: + # The subkey is not in + return {} + subtree = subtree[subkey] + if subkeys[-1] not in subtree: + # Final subkey is not in subtree + return {} + # Assign the subtree value to the result + aux_dict[subkeys[-1]] = subtree[subkeys[-1]] + return ret_dict + + log.trace('minion_id = {0}'.format(minion_id)) + log.trace('include = {0}'.format(include)) + log.trace('extra_minion_data = {0}'.format(extra_minion_data)) + data = {} + + if not extra_minion_data: + return {} + if include == '': + return extra_minion_data + data = {} + for key in include: + data.update(get_subtree(key, extra_minion_data)) + return data From 998c4a95fa8e8d82d15757265a1558f22b52b6b4 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 28 Sep 2017 06:31:35 -0400 Subject: [PATCH 413/633] Added tests for salt.pillar.extra_minion_data_in_pillar --- .../test_extra_minion_data_in_pillar.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/unit/pillar/test_extra_minion_data_in_pillar.py diff --git a/tests/unit/pillar/test_extra_minion_data_in_pillar.py b/tests/unit/pillar/test_extra_minion_data_in_pillar.py new file mode 100644 index 0000000000..df4484e5e6 --- /dev/null +++ b/tests/unit/pillar/test_extra_minion_data_in_pillar.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Import python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock + +# Import Salt Libs +from salt.pillar import extra_minion_data_in_pillar + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class ExtraMinionDataInPillarTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.pillar.extra_minion_data_in_pillar + ''' + def setup_loader_modules(self): + return { + extra_minion_data_in_pillar : { + '__virtual__': True, + } + } + + def setUp(self): + self.pillar = MagicMock() + self.extra_minion_data = {'key1': {'subkey1': 'value1'}, + 'key2': {'subkey2': {'subsubkey2': 'value2'}}, + 'key3': 'value3', + 'key4': {'subkey4': 'value4'}} + + def test_extra_values_none_or_empty(self): + ret = extra_minion_data_in_pillar.ext_pillar('fake_id', self.pillar, + 'fake_include', None) + self.assertEqual(ret, {}) + ret = extra_minion_data_in_pillar.ext_pillar('fake_id', self.pillar, + 'fake_include', {}) + self.assertEqual(ret, {}) + + def test_include_all(self): + ret = extra_minion_data_in_pillar.ext_pillar( + 'fake_id', self.pillar, '', self.extra_minion_data) + self.assertEqual(ret, self.extra_minion_data) + + def test_include_specific_keys(self): + # Tests partially existing key, key with and without subkey, + ret = extra_minion_data_in_pillar.ext_pillar( + 'fake_id', self.pillar, + include=['key1:subkey1', 'key2:subkey3', 'key3', 'key4'], + extra_minion_data=self.extra_minion_data) + self.assertEqual(ret, {'key1': {'subkey1': 'value1'}, + 'key3': 'value3', + 'key4': {'subkey4': 'value4'}}) From d862a6f3f2e52c542d1f900408801cc299636344 Mon Sep 17 00:00:00 2001 From: rallytime Date: Thu, 28 Sep 2017 09:52:04 -0400 Subject: [PATCH 414/633] Fix some formatting issues on the oxygen release notes page --- doc/topics/releases/oxygen.rst | 481 ++++++++++++++++++--------------- 1 file changed, 260 insertions(+), 221 deletions(-) diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 4c651bfce9..bce4c56dad 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -8,17 +8,17 @@ Comparison Operators in Package Installation -------------------------------------------- Salt now supports using comparison operators (e.g. ``>=1.2.3``) when installing -packages on minions which use :mod:`yum/dnf ` or :mod:`apt -`. This is supported both in the :py:func:`pkg.installed -` state and in the ``pkg.install`` remote execution -function. +packages on minions which use :mod:`yum/dnf ` or +:mod:`apt `. This is supported both in the +:py:func:`pkg.installed ` state and in the ``pkg.install`` +remote execution function. :ref:`Master Tops ` Changes ----------------------------------------------- -When both :ref:`Master Tops ` and a :ref:`Top File -` produce SLS matches for a given minion, the matches were being -merged in an unpredictable manner which did not preserve ordering. This has +When both :ref:`Master Tops ` and a +:ref:`Top File ` produce SLS matches for a given minion, the matches +were being merged in an unpredictable manner which did not preserve ordering. This has been changed. The top file matches now execute in the expected order, followed by any master tops matches that are not matched via a top file. @@ -55,14 +55,14 @@ New support for Cisco UCS Chassis --------------------------------- The salt proxy minion now allows for control of Cisco USC chassis. See -the `cimc` modules for details. +the ``cimc`` modules for details. New salt-ssh roster ------------------- A new roster has been added that allows users to pull in a list of hosts -for salt-ssh targeting from a ~/.ssh configuration. For full details, -please see the `sshconfig` roster. +for salt-ssh targeting from a ``~/.ssh`` configuration. For full details, +please see the ``sshconfig`` roster. New GitFS Features ------------------ @@ -149,185 +149,200 @@ check the configuration for the correct format and only load if the validation p - ``avahi_announce`` beacon Old behavior: - ``` - beacons: - avahi_announce: - run_once: True - servicetype: _demo._tcp - port: 1234 - txt: - ProdName: grains.productname - SerialNo: grains.serialnumber - Comments: 'this is a test' - ``` + + .. code-block:: yaml + + beacons: + avahi_announce: + run_once: True + servicetype: _demo._tcp + port: 1234 + txt: + ProdName: grains.productname + SerialNo: grains.serialnumber + Comments: 'this is a test' New behavior: - ``` - beacons: - avahi_announce: - - run_once: True - - servicetype: _demo._tcp - - port: 1234 - - txt: - ProdName: grains.productname - SerialNo: grains.serialnumber - Comments: 'this is a test' - ``` + + .. code-block:: yaml + + beacons: + avahi_announce: + - run_once: True + - servicetype: _demo._tcp + - port: 1234 + - txt: + ProdName: grains.productname + SerialNo: grains.serialnumber + Comments: 'this is a test' - ``bonjour_announce`` beacon Old behavior: - ``` - beacons: - bonjour_announce: - run_once: True - servicetype: _demo._tcp - port: 1234 - txt: - ProdName: grains.productname - SerialNo: grains.serialnumber - Comments: 'this is a test' - ``` + + .. code-block:: yaml + + beacons: + bonjour_announce: + run_once: True + servicetype: _demo._tcp + port: 1234 + txt: + ProdName: grains.productname + SerialNo: grains.serialnumber + Comments: 'this is a test' New behavior: - ``` - beacons: - bonjour_announce: - - run_once: True - - servicetype: _demo._tcp - - port: 1234 - - txt: - ProdName: grains.productname - SerialNo: grains.serialnumber - Comments: 'this is a test' - ``` + + .. code-block:: yaml + + beacons: + bonjour_announce: + - run_once: True + - servicetype: _demo._tcp + - port: 1234 + - txt: + ProdName: grains.productname + SerialNo: grains.serialnumber + Comments: 'this is a test' - ``btmp`` beacon Old behavior: - ``` - beacons: - btmp: {} - ``` + + .. code-block:: yaml + + beacons: + btmp: {} New behavior: - ``` - beacons: - btmp: [] - ``` + .. code-block:: yaml + + beacons: + btmp: [] - ``glxinfo`` beacon Old behavior: - ``` - beacons: - glxinfo: - user: frank - screen_event: True - ``` + + .. code-block:: yaml + + beacons: + glxinfo: + user: frank + screen_event: True New behavior: - ``` - beacons: - glxinfo: - - user: frank - - screen_event: True - ``` + + .. code-block:: yaml + + beacons: + glxinfo: + - user: frank + - screen_event: True - ``haproxy`` beacon Old behavior: - ``` - beacons: - haproxy: - - www-backend: - threshold: 45 - servers: + + .. code-block:: yaml + + beacons: + haproxy: + - www-backend: + threshold: 45 + servers: + - web1 + - web2 + - interval: 120 + + New behavior: + + .. code-block:: yaml + + beacons: + haproxy: + - backends: + www-backend: + threshold: 45 + servers: - web1 - web2 - interval: 120 - ``` - - New behavior: - ``` - beacons: - haproxy: - - backends: - www-backend: - threshold: 45 - servers: - - web1 - - web2 - - interval: 120 - ``` - ``inotify`` beacon Old behavior: - ``` - beacons: - inotify: - /path/to/file/or/dir: - mask: - - open - - create - - close_write - recurse: True - auto_add: True - exclude: - - /path/to/file/or/dir/exclude1 - - /path/to/file/or/dir/exclude2 - - /path/to/file/or/dir/regex[a-m]*$: - regex: True - coalesce: True - ``` + + .. code-block:: yaml + + beacons: + inotify: + /path/to/file/or/dir: + mask: + - open + - create + - close_write + recurse: True + auto_add: True + exclude: + - /path/to/file/or/dir/exclude1 + - /path/to/file/or/dir/exclude2 + - /path/to/file/or/dir/regex[a-m]*$: + regex: True + coalesce: True New behavior: - ``` - beacons: - inotify: - - files: - /path/to/file/or/dir: - mask: - - open - - create - - close_write - recurse: True - auto_add: True - exclude: - - /path/to/file/or/dir/exclude1 - - /path/to/file/or/dir/exclude2 - - /path/to/file/or/dir/regex[a-m]*$: - regex: True - - coalesce: True -``` + + .. code-block:: yaml + + beacons: + inotify: + - files: + /path/to/file/or/dir: + mask: + - open + - create + - close_write + recurse: True + auto_add: True + exclude: + - /path/to/file/or/dir/exclude1 + - /path/to/file/or/dir/exclude2 + - /path/to/file/or/dir/regex[a-m]*$: + regex: True + - coalesce: True - ``journald`` beacon Old behavior: - ``` - beacons: - journald: - sshd: - SYSLOG_IDENTIFIER: sshd - PRIORITY: 6 - ``` - New behavior: - ``` - beacons: - journald: - - services: + .. code-block:: yaml + + beacons: + journald: sshd: SYSLOG_IDENTIFIER: sshd PRIORITY: 6 - ``` + + New behavior: + + .. code-block:: yaml + + beacons: + journald: + - services: + sshd: + SYSLOG_IDENTIFIER: sshd + PRIORITY: 6 - ``load`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: load: 1m: @@ -341,51 +356,55 @@ check the configuration for the correct format and only load if the validation p - 1.0 emitatstartup: True onchangeonly: False - ``` New behavior: - ``` - beacons: - load: - - averages: - 1m: - - 0.0 - - 2.0 - 5m: - - 0.0 - - 1.5 - 15m: - - 0.1 - - 1.0 - - emitatstartup: True - - onchangeonly: False - ``` + + .. code-block:: yaml + + beacons: + load: + - averages: + 1m: + - 0.0 + - 2.0 + 5m: + - 0.0 + - 1.5 + 15m: + - 0.1 + - 1.0 + - emitatstartup: True + - onchangeonly: False - ``log`` beacon Old behavior: - ``` - beacons: - log: - file: - : - regex: - ``` - New behavior: - ``` - beacons: - log: - - file: - - tags: + .. code-block:: yaml + + beacons: + log: + file: : regex: - ``` + + New behavior: + + .. code-block:: yaml + + beacons: + log: + - file: + - tags: + : + regex: - ``network_info`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: network_info: - eth0: @@ -398,10 +417,11 @@ check the configuration for the correct format and only load if the validation p errout: 100 dropin: 100 dropout: 100 - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: network_info: - interfaces: @@ -415,12 +435,13 @@ check the configuration for the correct format and only load if the validation p errout: 100 dropin: 100 dropout: 100 - ``` - ``network_settings`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: network_settings: eth0: @@ -429,10 +450,11 @@ check the configuration for the correct format and only load if the validation p onvalue: 1 eth1: linkmode: - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: network_settings: - interfaces: @@ -442,12 +464,13 @@ check the configuration for the correct format and only load if the validation p onvalue: 1 - eth1: linkmode: - ``` - ``proxy_example`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: proxy_example: endpoint: beacon @@ -458,60 +481,66 @@ check the configuration for the correct format and only load if the validation p beacons: proxy_example: - endpoint: beacon - ``` - ``ps`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: ps: - salt-master: running - mysql: stopped - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: ps: - processes: salt-master: running mysql: stopped - ``` - ``salt_proxy`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: salt_proxy: - p8000: {} - p8001: {} - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: salt_proxy: - proxies: p8000: {} p8001: {} - ``` - ``sensehat`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: sensehat: humidity: 70% temperature: [20, 40] temperature_from_pressure: 40 pressure: 1500 - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: sensehat: - sensors: @@ -519,21 +548,22 @@ check the configuration for the correct format and only load if the validation p temperature: [20, 40] temperature_from_pressure: 40 pressure: 1500 - ``` - ``service`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: service: salt-master: mysql: - ``` - New behavior: - ``` + + .. code-block:: yaml + beacons: service: - services: @@ -541,93 +571,102 @@ check the configuration for the correct format and only load if the validation p onchangeonly: True delay: 30 uncleanshutdown: /run/nginx.pid - ``` - ``sh`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: sh: {} - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: sh: [] - ``` - ``status`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: status: {} - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: status: [] - ``` - ``telegram_bot_msg`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: telegram_bot_msg: token: "" accept_from: - "" interval: 10 - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: telegram_bot_msg: - token: "" - accept_from: - "" - interval: 10 - ``` - ``twilio_txt_msg`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: twilio_txt_msg: account_sid: "" auth_token: "" twilio_number: "+15555555555" interval: 10 - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: twilio_txt_msg: - account_sid: "" - auth_token: "" - twilio_number: "+15555555555" - interval: 10 - ``` - ``wtmp`` beacon Old behavior: - ``` + + .. code-block:: yaml + beacons: wtmp: {} - ``` New behavior: - ``` + + .. code-block:: yaml + beacons: wtmp: [] - ``` Deprecations ------------ From 25a440a2ea9d2aa03d83e3e873dba23db965c01e Mon Sep 17 00:00:00 2001 From: rallytime Date: Thu, 28 Sep 2017 10:12:06 -0400 Subject: [PATCH 415/633] Fixup a couple of issues with the panos execution module - Spelling errors - Version added tag for Oxygen - Use correct import for salt.exceptions --- salt/modules/panos.py | 56 +++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/salt/modules/panos.py b/salt/modules/panos.py index aecf93fffe..006cef2f4b 100644 --- a/salt/modules/panos.py +++ b/salt/modules/panos.py @@ -7,9 +7,11 @@ Module to provide Palo Alto compatibility to Salt. :depends: none :platform: unix +.. versionadded:: Oxygen Configuration ============= + This module accepts connection configuration details either as parameters, or as configuration settings in pillar as a Salt proxy. Options passed into opts will be ignored if options are passed into pillar. @@ -19,6 +21,7 @@ Options passed into opts will be ignored if options are passed into pillar. About ===== + This execution module was designed to handle connections to a Palo Alto based firewall. This module adds support to send connections directly to the device through the XML API or through a brokered connection to Panorama. @@ -31,8 +34,9 @@ import logging import time # Import Salt Libs -import salt.utils.platform +from salt.exceptions import CommandExecutionError import salt.proxy.panos +import salt.utils.platform log = logging.getLogger(__name__) @@ -55,11 +59,11 @@ def __virtual__(): def _get_job_results(query=None): ''' - Executes a query that requires a job for completion. This funciton will wait for the job to complete + Executes a query that requires a job for completion. This function will wait for the job to complete and return the results. ''' if not query: - raise salt.exception.CommandExecutionError("Query parameters cannot be empty.") + raise CommandExecutionError("Query parameters cannot be empty.") response = __proxy__['panos.call'](query) @@ -241,10 +245,10 @@ def download_software_file(filename=None, synch=False): ''' if not filename: - raise salt.exception.CommandExecutionError("Filename option must not be none.") + raise CommandExecutionError("Filename option must not be none.") if not isinstance(synch, bool): - raise salt.exception.CommandExecutionError("Synch option must be boolean..") + raise CommandExecutionError("Synch option must be boolean..") if synch is True: query = {'type': 'op', @@ -276,10 +280,10 @@ def download_software_version(version=None, synch=False): ''' if not version: - raise salt.exception.CommandExecutionError("Version option must not be none.") + raise CommandExecutionError("Version option must not be none.") if not isinstance(synch, bool): - raise salt.exception.CommandExecutionError("Synch option must be boolean..") + raise CommandExecutionError("Synch option must be boolean..") if synch is True: query = {'type': 'op', @@ -644,7 +648,7 @@ def get_job(jid=None): ''' if not jid: - raise salt.exception.CommandExecutionError("ID option must not be none.") + raise CommandExecutionError("ID option must not be none.") query = {'type': 'op', 'cmd': '{0}'.format(jid)} @@ -675,7 +679,7 @@ def get_jobs(state='all'): elif state.lower() == 'processed': query = {'type': 'op', 'cmd': ''} else: - raise salt.exception.CommandExecutionError("The state parameter must be all, pending, or processed.") + raise CommandExecutionError("The state parameter must be all, pending, or processed.") return __proxy__['panos.call'](query) @@ -1163,7 +1167,7 @@ def install_antivirus(version=None, latest=False, synch=False, skip_commit=False ''' if not version and latest is False: - raise salt.exception.CommandExecutionError("Version option must not be none.") + raise CommandExecutionError("Version option must not be none.") if synch is True: s = "yes" @@ -1220,7 +1224,7 @@ def install_software(version=None): ''' if not version: - raise salt.exception.CommandExecutionError("Version option must not be none.") + raise CommandExecutionError("Version option must not be none.") query = {'type': 'op', 'cmd': '' @@ -1261,7 +1265,7 @@ def refresh_fqdn_cache(force=False): ''' if not isinstance(force, bool): - raise salt.exception.CommandExecutionError("Force option must be boolean.") + raise CommandExecutionError("Force option must be boolean.") if force: query = {'type': 'op', @@ -1312,7 +1316,7 @@ def resolve_address(address=None, vsys=None): return False, 'The panos device requires version {0} or greater for this command.'.format(_required_version) if not address: - raise salt.exception.CommandExecutionError("FQDN to resolve must be provided as address.") + raise CommandExecutionError("FQDN to resolve must be provided as address.") if not vsys: query = {'type': 'op', @@ -1340,7 +1344,7 @@ def save_device_config(filename=None): ''' if not filename: - raise salt.exception.CommandExecutionError("Filename must not be empty.") + raise CommandExecutionError("Filename must not be empty.") query = {'type': 'op', 'cmd': '{0}'.format(filename)} @@ -1382,7 +1386,7 @@ def set_authentication_profile(profile=None, deploy=False): ''' if not profile: - salt.exception.CommandExecutionError("Profile name option must not be none.") + CommandExecutionError("Profile name option must not be none.") ret = {} @@ -1419,7 +1423,7 @@ def set_hostname(hostname=None, deploy=False): ''' if not hostname: - salt.exception.CommandExecutionError("Hostname option must not be none.") + CommandExecutionError("Hostname option must not be none.") ret = {} @@ -1459,7 +1463,7 @@ def set_management_icmp(enabled=True, deploy=False): elif enabled is False: value = "yes" else: - salt.exception.CommandExecutionError("Invalid option provided for service enabled option.") + CommandExecutionError("Invalid option provided for service enabled option.") ret = {} @@ -1499,7 +1503,7 @@ def set_management_http(enabled=True, deploy=False): elif enabled is False: value = "yes" else: - salt.exception.CommandExecutionError("Invalid option provided for service enabled option.") + CommandExecutionError("Invalid option provided for service enabled option.") ret = {} @@ -1539,7 +1543,7 @@ def set_management_https(enabled=True, deploy=False): elif enabled is False: value = "yes" else: - salt.exception.CommandExecutionError("Invalid option provided for service enabled option.") + CommandExecutionError("Invalid option provided for service enabled option.") ret = {} @@ -1579,7 +1583,7 @@ def set_management_ocsp(enabled=True, deploy=False): elif enabled is False: value = "yes" else: - salt.exception.CommandExecutionError("Invalid option provided for service enabled option.") + CommandExecutionError("Invalid option provided for service enabled option.") ret = {} @@ -1619,7 +1623,7 @@ def set_management_snmp(enabled=True, deploy=False): elif enabled is False: value = "yes" else: - salt.exception.CommandExecutionError("Invalid option provided for service enabled option.") + CommandExecutionError("Invalid option provided for service enabled option.") ret = {} @@ -1659,7 +1663,7 @@ def set_management_ssh(enabled=True, deploy=False): elif enabled is False: value = "yes" else: - salt.exception.CommandExecutionError("Invalid option provided for service enabled option.") + CommandExecutionError("Invalid option provided for service enabled option.") ret = {} @@ -1699,7 +1703,7 @@ def set_management_telnet(enabled=True, deploy=False): elif enabled is False: value = "yes" else: - salt.exception.CommandExecutionError("Invalid option provided for service enabled option.") + CommandExecutionError("Invalid option provided for service enabled option.") ret = {} @@ -1892,7 +1896,7 @@ def set_permitted_ip(address=None, deploy=False): ''' if not address: - salt.exception.CommandExecutionError("Address option must not be empty.") + CommandExecutionError("Address option must not be empty.") ret = {} @@ -1928,7 +1932,7 @@ def set_timezone(tz=None, deploy=False): ''' if not tz: - salt.exception.CommandExecutionError("Timezone name option must not be none.") + CommandExecutionError("Timezone name option must not be none.") ret = {} @@ -1976,7 +1980,7 @@ def unlock_admin(username=None): ''' if not username: - raise salt.exception.CommandExecutionError("Username option must not be none.") + raise CommandExecutionError("Username option must not be none.") query = {'type': 'op', 'cmd': '{0}' From cdafbe2068c3bdcfa0853badeabd7311c3526d01 Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Thu, 28 Sep 2017 07:26:30 -0700 Subject: [PATCH 416/633] Update Oxygen Release notes with new grains and module Also do a final cosmetic fix to the purefa module documentation --- doc/topics/releases/oxygen.rst | 15 +++++++++++++++ salt/modules/purefa.py | 1 + 2 files changed, 16 insertions(+) diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 2469018a71..d08c6c8f3d 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -46,6 +46,21 @@ noon PST so the Stormpath external authentication module has been removed. https://stormpath.com/oktaplusstormpath +New Grains +---------- + +New core grains have been added to expose any storage inititator setting. + +The new grains added are: + +* ``fc_wwn``: Show all fibre channel world wide port names for a host +* ``iscsi_iqn``: Show the iSCSI IQN name for a host + +New Modules +----------- + +- :mod:`salt.modules.purefa ` + New NaCl Renderer ----------------- diff --git a/salt/modules/purefa.py b/salt/modules/purefa.py index 8bcf06fbe8..ce42818fb8 100644 --- a/salt/modules/purefa.py +++ b/salt/modules/purefa.py @@ -31,6 +31,7 @@ Installation Prerequisites three methods. 1) From the minion config + .. code-block:: yaml pure_tags: From 8c5b021519e6ce194ae508d7a13ab8e7940a6ae3 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 28 Sep 2017 11:16:14 -0400 Subject: [PATCH 417/633] Added * as an include all wildcard in extra_minion_data_in_pillar external pillar (+ test) --- salt/pillar/extra_minion_data_in_pillar.py | 9 +++++++-- tests/unit/pillar/test_extra_minion_data_in_pillar.py | 7 ++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/salt/pillar/extra_minion_data_in_pillar.py b/salt/pillar/extra_minion_data_in_pillar.py index 2dad741c66..ee8961a887 100644 --- a/salt/pillar/extra_minion_data_in_pillar.py +++ b/salt/pillar/extra_minion_data_in_pillar.py @@ -15,13 +15,18 @@ Complete example in etc/salt/master ext_pillar: - extra_minion_data_in_pillar: - include: + include: * ext_pillar: - extra_minion_data_in_pillar: include: - key1 - key2:subkey2 + + ext_pillar: + - extra_minion_data_in_pillar: + include: + ''' @@ -78,7 +83,7 @@ def ext_pillar(minion_id, pillar, include, extra_minion_data=None): if not extra_minion_data: return {} - if include == '': + if include in ['*', '']: return extra_minion_data data = {} for key in include: diff --git a/tests/unit/pillar/test_extra_minion_data_in_pillar.py b/tests/unit/pillar/test_extra_minion_data_in_pillar.py index df4484e5e6..36e0f9e286 100644 --- a/tests/unit/pillar/test_extra_minion_data_in_pillar.py +++ b/tests/unit/pillar/test_extra_minion_data_in_pillar.py @@ -40,9 +40,10 @@ class ExtraMinionDataInPillarTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(ret, {}) def test_include_all(self): - ret = extra_minion_data_in_pillar.ext_pillar( - 'fake_id', self.pillar, '', self.extra_minion_data) - self.assertEqual(ret, self.extra_minion_data) + for include_all in ['*', '']: + ret = extra_minion_data_in_pillar.ext_pillar( + 'fake_id', self.pillar, include_all, self.extra_minion_data) + self.assertEqual(ret, self.extra_minion_data) def test_include_specific_keys(self): # Tests partially existing key, key with and without subkey, From 503cb9c93afb31be60a35c519773eb2cfcf073f0 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Thu, 28 Sep 2017 11:31:11 -0400 Subject: [PATCH 418/633] pylint --- salt/pillar/extra_minion_data_in_pillar.py | 5 ++--- tests/unit/pillar/test_extra_minion_data_in_pillar.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/salt/pillar/extra_minion_data_in_pillar.py b/salt/pillar/extra_minion_data_in_pillar.py index ee8961a887..13c7e812a3 100644 --- a/salt/pillar/extra_minion_data_in_pillar.py +++ b/salt/pillar/extra_minion_data_in_pillar.py @@ -31,16 +31,15 @@ Complete example in etc/salt/master from __future__ import absolute_import -import os import logging # Set up logging log = logging.getLogger(__name__) - __virtualname__ = 'extra_minion_data_in_pillar' + def __virtual__(): return __virtualname__ @@ -65,7 +64,7 @@ def ext_pillar(minion_id, pillar, include, extra_minion_data=None): # The result will be built in aux_dict aux_dict[subkey] = {} aux_dict = aux_dict[subkey] - if not subkey in subtree: + if subkey not in subtree: # The subkey is not in return {} subtree = subtree[subkey] diff --git a/tests/unit/pillar/test_extra_minion_data_in_pillar.py b/tests/unit/pillar/test_extra_minion_data_in_pillar.py index 36e0f9e286..ed6eeaf65c 100644 --- a/tests/unit/pillar/test_extra_minion_data_in_pillar.py +++ b/tests/unit/pillar/test_extra_minion_data_in_pillar.py @@ -19,7 +19,7 @@ class ExtraMinionDataInPillarTestCase(TestCase, LoaderModuleMockMixin): ''' def setup_loader_modules(self): return { - extra_minion_data_in_pillar : { + extra_minion_data_in_pillar: { '__virtual__': True, } } From 50779c3b1c50015637125ff51cce39f4626a63c1 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Thu, 28 Sep 2017 11:50:54 -0400 Subject: [PATCH 419/633] Add note to nitrogen release notes about pip for cent6 --- doc/topics/releases/2017.7.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/topics/releases/2017.7.0.rst b/doc/topics/releases/2017.7.0.rst index a0e257b347..1681101522 100644 --- a/doc/topics/releases/2017.7.0.rst +++ b/doc/topics/releases/2017.7.0.rst @@ -21,6 +21,9 @@ Salt will no longer support Python 2.6. We will provide python2.7 packages on ou .. _repo: https://repo.saltstack.com/ +As this will impact the installation of additional dependencies for salt modules please use pip packages if there is not a package available in a repository. You will need to install the python27-pip package to get access to the correct pip27 executable: ``yum install python27-pip`` + + ============ Known Issues ============ From d8708bf698aced1a10e9c5eb68615af02049cebe Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 28 Sep 2017 13:10:03 -0500 Subject: [PATCH 420/633] cmdmod: Don't list-ify string commands on Windows See https://github.com/saltstack/salt/issues/43522. --- salt/modules/cmdmod.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index 65b6767e33..e5d058505f 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -378,9 +378,6 @@ def _run(cmd, msg = 'missing salt/utils/win_runas.py' raise CommandExecutionError(msg) - if not isinstance(cmd, list): - cmd = salt.utils.shlex_split(cmd, posix=False) - cmd = ' '.join(cmd) return win_runas(cmd, runas, password, cwd) @@ -516,11 +513,11 @@ def _run(cmd, .format(cwd) ) - if python_shell is not True and not isinstance(cmd, list): - posix = True - if salt.utils.is_windows(): - posix = False - cmd = salt.utils.shlex_split(cmd, posix=posix) + if python_shell is not True \ + and not salt.utils.is_windows() \ + and not isinstance(cmd, list): + cmd = salt.utils.shlex_split(cmd) + if not use_vt: # This is where the magic happens try: From 66e6e89dc7641f0fe75b5c3e73ee20ede24b9185 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 28 Sep 2017 11:29:46 -0500 Subject: [PATCH 421/633] Properly handle UNC paths in salt.utils.path.readlink() When unpacking the destination of a symbolic link in Windows, links which point to UNC paths start with "UNC\" instead of "\\". This causes win32file.GetLongFileName to raise an exception. This commit modifies salt.utils.path.readlink() to return the path in the proper UNC format, and it also updates the roots backend to simply use the link path rather than the destination when following symlinks which point to a UNC path. --- salt/fileserver/roots.py | 10 ++++++++++ salt/utils/path.py | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/salt/fileserver/roots.py b/salt/fileserver/roots.py index 62563006c2..b3fe8ad074 100644 --- a/salt/fileserver/roots.py +++ b/salt/fileserver/roots.py @@ -367,6 +367,16 @@ def _file_lists(load, form): 'roots: %s symlink destination is %s', abs_path, link_dest ) + if salt.utils.is_windows() \ + and link_dest.startswith('\\\\'): + # Symlink points to a network path. Since you can't + # join UNC and non-UNC paths, just assume the original + # path. + log.trace( + 'roots: %s is a UNCH path, using %s instead', + link_dest, abs_path + ) + link_dest = abs_path if link_dest.startswith('..'): joined = os.path.join(abs_path, link_dest) else: diff --git a/salt/utils/path.py b/salt/utils/path.py index 53f94792b3..90f7a7f53a 100644 --- a/salt/utils/path.py +++ b/salt/utils/path.py @@ -9,6 +9,7 @@ from __future__ import absolute_import import errno import logging import os +import re import struct # Import 3rd-party libs @@ -110,6 +111,11 @@ def readlink(path): # comes out in 8.3 form; convert it to LFN to make it look nicer target = win32file.GetLongPathName(target) except pywinerror as exc: + # If target is on a UNC share, the decoded target will be in the format + # "UNC\hostanme\sharename\additional\subdirs\under\share". So, in + # these cases, return the target path in the proper UNC path format. + if target.startswith('UNC\\'): + return re.sub(r'^UNC\\+', r'\\\\', target) # if file is not found (i.e. bad symlink), return it anyway like on *nix if exc.winerror == 2: return target From 0c884b183f9e1c509e9213efebae3f400acb83f0 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Thu, 28 Sep 2017 14:29:28 -0600 Subject: [PATCH 422/633] correct handling of machine name lookup --- salt/modules/vagrant.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index e1f2ef0a5e..5044b7fb63 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -121,11 +121,7 @@ def get_machine_id(machine, cwd): :param cwd: the path to Vagrantfile :return: salt_id name ''' - name = salt.utils.sdb.sdb_get(_build_machine_uri(machine, cwd)) - if name is None: - raise SaltInvocationError( - 'No Salt name found for Vagrant machine {} in {}'.format( - machine, cwd)) + name = salt.utils.sdb.sdb_get(_build_machine_uri(machine, cwd), __opts__) return name @@ -146,7 +142,7 @@ def _erase_vm_info(name): salt.utils.sdb.sdb_delete(key, __opts__) except KeyError: # no delete method found -- load a blank value - salt.utils.sdb.sdb_set(key, '', __opts__) + salt.utils.sdb.sdb_set(key, None, __opts__) except Exception: pass From 676c18481fe30a20104814271d0c7568ec68a316 Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Tue, 26 Sep 2017 13:05:02 -0700 Subject: [PATCH 423/633] Fix trailing newline on grains The grains iscsi_iqn and fc_wwn have training newlines which cause show when calling the grain from a state file and trying to pass this to an execution module. Remove the training newline with rstrip() --- salt/grains/core.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/salt/grains/core.py b/salt/grains/core.py index c613c27d64..9adf2fd776 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -2517,7 +2517,8 @@ def _linux_iqn(): for line in _iscsi: if line.find('InitiatorName') != -1: iqn = line.split('=') - ret.extend([iqn[1]]) + final_iqn = iqn[1].rstrip() + ret.extend([final_iqn]) return ret @@ -2532,7 +2533,8 @@ def _aix_iqn(): aixret = __salt__['cmd.run'](aixcmd) if aixret[0].isalpha(): iqn = aixret.split() - ret.extend([iqn[1]]) + final_iqn = iqn[1].rstrip() + ret.extend([final_iqn]) return ret @@ -2545,6 +2547,7 @@ def _linux_wwns(): for fcfile in glob.glob('/sys/class/fc_host/*/port_name'): with salt.utils.files.fopen(fcfile, 'r') as _wwn: for line in _wwn: + line = line.rstrip() ret.extend([line[2:]]) return ret @@ -2571,6 +2574,7 @@ def _windows_iqn(): for line in cmdret['stdout'].splitlines(): if line[0].isalpha(): continue + line = line.rstrip() ret.extend([line]) return ret @@ -2587,6 +2591,7 @@ def _windows_wwns(): cmdret = __salt__['cmd.run_ps'](ps_cmd) for line in cmdret: + line = line.rstrip() ret.append(line) return ret From 8d27e20b631d1da84a4a184b229e51f15d232f92 Mon Sep 17 00:00:00 2001 From: vernoncole Date: Thu, 28 Sep 2017 16:43:47 -0600 Subject: [PATCH 424/633] replace salt.utils.sdb calls with __utils__ --- salt/modules/vagrant.py | 19 +++++++++---------- tests/unit/modules/test_vagrant.py | 11 ++++++++++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 5044b7fb63..dd28f1e653 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -35,7 +35,6 @@ import os # Import salt libs import salt.utils -import salt.utils.sdb from salt.exceptions import CommandExecutionError, SaltInvocationError import salt.ext.six as six @@ -56,7 +55,7 @@ def __virtual__(): run Vagrant commands if possible ''' # noinspection PyUnresolvedReferences - if salt.utils.path.which('vagrant') is None: + if __utils__['path.which']('vagrant') is None: return False, 'The vagrant module could not be loaded: vagrant command not found' return __virtualname__ @@ -85,11 +84,11 @@ def _build_machine_uri(machine, cwd): def _update_vm_info(name, vm_): ''' store the vm_ information keyed by name ''' - salt.utils.sdb.sdb_set(_build_sdb_uri(name), vm_, __opts__) + __utils__['sdb.sdb_set'](_build_sdb_uri(name), vm_, __opts__) # store machine-to-name mapping, too if vm_['machine']: - salt.utils.sdb.sdb_set( + __utils__['sdb.sdb_set']( _build_machine_uri(vm_['machine'], vm_.get('cwd', '.')), name, __opts__) @@ -103,7 +102,7 @@ def get_vm_info(name): :return: dictionary of {'machine': x, 'cwd': y, ...}. ''' try: - vm_ = salt.utils.sdb.sdb_get(_build_sdb_uri(name), __opts__) + vm_ = __utils__['sdb.sdb_get'](_build_sdb_uri(name), __opts__) except KeyError: raise SaltInvocationError( 'Probable sdb driver not found. Check your configuration.') @@ -121,7 +120,7 @@ def get_machine_id(machine, cwd): :param cwd: the path to Vagrantfile :return: salt_id name ''' - name = salt.utils.sdb.sdb_get(_build_machine_uri(machine, cwd), __opts__) + name = __utils__['sdb.sdb_get'](_build_machine_uri(machine, cwd), __opts__) return name @@ -139,20 +138,20 @@ def _erase_vm_info(name): if vm_['machine']: key = _build_machine_uri(vm_['machine'], vm_.get('cwd', '.')) try: - salt.utils.sdb.sdb_delete(key, __opts__) + __utils__['sdb.sdb_delete'](key, __opts__) except KeyError: # no delete method found -- load a blank value - salt.utils.sdb.sdb_set(key, None, __opts__) + __utils__['sdb.sdb_set'](key, None, __opts__) except Exception: pass uri = _build_sdb_uri(name) try: # delete the name record - salt.utils.sdb.sdb_delete(uri, __opts__) + __utils__['sdb.sdb_delete'](uri, __opts__) except KeyError: # no delete method found -- load an empty dictionary - salt.utils.sdb.sdb_set(uri, {}, __opts__) + __utils__['sdb.sdb_set'](uri, {}, __opts__) except Exception: pass diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index c6ba35c23a..4eccf547fc 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -12,6 +12,7 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON # Import salt libs import salt.modules.vagrant as vagrant import salt.modules.cmdmod as cmd +import salt.utils.sdb as sdb import salt.exceptions # Import third party libs @@ -27,7 +28,10 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): ''' @classmethod def tearDownClass(cls): - os.unlink(TEMP_DATABASE_FILE) + try: + os.unlink(TEMP_DATABASE_FILE) + except: + pass def setup_loader_modules(self): vagrant_globals = { @@ -44,6 +48,11 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): 'cmd.shell': cmd.shell, 'cmd.retcode': cmd.retcode, }, + '__utils__': { + 'sdb.sdb_set': sdb.sdb_set, + 'sdb.sdb_get': sdb.sdb_get, + 'sdb.sdb_delete': sdb.sdb_delete + } } return {vagrant: vagrant_globals} From e9661aea0795674e4695aa5cd056dddb5e293fbf Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Thu, 28 Sep 2017 18:27:54 -0600 Subject: [PATCH 425/633] lint: must not use bare except: even in teardown --- tests/unit/modules/test_vagrant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 4eccf547fc..8862b56793 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -30,7 +30,7 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): def tearDownClass(cls): try: os.unlink(TEMP_DATABASE_FILE) - except: + except OSError: pass def setup_loader_modules(self): From 8f58f7b8ab6b1a38c29b12a65ed2096c718d506a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B3=E8=89=B3=E8=8A=AC?= Date: Sun, 24 Sep 2017 11:49:39 +0800 Subject: [PATCH 426/633] bugfix: Catch OSError/IOError when check minion cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 申艳芬 --- salt/key.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/salt/key.py b/salt/key.py index 52b2d268e1..40881e479a 100644 --- a/salt/key.py +++ b/salt/key.py @@ -501,7 +501,13 @@ class Key(object): if os.path.isdir(m_cache): for minion in os.listdir(m_cache): if minion not in minions and minion not in preserve_minions: - shutil.rmtree(os.path.join(m_cache, minion)) + try: + shutil.rmtree(os.path.join(m_cache, minion)) + except (OSError, IOError) as ex: + log.warning('Key: Delete cache for %s got OSError/IOError: %s \n', + minion, + ex) + continue cache = salt.cache.factory(self.opts) clist = cache.list(self.ACC) if clist: @@ -979,7 +985,13 @@ class RaetKey(Key): if os.path.isdir(m_cache): for minion in os.listdir(m_cache): if minion not in minions and minion not in preserve_minions: - shutil.rmtree(os.path.join(m_cache, minion)) + try: + shutil.rmtree(os.path.join(m_cache, minion)) + except (OSError, IOError) as ex: + log.warning('RaetKey: Delete cache for %s got OSError/IOError: %s \n', + minion, + ex) + continue cache = salt.cache.factory(self.opts) clist = cache.list(self.ACC) if clist: From dbe363700a23a1c678abe4ef08f86ad65c4274d3 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:16:29 -0400 Subject: [PATCH 427/633] Fixes to several functions in modules.vsphere --- salt/modules/vsphere.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index c88485d555..d3ba2a2529 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -5672,7 +5672,6 @@ def remove_datastore(datastore, service_instance=None): ''' log.trace('Removing datastore \'{0}\''.format(datastore)) target = _get_proxy_target(service_instance) - taget_name = target.name datastores = salt.utils.vmware.get_datastores( service_instance, reference=target, @@ -6340,7 +6339,7 @@ def remove_capacity_from_diskgroup(cache_disk_id, capacity_disk_ids, 'capacity_ids': capacity_disk_ids}]}, schema) except jsonschema.exceptions.ValidationError as exc: - raise ArgumentValueError(exc) + raise ArgumentValueError(str(exc)) host_ref = _get_proxy_target(service_instance) hostname = __proxy__['esxi.get_details']()['esxi_host'] disks = salt.utils.vmware.get_disks(host_ref, disk_ids=capacity_disk_ids) @@ -6390,7 +6389,6 @@ def remove_diskgroup(cache_disk_id, data_accessibility=True, salt '*' vsphere.remove_diskgroup cache_disk_id='naa.000000000000001' ''' log.trace('Validating diskgroup input') - schema = DiskGroupsDiskIdSchema.serialize() host_ref = _get_proxy_target(service_instance) hostname = __proxy__['esxi.get_details']()['esxi_host'] diskgroups = \ From 51703a8a34ac966d371c76d7d0749123d19e42b2 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:18:10 -0400 Subject: [PATCH 428/633] Added tests for salt.modules.vsphere.erase_disk_partitions --- tests/unit/modules/test_vsphere.py | 86 ++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index ed043f2728..4062d09248 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -1173,6 +1173,92 @@ class CreateDatacenterTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(res, {'create_datacenter': True}) +@skipIf(NO_MOCK, NO_MOCK_REASON) +class EraseDiskPartitionsTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.modules.vsphere.erase_disk_partitions''' + def setup_loader_modules(self): + return { + vsphere: { + '__virtual__': MagicMock(return_value='vsphere'), + '_get_proxy_connection_details': MagicMock(), + '__proxy__': {'esxi.get_details': MagicMock( + return_value={'esxi_host': 'fake_host'})} + } + } + + def setUp(self): + attrs = (('mock_si', MagicMock()), + ('mock_host', MagicMock())) + for attr, mock_obj in attrs: + setattr(self, attr, mock_obj) + self.addCleanup(delattr, self, attr) + attrs = (('mock_proxy_target', MagicMock(return_value=self.mock_host)), + ('mock_erase_disk_partitions', MagicMock())) + for attr, mock_obj in attrs: + setattr(self, attr, mock_obj) + self.addCleanup(delattr, self, attr) + + patches = ( + ('salt.utils.vmware.get_service_instance', + MagicMock(return_value=self.mock_si)), + ('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxi')), + ('salt.modules.vsphere._get_proxy_target', + MagicMock(return_value=self.mock_host)), + ('salt.utils.vmware.erase_disk_partitions', + self.mock_erase_disk_partitions)) + for module, mock_obj in patches: + patcher = patch(module, mock_obj) + patcher.start() + self.addCleanup(patcher.stop) + + def test_supported_proxies(self): + supported_proxies = ['esxi'] + for proxy_type in supported_proxies: + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value=proxy_type)): + vsphere.erase_disk_partitions(disk_id='fake_disk') + + def test_no_disk_id_or_scsi_address(self): + with self.assertRaises(ArgumentValueError) as excinfo: + vsphere.erase_disk_partitions() + self.assertEqual('Either \'disk_id\' or \'scsi_address\' needs to ' + 'be specified', excinfo.exception.strerror) + + def test_get_proxy_target(self): + mock_test_proxy_target = MagicMock() + with patch('salt.modules.vsphere._get_proxy_target', + mock_test_proxy_target): + vsphere.erase_disk_partitions(disk_id='fake_disk') + mock_test_proxy_target.assert_called_once_with(self.mock_si) + + def test_scsi_address_not_found(self): + mock = MagicMock(return_value={'bad_scsi_address': 'bad_disk_id'}) + with patch('salt.utils.vmware.get_scsi_address_to_lun_map', mock): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vsphere.erase_disk_partitions(scsi_address='fake_scsi_address') + self.assertEqual('Scsi lun with address \'fake_scsi_address\' was ' + 'not found on host \'fake_host\'', + excinfo.exception.strerror) + + def test_scsi_address_to_disk_id_map(self): + mock_disk_id = MagicMock(canonicalName='fake_scsi_disk_id') + mock_get_scsi_addr_to_lun = \ + MagicMock(return_value={'fake_scsi_address': mock_disk_id}) + with patch('salt.utils.vmware.get_scsi_address_to_lun_map', + mock_get_scsi_addr_to_lun): + vsphere.erase_disk_partitions(scsi_address='fake_scsi_address') + mock_get_scsi_addr_to_lun.assert_called_once_with(self.mock_host) + self.mock_erase_disk_partitions.assert_called_once_with( + self.mock_si, self.mock_host, 'fake_scsi_disk_id', + hostname='fake_host') + + def test_erase_disk_partitions(self): + vsphere.erase_disk_partitions(disk_id='fake_disk_id') + self.mock_erase_disk_partitions.assert_called_once_with( + self.mock_si, self.mock_host, 'fake_disk_id', hostname='fake_host') + + @skipIf(NO_MOCK, NO_MOCK_REASON) class ListClusterTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.list_cluster''' From 4e0755de2069562cc39b535b65d0b9c041166f65 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:19:37 -0400 Subject: [PATCH 429/633] Added tests for salt.modules.vsphere.remove_datastore --- tests/unit/modules/test_vsphere.py | 87 ++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index 4062d09248..ec93b85521 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -1259,6 +1259,93 @@ class EraseDiskPartitionsTestCase(TestCase, LoaderModuleMockMixin): self.mock_si, self.mock_host, 'fake_disk_id', hostname='fake_host') +@skipIf(NO_MOCK, NO_MOCK_REASON) +class RemoveDatastoreTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.modules.vsphere.remove_datastore''' + def setup_loader_modules(self): + return { + vsphere: { + '__virtual__': MagicMock(return_value='vsphere'), + '_get_proxy_connection_details': MagicMock(), + 'get_proxy_type': MagicMock(return_value='esxdatacenter'), + } + } + + def setUp(self): + attrs = (('mock_si', MagicMock()), + ('mock_target', MagicMock()), + ('mock_ds', MagicMock())) + for attr, mock_obj in attrs: + setattr(self, attr, mock_obj) + self.addCleanup(delattr, self, attr) + + patches = ( + ('salt.utils.vmware.get_service_instance', + MagicMock(return_value=self.mock_si)), + ('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxdatacenter')), + ('salt.modules.vsphere._get_proxy_target', + MagicMock(return_value=self.mock_target)), + ('salt.utils.vmware.get_datastores', + MagicMock(return_value=[self.mock_ds])), + ('salt.utils.vmware.remove_datastore', MagicMock())) + for module, mock_obj in patches: + patcher = patch(module, mock_obj) + patcher.start() + self.addCleanup(patcher.stop) + + def test_supported_proxes(self): + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] + for proxy_type in supported_proxies: + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value=proxy_type)): + vsphere.remove_datastore(datastore='fake_ds_name') + + def test__get_proxy_target_call(self): + mock__get_proxy_target = MagicMock(return_value=self.mock_target) + with patch('salt.modules.vsphere._get_proxy_target', + mock__get_proxy_target): + vsphere.remove_datastore(datastore='fake_ds_name') + mock__get_proxy_target.assert_called_once_with(self.mock_si) + + def test_get_datastores_call(self): + mock_get_datastores = MagicMock() + with patch('salt.utils.vmware.get_datastores', + mock_get_datastores): + vsphere.remove_datastore(datastore='fake_ds') + mock_get_datastores.assert_called_once_with( + self.mock_si, reference=self.mock_target, + datastore_names=['fake_ds']) + + def test_datastore_not_found(self): + with patch('salt.utils.vmware.get_datastores', + MagicMock(return_value=[])): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vsphere.remove_datastore(datastore='fake_ds') + self.assertEqual('Datastore \'fake_ds\' was not found', + excinfo.exception.strerror) + + def test_multiple_datastores_found(self): + with patch('salt.utils.vmware.get_datastores', + MagicMock(return_value=[MagicMock(), MagicMock()])): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vsphere.remove_datastore(datastore='fake_ds') + self.assertEqual('Multiple datastores \'fake_ds\' were found', + excinfo.exception.strerror) + + def test_remove_datastore_call(self): + mock_remove_datastore = MagicMock() + with patch('salt.utils.vmware.remove_datastore', + mock_remove_datastore): + vsphere.remove_datastore(datastore='fake_ds') + mock_remove_datastore.assert_called_once_with( + self.mock_si, self.mock_ds) + + def test_success_output(self): + res = vsphere.remove_datastore(datastore='fake_ds') + self.assertTrue(res) + + @skipIf(NO_MOCK, NO_MOCK_REASON) class ListClusterTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.list_cluster''' From ec2637db44e0e232f674a7d4efb1795d3603a1eb Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:20:52 -0400 Subject: [PATCH 430/633] Added tests for salt.modules.vsphere.remove_diskgroup --- tests/unit/modules/test_vsphere.py | 92 ++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index ec93b85521..772e461f32 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -1346,6 +1346,98 @@ class RemoveDatastoreTestCase(TestCase, LoaderModuleMockMixin): self.assertTrue(res) +@skipIf(NO_MOCK, NO_MOCK_REASON) +class RemoveDiskgroupTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.modules.vsphere.remove_diskgroup''' + def setup_loader_modules(self): + return { + vsphere: { + '__virtual__': MagicMock(return_value='vsphere'), + '_get_proxy_connection_details': MagicMock(), + '__proxy__': {'esxi.get_details': MagicMock( + return_value={'esxi_host': 'fake_host'})} + } + } + + def setUp(self): + attrs = (('mock_si', MagicMock()), + ('mock_host', MagicMock()), + ('mock_diskgroup', MagicMock())) + for attr, mock_obj in attrs: + setattr(self, attr, mock_obj) + self.addCleanup(delattr, self, attr) + + patches = ( + ('salt.utils.vmware.get_service_instance', + MagicMock(return_value=self.mock_si)), + ('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxi')), + ('salt.modules.vsphere._get_proxy_target', + MagicMock(return_value=self.mock_host)), + ('salt.utils.vmware.get_diskgroups', + MagicMock(return_value=[self.mock_diskgroup])), + ('salt.utils.vsan.remove_diskgroup', MagicMock())) + for module, mock_obj in patches: + patcher = patch(module, mock_obj) + patcher.start() + self.addCleanup(patcher.stop) + + def test_supported_proxes(self): + supported_proxies = ['esxi'] + for proxy_type in supported_proxies: + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value=proxy_type)): + vsphere.remove_diskgroup(cache_disk_id='fake_disk_id') + + def test__get_proxy_target_call(self): + mock__get_proxy_target = MagicMock(return_value=self.mock_host) + with patch('salt.modules.vsphere._get_proxy_target', + mock__get_proxy_target): + vsphere.remove_diskgroup(cache_disk_id='fake_disk_id') + mock__get_proxy_target.assert_called_once_with(self.mock_si) + + def test_get_disk_groups(self): + mock_get_diskgroups = MagicMock(return_value=[self.mock_diskgroup]) + with patch('salt.utils.vmware.get_diskgroups', + mock_get_diskgroups): + vsphere.remove_diskgroup(cache_disk_id='fake_disk_id') + mock_get_diskgroups.assert_called_once_with( + self.mock_host, cache_disk_ids=['fake_disk_id']) + + def test_disk_group_not_found_safety_checks_set(self): + with patch('salt.utils.vmware.get_diskgroups', + MagicMock(return_value=[])): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vsphere.remove_diskgroup(cache_disk_id='fake_disk_id') + self.assertEqual('No diskgroup with cache disk id ' + '\'fake_disk_id\' was found in ESXi host ' + '\'fake_host\'', + excinfo.exception.strerror) + + def test_remove_disk_group(self): + mock_remove_diskgroup = MagicMock(return_value=None) + with patch('salt.utils.vsan.remove_diskgroup', + mock_remove_diskgroup): + vsphere.remove_diskgroup(cache_disk_id='fake_disk_id') + mock_remove_diskgroup.assert_called_once_with( + self.mock_si, self.mock_host, self.mock_diskgroup, + data_accessibility=True) + + def test_remove_disk_group_data_accessibility_false(self): + mock_remove_diskgroup = MagicMock(return_value=None) + with patch('salt.utils.vsan.remove_diskgroup', + mock_remove_diskgroup): + vsphere.remove_diskgroup(cache_disk_id='fake_disk_id', + data_accessibility=False) + mock_remove_diskgroup.assert_called_once_with( + self.mock_si, self.mock_host, self.mock_diskgroup, + data_accessibility=False) + + def test_success_output(self): + res = vsphere.remove_diskgroup(cache_disk_id='fake_disk_id') + self.assertTrue(res) + + @skipIf(NO_MOCK, NO_MOCK_REASON) class ListClusterTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.list_cluster''' From b4a05dc7c0e7e6540491475cd9d7916b34aa4921 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:21:40 -0400 Subject: [PATCH 431/633] Added tests for salt.modules.vsphere.remove_capacity_from_diskgroup --- tests/unit/modules/test_vsphere.py | 166 +++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index 772e461f32..004149ebc0 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -1438,6 +1438,172 @@ class RemoveDiskgroupTestCase(TestCase, LoaderModuleMockMixin): self.assertTrue(res) +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not vsphere.HAS_JSONSCHEMA, 'The \'jsonschema\' library is missing') +class RemoveCapacityFromDiskgroupTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.modules.vsphere.remove_capacity_from_diskgroup''' + def setup_loader_modules(self): + return { + vsphere: { + '__virtual__': MagicMock(return_value='vsphere'), + '_get_proxy_connection_details': MagicMock(), + '__proxy__': {'esxi.get_details': MagicMock( + return_value={'esxi_host': 'fake_host'})} + } + } + + def setUp(self): + attrs = (('mock_si', MagicMock()), + ('mock_schema', MagicMock()), + ('mock_host', MagicMock()), + ('mock_disk1', MagicMock(canonicalName='fake_disk1')), + ('mock_disk2', MagicMock(canonicalName='fake_disk2')), + ('mock_disk3', MagicMock(canonicalName='fake_disk3')), + ('mock_diskgroup', MagicMock())) + for attr, mock_obj in attrs: + setattr(self, attr, mock_obj) + self.addCleanup(delattr, self, attr) + + patches = ( + ('salt.utils.vmware.get_service_instance', + MagicMock(return_value=self.mock_si)), + ('salt.modules.vsphere.DiskGroupsDiskIdSchema.serialize', + MagicMock(return_value=self.mock_schema)), + ('salt.modules.vsphere.jsonschema.validate', MagicMock()), + ('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='esxi')), + ('salt.modules.vsphere._get_proxy_target', + MagicMock(return_value=self.mock_host)), + ('salt.utils.vmware.get_disks', + MagicMock(return_value=[self.mock_disk1, self.mock_disk2, + self.mock_disk3])), + ('salt.utils.vmware.get_diskgroups', + MagicMock(return_value=[self.mock_diskgroup])), + ('salt.utils.vsan.remove_capacity_from_diskgroup', MagicMock())) + for module, mock_obj in patches: + patcher = patch(module, mock_obj) + patcher.start() + self.addCleanup(patcher.stop) + + def test_validate(self): + mock_schema_validate = MagicMock() + with patch('salt.modules.vsphere.jsonschema.validate', + mock_schema_validate): + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2']) + mock_schema_validate.assert_called_once_with( + {'diskgroups': [{'cache_id': 'fake_cache_disk_id', + 'capacity_ids': ['fake_disk1', + 'fake_disk2']}]}, + self.mock_schema) + + def test_invalid_schema_validation(self): + mock_schema_validate = MagicMock( + side_effect=vsphere.jsonschema.exceptions.ValidationError('err')) + with patch('salt.modules.vsphere.jsonschema.validate', + mock_schema_validate): + with self.assertRaises(ArgumentValueError) as excinfo: + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2']) + self.assertEqual('err', excinfo.exception.strerror) + + def test_supported_proxes(self): + supported_proxies = ['esxi'] + for proxy_type in supported_proxies: + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value=proxy_type)): + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2']) + + def test__get_proxy_target_call(self): + mock__get_proxy_target = MagicMock(return_value=self.mock_host) + with patch('salt.modules.vsphere._get_proxy_target', + mock__get_proxy_target): + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2']) + mock__get_proxy_target.assert_called_once_with(self.mock_si) + + def test_get_disks(self): + mock_get_disks = MagicMock( + return_value=[self.mock_disk1, self.mock_disk2, self.mock_disk3]) + with patch('salt.utils.vmware.get_disks', mock_get_disks): + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2']) + mock_get_disks.assert_called_once_with( + self.mock_host, disk_ids=['fake_disk1', 'fake_disk2']) + + def test_disk_not_found_safety_checks_set(self): + mock_get_disks = MagicMock( + return_value=[self.mock_disk1, self.mock_disk2, self.mock_disk3]) + with patch('salt.utils.vmware.get_disks', mock_get_disks): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk4'], + safety_checks=True) + self.assertEqual('No disk with id \'fake_disk4\' was found ' + 'in ESXi host \'fake_host\'', + excinfo.exception.strerror) + + def test_get_diskgroups(self): + mock_get_diskgroups = MagicMock(return_value=[self.mock_diskgroup]) + with patch('salt.utils.vmware.get_diskgroups', + mock_get_diskgroups): + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2']) + mock_get_diskgroups.assert_called_once_with( + self.mock_host, cache_disk_ids=['fake_cache_disk_id']) + + def test_diskgroup_not_found(self): + with patch('salt.utils.vmware.get_diskgroups', + MagicMock(return_value=[])): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2']) + self.assertEqual('No diskgroup with cache disk id ' + '\'fake_cache_disk_id\' was found in ESXi host ' + '\'fake_host\'', + excinfo.exception.strerror) + + def test_remove_capacity_from_diskgroup(self): + mock_remove_capacity_from_diskgroup = MagicMock() + with patch('salt.utils.vsan.remove_capacity_from_diskgroup', + mock_remove_capacity_from_diskgroup): + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2']) + mock_remove_capacity_from_diskgroup.assert_called_once_with( + self.mock_si, self.mock_host, self.mock_diskgroup, + capacity_disks=[self.mock_disk1, self.mock_disk2], + data_evacuation=True) + + def test_remove_capacity_from_diskgroup_data_evacuation_false(self): + mock_remove_capacity_from_diskgroup = MagicMock() + with patch('salt.utils.vsan.remove_capacity_from_diskgroup', + mock_remove_capacity_from_diskgroup): + vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2'], + data_evacuation=False) + mock_remove_capacity_from_diskgroup.assert_called_once_with( + self.mock_si, self.mock_host, self.mock_diskgroup, + capacity_disks=[self.mock_disk1, self.mock_disk2], + data_evacuation=False) + + def test_success_output(self): + res = vsphere.remove_capacity_from_diskgroup( + cache_disk_id='fake_cache_disk_id', + capacity_disk_ids=['fake_disk1', 'fake_disk2']) + self.assertTrue(res) + + @skipIf(NO_MOCK, NO_MOCK_REASON) class ListClusterTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.list_cluster''' From dac9522814e49ad40251378a04f3a6d1e1a8a0c2 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:23:14 -0400 Subject: [PATCH 432/633] Added tests for salt.utils.vsan.get_vsan_disk_management_system --- tests/unit/utils/test_vsan.py | 65 ++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/unit/utils/test_vsan.py b/tests/unit/utils/test_vsan.py index 9d76d6dcae..376d445f77 100644 --- a/tests/unit/utils/test_vsan.py +++ b/tests/unit/utils/test_vsan.py @@ -16,7 +16,8 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, \ PropertyMock # Import Salt libraries -from salt.exceptions import VMwareApiError, VMwareRuntimeError +from salt.exceptions import VMwareApiError, VMwareRuntimeError, \ + VMwareObjectRetrievalError from salt.utils import vsan try: @@ -137,6 +138,68 @@ class GetVsanClusterConfigSystemTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(ret, self.mock_ret) +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'pyvsan\' bindings are missing') +class GetVsanDiskManagementSystemTestCase(TestCase, LoaderModuleMockMixin): + '''Tests for salt.utils.vsan.get_vsan_disk_management_system''' + def setup_loader_modules(self): + return {vsan: { + '__virtual__': MagicMock(return_value='vsan'), + 'sys': MagicMock(), + 'ssl': MagicMock()}} + + def setUp(self): + self.stub_mock = MagicMock() + self.si_mock = MagicMock(_stub=self.stub_mock) + + def setUp(self): + self.mock_si = MagicMock() + self.mock_ret = MagicMock() + patches = (('salt.utils.vsan.vsanapiutils.GetVsanVcMos', + MagicMock( + return_value={'vsan-disk-management-system': + self.mock_ret})),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + type(vsan.sys).version_info = PropertyMock(return_value=(2, 7, 9)) + self.mock_context = MagicMock() + self.mock_create_default_context = \ + MagicMock(return_value=self.mock_context) + vsan.ssl.create_default_context = self.mock_create_default_context + + def tearDown(self): + for attr in ('mock_si', 'mock_ret', 'mock_context', + 'mock_create_default_context'): + delattr(self, attr) + + def test_ssl_default_context_loaded(self): + vsan.get_vsan_disk_management_system(self.mock_si) + self.mock_create_default_context.assert_called_once_with() + self.assertFalse(self.mock_context.check_hostname) + self.assertEqual(self.mock_context.verify_mode, vsan.ssl.CERT_NONE) + + def test_ssl_default_context_not_loaded(self): + type(vsan.sys).version_info = PropertyMock(return_value=(2, 7, 8)) + vsan.get_vsan_disk_management_system(self.mock_si) + self.assertEqual(self.mock_create_default_context.call_count, 0) + + def test_GetVsanVcMos_call(self): + mock_get_vsan_vc_mos = MagicMock() + with patch('salt.utils.vsan.vsanapiutils.GetVsanVcMos', + mock_get_vsan_vc_mos): + vsan.get_vsan_disk_management_system(self.mock_si) + mock_get_vsan_vc_mos.assert_called_once_with(self.mock_si._stub, + context=self.mock_context) + + def test_return(self): + ret = vsan.get_vsan_disk_management_system(self.mock_si) + self.assertEqual(ret, self.mock_ret) + + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') @skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') From 523a2ac21d33062ae8cf40af61fed48b73b418f8 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:24:17 -0400 Subject: [PATCH 433/633] Added tests for salt.utils.vsan.get_host_vsan_system --- tests/unit/utils/test_vsan.py | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/unit/utils/test_vsan.py b/tests/unit/utils/test_vsan.py index 376d445f77..1b53015f2b 100644 --- a/tests/unit/utils/test_vsan.py +++ b/tests/unit/utils/test_vsan.py @@ -200,6 +200,90 @@ class GetVsanDiskManagementSystemTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(ret, self.mock_ret) + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class GetHostVsanSystemTestCase(TestCase): + '''Tests for salt.utils.vsan.get_host_vsan_system''' + + def setUp(self): + self.mock_host_ref = MagicMock() + self.mock_si = MagicMock() + self.mock_traversal_spec = MagicMock() + self.mock_vsan_system = MagicMock() + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_hostname')), + ('salt.utils.vsan.vmodl.query.PropertyCollector.TraversalSpec', + MagicMock(return_value=self.mock_traversal_spec)), + ('salt.utils.vmware.get_mors_with_properties', + MagicMock(return_value=self.mock_traversal_spec)), + ('salt.utils.vmware.get_mors_with_properties', + MagicMock(return_value=[{'object': self.mock_vsan_system}]))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def test_get_hostname(self): + mock_get_managed_object_name = MagicMock(return_value='fake_hostname') + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vsan.get_host_vsan_system(self.mock_si, self.mock_host_ref) + mock_get_managed_object_name.assert_called_once_with( + self.mock_host_ref) + + def test_hostname_argument(self): + mock_get_managed_object_name = MagicMock(return_value='fake_hostname') + with patch('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_hostname')): + vsan.get_host_vsan_system(self.mock_si, + self.mock_host_ref, + hostname='passedin_hostname') + self.assertEqual(mock_get_managed_object_name.call_count, 0) + + def test_traversal_spec(self): + mock_traversal_spec = MagicMock(return_value=self.mock_traversal_spec) + with patch( + 'salt.utils.vmware.vmodl.query.PropertyCollector.TraversalSpec', + mock_traversal_spec): + + vsan.get_host_vsan_system(self.mock_si, self.mock_host_ref) + mock_traversal_spec.assert_called_once_with( + path='configManager.vsanSystem', + type=vim.HostSystem, + skip=False) + + def test_get_mors_with_properties(self): + mock_get_mors = \ + MagicMock(return_value=[{'object': self.mock_vsan_system}]) + with patch('salt.utils.vmware.get_mors_with_properties', + mock_get_mors): + vsan.get_host_vsan_system(self.mock_si, self.mock_host_ref) + mock_get_mors.assert_called_once_with( + self.mock_si, + vim.HostVsanSystem, + property_list=['config.enabled'], + container_ref=self.mock_host_ref, + traversal_spec=self.mock_traversal_spec) + + def test_empty_mors_result(self): + mock_get_mors = MagicMock(return_value=None) + with patch('salt.utils.vmware.get_mors_with_properties', + mock_get_mors): + + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + vsan.get_host_vsan_system(self.mock_si, self.mock_host_ref) + self.assertEqual(excinfo.exception.strerror, + 'Host\'s \'fake_hostname\' VSAN system was ' + 'not retrieved') + + def test_valid_mors_result(self): + res = vsan.get_host_vsan_system(self.mock_si, self.mock_host_ref) + self.assertEqual(res, self.mock_vsan_system) + + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') @skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') From d813164d185f8a35bec6a69148cd53349d417552 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:25:15 -0400 Subject: [PATCH 434/633] Added tests for salt.utils.vsan.create_diskgroup --- tests/unit/utils/test_vsan.py | 129 ++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tests/unit/utils/test_vsan.py b/tests/unit/utils/test_vsan.py index 1b53015f2b..8dea5050d1 100644 --- a/tests/unit/utils/test_vsan.py +++ b/tests/unit/utils/test_vsan.py @@ -284,6 +284,135 @@ class GetHostVsanSystemTestCase(TestCase): self.assertEqual(res, self.mock_vsan_system) +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class CreateDiskgroupTestCase(TestCase): + '''Tests for salt.utils.vsan.create_diskgroup''' + def setUp(self): + self.mock_si = MagicMock() + self.mock_task = MagicMock() + self.mock_initialise_disk_mapping = \ + MagicMock(return_value=self.mock_task) + self.mock_vsan_disk_mgmt_system = MagicMock( + InitializeDiskMappings=self.mock_initialise_disk_mapping) + self.mock_host_ref = MagicMock() + self.mock_cache_disk = MagicMock() + self.mock_cap_disk1 = MagicMock() + self.mock_cap_disk2 = MagicMock() + self.mock_spec = MagicMock() + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_hostname')), + ('salt.utils.vsan.vim.VimVsanHostDiskMappingCreationSpec', + MagicMock(return_value=self.mock_spec)), + ('salt.utils.vsan._wait_for_tasks', MagicMock())) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def test_get_hostname(self): + mock_get_managed_object_name = MagicMock(return_value='fake_hostname') + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vsan.create_diskgroup(self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + mock_get_managed_object_name.assert_called_once_with( + self.mock_host_ref) + + def test_vsan_spec_all_flash(self): + self.mock_cap_disk1.ssd = True + vsan.create_diskgroup(self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(self.mock_spec.capacityDisks, [self.mock_cap_disk1, + self.mock_cap_disk2]) + self.assertEqual(self.mock_spec.cacheDisks, [self.mock_cache_disk]) + self.assertEqual(self.mock_spec.creationType, 'allFlash') + self.assertEqual(self.mock_spec.host, self.mock_host_ref) + + def test_vsan_spec_hybrid(self): + self.mock_cap_disk1.ssd = False + vsan.create_diskgroup(self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.mock_cap_disk1.ssd = False + self.assertEqual(self.mock_spec.creationType, 'hybrid') + + def test_initialize_disk_mapping(self): + vsan.create_diskgroup(self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.mock_initialise_disk_mapping.assert_called_once_with( + self.mock_spec) + + def test_initialize_disk_mapping_raise_no_permission(self): + err = vim.fault.NoPermission() + err.privilegeId = 'Fake privilege' + self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.create_diskgroup(self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_initialize_disk_mapping_raise_vim_fault(self): + err = vim.fault.VimFault() + err.msg = 'vim_fault' + self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.create_diskgroup(self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, 'vim_fault') + + def test_initialize_disk_mapping_raise_method_not_found(self): + err = vmodl.fault.MethodNotFound() + err.method = 'fake_method' + self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan.create_diskgroup(self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, + 'Method \'fake_method\' not found') + + def test_initialize_disk_mapping_raise_runtime_fault(self): + err = vmodl.RuntimeFault() + err.msg = 'runtime_fault' + self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan.create_diskgroup(self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, 'runtime_fault') + + def test__wait_for_tasks(self): + mock___wait_for_tasks = MagicMock() + with patch('salt.utils.vsan._wait_for_tasks', + mock___wait_for_tasks): + vsan.create_diskgroup(self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + mock___wait_for_tasks.assert_called_once_with( + [self.mock_task], self.mock_si) + + def test_result(self): + res = vsan.create_diskgroup(self.mock_si, + self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_cache_disk, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertTrue(res) + + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') @skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') From 32d44142f20f62980a055d31cb297565d80c7ad1 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:26:14 -0400 Subject: [PATCH 435/633] Added tests for salt.utils.vsan.add_capacity_to_diskgroup --- tests/unit/utils/test_vsan.py | 139 ++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/tests/unit/utils/test_vsan.py b/tests/unit/utils/test_vsan.py index 8dea5050d1..0d549f898a 100644 --- a/tests/unit/utils/test_vsan.py +++ b/tests/unit/utils/test_vsan.py @@ -413,6 +413,145 @@ class CreateDiskgroupTestCase(TestCase): self.assertTrue(res) +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class AddCapacityToDiskGroupTestCase(TestCase): + '''Tests for salt.utils.vsan.add_capacity_to_diskgroup''' + def setUp(self): + self.mock_si = MagicMock() + self.mock_task = MagicMock() + self.mock_initialise_disk_mapping = \ + MagicMock(return_value=self.mock_task) + self.mock_vsan_disk_mgmt_system = MagicMock( + InitializeDiskMappings=self.mock_initialise_disk_mapping) + self.mock_host_ref = MagicMock() + self.mock_cache_disk = MagicMock() + self.mock_diskgroup = MagicMock(ssd=self.mock_cache_disk) + self.mock_cap_disk1 = MagicMock() + self.mock_cap_disk2 = MagicMock() + self.mock_spec = MagicMock() + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_hostname')), + ('salt.utils.vsan.vim.VimVsanHostDiskMappingCreationSpec', + MagicMock(return_value=self.mock_spec)), + ('salt.utils.vsan._wait_for_tasks', MagicMock())) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def test_get_hostname(self): + mock_get_managed_object_name = MagicMock(return_value='fake_hostname') + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + mock_get_managed_object_name.assert_called_once_with( + self.mock_host_ref) + + def test_vsan_spec_all_flash(self): + self.mock_cap_disk1.ssd = True + vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(self.mock_spec.capacityDisks, [self.mock_cap_disk1, + self.mock_cap_disk2]) + self.assertEqual(self.mock_spec.cacheDisks, [self.mock_cache_disk]) + self.assertEqual(self.mock_spec.creationType, 'allFlash') + self.assertEqual(self.mock_spec.host, self.mock_host_ref) + + def test_vsan_spec_hybrid(self): + self.mock_cap_disk1.ssd = False + vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.mock_cap_disk1.ssd = False + self.assertEqual(self.mock_spec.creationType, 'hybrid') + + def test_initialize_disk_mapping(self): + vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.mock_initialise_disk_mapping.assert_called_once_with( + self.mock_spec) + + def test_initialize_disk_mapping_raise_no_permission(self): + err = vim.fault.NoPermission() + err.privilegeId = 'Fake privilege' + self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_initialize_disk_mapping_raise_vim_fault(self): + err = vim.fault.VimFault() + err.msg = 'vim_fault' + self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, 'vim_fault') + + def test_initialize_disk_mapping_raise_method_not_found(self): + err = vmodl.fault.MethodNotFound() + err.method = 'fake_method' + self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, + 'Method \'fake_method\' not found') + + def test_initialize_disk_mapping_raise_runtime_fault(self): + err = vmodl.RuntimeFault() + err.msg = 'runtime_fault' + self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, 'runtime_fault') + + def test__wait_for_tasks(self): + mock___wait_for_tasks = MagicMock() + with patch('salt.utils.vsan._wait_for_tasks', + mock___wait_for_tasks): + vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + mock___wait_for_tasks.assert_called_once_with( + [self.mock_task], self.mock_si) + + def test_result(self): + res = vsan.add_capacity_to_diskgroup( + self.mock_si, self.mock_vsan_disk_mgmt_system, + self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertTrue(res) + + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') @skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') From 9de868fdfbe65463a7c4ba7a58ec9cdcb3f73480 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:27:12 -0400 Subject: [PATCH 436/633] Added tests for salt.utils.vsan.remove_capacity_from_diskgroup --- tests/unit/utils/test_vsan.py | 117 ++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/tests/unit/utils/test_vsan.py b/tests/unit/utils/test_vsan.py index 0d549f898a..d415ac4fe5 100644 --- a/tests/unit/utils/test_vsan.py +++ b/tests/unit/utils/test_vsan.py @@ -552,6 +552,123 @@ class AddCapacityToDiskGroupTestCase(TestCase): self.assertTrue(res) +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class RemoveCapacityFromDiskGroup(TestCase): + '''Tests for salt.utils.vsan.remove_capacity_from_diskgroup''' + def setUp(self): + self.mock_si = MagicMock() + self.mock_task = MagicMock() + self.mock_remove_disk = \ + MagicMock(return_value=self.mock_task) + self.mock_host_vsan_system = MagicMock( + RemoveDisk_Task=self.mock_remove_disk) + self.mock_host_ref = MagicMock() + self.mock_cache_disk = MagicMock() + self.mock_diskgroup = MagicMock(ssd=self.mock_cache_disk) + self.mock_cap_disk1 = MagicMock() + self.mock_cap_disk2 = MagicMock() + self.mock_spec = MagicMock() + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_hostname')), + ('salt.utils.vsan.get_host_vsan_system', + MagicMock(return_value=self.mock_host_vsan_system)), + ('salt.utils.vsan.vim.HostMaintenanceSpec', + MagicMock(return_value=self.mock_spec)), + ('salt.utils.vsan.vim.VsanHostDecommissionMode', MagicMock()), + ('salt.utils.vmware.wait_for_task', MagicMock())) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def test_get_hostname(self): + mock_get_managed_object_name = MagicMock(return_value='fake_hostname') + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + mock_get_managed_object_name.assert_called_once_with( + self.mock_host_ref) + + def test_maintenance_mode_evacuate_all_data(self): + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(self.mock_spec.vsanMode.objectAction, + vim.VsanHostDecommissionModeObjectAction.evacuateAllData) + + def test_maintenance_mode_no_action(self): + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2], + data_evacuation=False) + self.assertEqual(self.mock_spec.vsanMode.objectAction, + vim.VsanHostDecommissionModeObjectAction.noAction) + + def test_remove_disk(self): + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.mock_remove_disk.assert_called_once_with( + disk=[self.mock_cap_disk1, self.mock_cap_disk2], + maintenanceSpec=self.mock_spec) + + def test_remove_disk_raise_no_permission(self): + err = vim.fault.NoPermission() + err.privilegeId = 'Fake privilege' + self.mock_host_vsan_system.RemoveDisk_Task= \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_remove_disk_raise_vim_fault(self): + err = vim.fault.VimFault() + err.msg = 'vim_fault' + self.mock_host_vsan_system.RemoveDisk_Task= \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, 'vim_fault') + + def test_remove_disk_raise_runtime_fault(self): + err = vmodl.RuntimeFault() + err.msg = 'runtime_fault' + self.mock_host_vsan_system.RemoveDisk_Task= \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(excinfo.exception.strerror, 'runtime_fault') + + def test_wait_for_tasks(self): + mock_wait_for_task = MagicMock() + with patch('salt.utils.vmware.wait_for_task', + mock_wait_for_task): + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + mock_wait_for_task.assert_called_once_with( + self.mock_task, 'fake_hostname', 'remove_capacity') + + def test_result(self): + res = vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertTrue(res) + + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') @skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') From 0c6a49ba38d68e93263433408b118e325d792930 Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 13:28:05 -0400 Subject: [PATCH 437/633] Added tests for salt.utils.vsan.remove_diskgroup --- tests/unit/utils/test_vsan.py | 119 ++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/unit/utils/test_vsan.py b/tests/unit/utils/test_vsan.py index d415ac4fe5..ab7ccd5a21 100644 --- a/tests/unit/utils/test_vsan.py +++ b/tests/unit/utils/test_vsan.py @@ -669,6 +669,125 @@ class RemoveCapacityFromDiskGroup(TestCase): self.assertTrue(res) +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +@skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') +class RemoveDiskgroup(TestCase): + '''Tests for salt.utils.vsan.remove_diskgroup''' + def setUp(self): + self.mock_si = MagicMock() + self.mock_task = MagicMock() + self.mock_remove_disk_mapping = \ + MagicMock(return_value=self.mock_task) + self.mock_host_vsan_system = MagicMock( + RemoveDiskMapping_Task=self.mock_remove_disk_mapping) + self.mock_host_ref = MagicMock() + self.mock_cache_disk = MagicMock() + self.mock_diskgroup = MagicMock(ssd=self.mock_cache_disk) + self.mock_cap_disk1 = MagicMock() + self.mock_cap_disk2 = MagicMock() + self.mock_spec = MagicMock() + patches = ( + ('salt.utils.vmware.get_managed_object_name', + MagicMock(return_value='fake_hostname')), + ('salt.utils.vsan.get_host_vsan_system', + MagicMock(return_value=self.mock_host_vsan_system)), + ('salt.utils.vsan.vim.HostMaintenanceSpec', + MagicMock(return_value=self.mock_spec)), + ('salt.utils.vsan.vim.VsanHostDecommissionMode', MagicMock()), + ('salt.utils.vmware.wait_for_task', MagicMock())) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def test_get_hostname(self): + mock_get_managed_object_name = MagicMock(return_value='fake_hostname') + with patch('salt.utils.vmware.get_managed_object_name', + mock_get_managed_object_name): + vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + mock_get_managed_object_name.assert_called_once_with( + self.mock_host_ref) + + def test_maintenance_mode_evacuate_all_data(self): + vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.assertEqual(self.mock_spec.vsanMode.objectAction, + vim.VsanHostDecommissionModeObjectAction.evacuateAllData) + + def test_maintenance_mode_no_action(self): + vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2], + data_evacuation=False) + self.assertEqual(self.mock_spec.vsanMode.objectAction, + vim.VsanHostDecommissionModeObjectAction.noAction) + + def test_remove_disk_mapping(self): + vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + vsan.remove_capacity_from_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup, + [self.mock_cap_disk1, self.mock_cap_disk2]) + self.mock_remove_disk_mapping.assert_called_once_with( + mapping=[self.mock_diskgroup], + maintenanceSpec=self.mock_spec) + + def test_remove_disk_mapping_raise_no_permission(self): + vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + err = vim.fault.NoPermission() + err.privilegeId = 'Fake privilege' + self.mock_host_vsan_system.RemoveDiskMapping_Task= \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_remove_disk_mapping_raise_vim_fault(self): + err = vim.fault.VimFault() + err.msg = 'vim_fault' + self.mock_host_vsan_system.RemoveDiskMapping_Task= \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareApiError) as excinfo: + vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + self.assertEqual(excinfo.exception.strerror, 'vim_fault') + + def test_remove_disk_mapping_raise_runtime_fault(self): + err = vmodl.RuntimeFault() + err.msg = 'runtime_fault' + self.mock_host_vsan_system.RemoveDiskMapping_Task= \ + MagicMock(side_effect=err) + with self.assertRaises(VMwareRuntimeError) as excinfo: + vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + self.assertEqual(excinfo.exception.strerror, 'runtime_fault') + + def test_wait_for_tasks(self): + mock_wait_for_task = MagicMock() + with patch('salt.utils.vmware.wait_for_task', + mock_wait_for_task): + vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + mock_wait_for_task.assert_called_once_with( + self.mock_task, 'fake_hostname', 'remove_diskgroup') + + def test_result(self): + res = vsan.remove_diskgroup( + self.mock_si, self.mock_host_ref, self.mock_diskgroup) + self.assertTrue(res) + + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') @skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') From c976df7c09f331e1e38909322ead04a5537a20f9 Mon Sep 17 00:00:00 2001 From: rallytime Date: Fri, 29 Sep 2017 13:56:01 -0400 Subject: [PATCH 438/633] Fix mocking in test_status since the "which" util has moved --- tests/unit/modules/test_status.py | 60 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/unit/modules/test_status.py b/tests/unit/modules/test_status.py index 87962c2a3d..24efc397c5 100644 --- a/tests/unit/modules/test_status.py +++ b/tests/unit/modules/test_status.py @@ -79,19 +79,19 @@ class StatusTestCase(TestCase, LoaderModuleMockMixin): is_darwin=MagicMock(return_value=False), is_freebsd=MagicMock(return_value=False), is_openbsd=MagicMock(return_value=False), - is_netbsd=MagicMock(return_value=False), - which=MagicMock(return_value=True)): - with patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value=os.linesep.join(['1', '2', '3']))}): - with patch('time.time', MagicMock(return_value=m.now)): - with patch('os.path.exists', MagicMock(return_value=True)): - proc_uptime = '{0} {1}'.format(m.ut, m.idle) - with patch('salt.utils.files.fopen', mock_open(read_data=proc_uptime)): - ret = status.uptime() - self.assertDictEqual(ret, m.ret) + is_netbsd=MagicMock(return_value=False)), \ + patch('salt.utils.path.which', MagicMock(return_value=True)), \ + patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value=os.linesep.join(['1', '2', '3']))}), \ + patch('time.time', MagicMock(return_value=m.now)), \ + patch('os.path.exists', MagicMock(return_value=True)): + proc_uptime = '{0} {1}'.format(m.ut, m.idle) - with patch('os.path.exists', MagicMock(return_value=False)): - with self.assertRaises(CommandExecutionError): - status.uptime() + with patch('salt.utils.files.fopen', mock_open(read_data=proc_uptime)): + ret = status.uptime() + self.assertDictEqual(ret, m.ret) + with patch('os.path.exists', MagicMock(return_value=False)): + with self.assertRaises(CommandExecutionError): + status.uptime() def test_uptime_sunos(self): ''' @@ -105,14 +105,13 @@ class StatusTestCase(TestCase, LoaderModuleMockMixin): is_darwin=MagicMock(return_value=False), is_freebsd=MagicMock(return_value=False), is_openbsd=MagicMock(return_value=False), - is_netbsd=MagicMock(return_value=False), - which=MagicMock(return_value=True)): - - with patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value=os.linesep.join(['1', '2', '3'])), - 'cmd.run_all': MagicMock(return_value=m2.ret)}): - with patch('time.time', MagicMock(return_value=m.now)): - ret = status.uptime() - self.assertDictEqual(ret, m.ret) + is_netbsd=MagicMock(return_value=False)), \ + patch('salt.utils.path.which', MagicMock(return_value=True)), \ + patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value=os.linesep.join(['1', '2', '3'])), + 'cmd.run_all': MagicMock(return_value=m2.ret)}), \ + patch('time.time', MagicMock(return_value=m.now)): + ret = status.uptime() + self.assertDictEqual(ret, m.ret) def test_uptime_macos(self): ''' @@ -128,17 +127,18 @@ class StatusTestCase(TestCase, LoaderModuleMockMixin): is_darwin=MagicMock(return_value=True), is_freebsd=MagicMock(return_value=False), is_openbsd=MagicMock(return_value=False), - is_netbsd=MagicMock(return_value=False), - which=MagicMock(return_value=True)): - with patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value=os.linesep.join(['1', '2', '3'])), - 'sysctl.get': MagicMock(return_value=kern_boottime)}): - with patch('time.time', MagicMock(return_value=m.now)): - ret = status.uptime() - self.assertDictEqual(ret, m.ret) + is_netbsd=MagicMock(return_value=False)), \ + patch('salt.utils.path.which', MagicMock(return_value=True)), \ + patch.dict(status.__salt__, {'cmd.run': MagicMock(return_value=os.linesep.join(['1', '2', '3'])), + 'sysctl.get': MagicMock(return_value=kern_boottime)}), \ + patch('time.time', MagicMock(return_value=m.now)): - with patch.dict(status.__salt__, {'sysctl.get': MagicMock(return_value='')}): - with self.assertRaises(CommandExecutionError): - status.uptime() + ret = status.uptime() + self.assertDictEqual(ret, m.ret) + + with patch.dict(status.__salt__, {'sysctl.get': MagicMock(return_value='')}): + with self.assertRaises(CommandExecutionError): + status.uptime() def test_uptime_return_success_not_supported(self): ''' From e0d3028bb057eef4c662171d686dc2d85a98d855 Mon Sep 17 00:00:00 2001 From: rallytime Date: Fri, 29 Sep 2017 14:25:18 -0400 Subject: [PATCH 439/633] Reduce the number of days an issue is stale by 25 --- .github/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 35928803a7..38e95f8702 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,8 +1,8 @@ # Probot Stale configuration file # Number of days of inactivity before an issue becomes stale -# 1000 is approximately 2 years and 9 months -daysUntilStale: 1000 +# 975 is approximately 2 years and 8 months +daysUntilStale: 975 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 From fd1d6c31dee42abbcf3fe7ed92ab261e543bc159 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 29 Sep 2017 12:52:32 -0600 Subject: [PATCH 440/633] Fix `unit.states.test_augeas` for Windows Mock `os.path.isfile` so the test will continue --- tests/unit/states/test_augeas.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/states/test_augeas.py b/tests/unit/states/test_augeas.py index dd316ea7fb..392e1e02b5 100644 --- a/tests/unit/states/test_augeas.py +++ b/tests/unit/states/test_augeas.py @@ -140,7 +140,8 @@ class AugeasTestCase(TestCase, LoaderModuleMockMixin): 'augeas.method_map': self.mock_method_map} with patch.dict(augeas.__salt__, mock_dict_): mock_filename = MagicMock(return_value='/etc/services') - with patch.object(augeas, '_workout_filename', mock_filename): + with patch.object(augeas, '_workout_filename', mock_filename), \ + patch('os.path.isfile', MagicMock(return_value=True)): with patch('salt.utils.fopen', MagicMock(mock_open)): mock_diff = MagicMock(return_value=['+ zabbix-agent']) with patch('difflib.unified_diff', mock_diff): From 0878dbd0e8cf2d86e532942df74c35a20975b19c Mon Sep 17 00:00:00 2001 From: apapp Date: Fri, 29 Sep 2017 15:03:56 -0400 Subject: [PATCH 441/633] add -n with netstat so we don't resolve --- pkg/rpm/salt-minion | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/rpm/salt-minion b/pkg/rpm/salt-minion index c30af52bc9..68ee88b081 100755 --- a/pkg/rpm/salt-minion +++ b/pkg/rpm/salt-minion @@ -67,7 +67,7 @@ _su_cmd() { _get_pid() { - netstat $NS_NOTRIM -ap --protocol=unix 2>$ERROR_TO_DEVNULL \ + netstat -n $NS_NOTRIM -ap --protocol=unix 2>$ERROR_TO_DEVNULL \ | sed -r -e "\|\s${SOCK_DIR}/minion_event_${MINION_ID_HASH}_pub\.ipc$|"'!d; s|/.*||; s/.*\s//;' \ | uniq } @@ -156,7 +156,7 @@ start() { printf "\nPROCESSES:\n" >&2 ps wwwaxu | grep '[s]alt-minion' >&2 printf "\nSOCKETS:\n" >&2 - netstat $NS_NOTRIM -ap --protocol=unix | grep 'salt.*minion' >&2 + netstat -n $NS_NOTRIM -ap --protocol=unix | grep 'salt.*minion' >&2 printf "\nLOG_FILE:\n" >&2 tail -n 20 "$LOG_FILE" >&2 printf "\nENVIRONMENT:\n" >&2 From 18357ac59df7e772a0e3cb778daeed329610994e Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Fri, 29 Sep 2017 13:58:13 -0600 Subject: [PATCH 442/633] provide a unit test for the sdb utility --- tests/unit/utils/test_sdb.py | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/unit/utils/test_sdb.py diff --git a/tests/unit/utils/test_sdb.py b/tests/unit/utils/test_sdb.py new file mode 100644 index 0000000000..90b3922663 --- /dev/null +++ b/tests/unit/utils/test_sdb.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Vernon Cole ` +''' + +# Import Python Libs +from __future__ import absolute_import +import os + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + NO_MOCK, + NO_MOCK_REASON +) + +# Import Salt Libs +import salt.utils.sdb as sdb +import salt.exceptions + +TEMP_DATABASE_FILE = '/tmp/salt-tests-tmpdir/test_sdb.sqlite' + +SDB_OPTS = { + 'extension_modules': '', + 'test_sdb_data': { + 'driver': 'sqlite3', + 'database': TEMP_DATABASE_FILE, + 'table': 'sdb', + 'create_table': True + } + } + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class SdbTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.modules.sdb + ''' + + @classmethod + def tearDownClass(cls): + try: + os.unlink(TEMP_DATABASE_FILE) + except: + pass + + def setup_loader_modules(self): + return {sdb: {}} + + # test with SQLite database key not presest + + def test_sqlite_get_not_found(self): + what = sdb.sdb_get( + 'sdb://test_sdb_data/thisKeyDoesNotExist', SDB_OPTS) + self.assertEqual(what, None, 'what is "{!r}"'.format(what)) + + # test with SQLite database write and read + + def test_sqlite_get_found(self): + expected = dict(name='testone', + number=46, + ) + sdb.sdb_set('sdb://test_sdb_data/test1', expected, SDB_OPTS) + resp = sdb.sdb_get('sdb://test_sdb_data/test1', SDB_OPTS) + self.assertEqual(resp, expected) From c58c72aff9f3ac61a2e56c226a72556aaf1590d7 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 29 Sep 2017 14:31:46 -0700 Subject: [PATCH 443/633] When using URLs in archive.extracted, on failure the username & password is in the exception. Calling salt.utils.url.redact_http_basic_auth to ensure the credentials are redacted. --- salt/states/archive.py | 14 +++++++++----- salt/states/file.py | 8 +++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/salt/states/archive.py b/salt/states/archive.py index a992adb8b7..9fd14d49d8 100644 --- a/salt/states/archive.py +++ b/salt/states/archive.py @@ -772,7 +772,8 @@ def extracted(name, # Get rid of "file://" from start of source_match source_match = os.path.realpath(os.path.expanduser(urlparsed_source.path)) if not os.path.isfile(source_match): - ret['comment'] = 'Source file \'{0}\' does not exist'.format(source_match) + ret['comment'] = 'Source file \'{0}\' does not exist'.format( + salt.utils.url.redact_http_basic_auth(source_match)) return ret valid_archive_formats = ('tar', 'rar', 'zip') @@ -924,7 +925,7 @@ def extracted(name, if __opts__['test']: ret['result'] = None ret['comment'] = ( - 'Archive {0} would be ached (if necessary) and checked to ' + 'Archive {0} would be cached (if necessary) and checked to ' 'discover if extraction is needed'.format( salt.utils.url.redact_http_basic_auth(source_match) ) @@ -938,7 +939,7 @@ def extracted(name, # file states would be unavailable. ret['comment'] = ( 'Unable to cache {0}, file.cached state not available'.format( - source_match + salt.utils.url.redact_http_basic_auth(source_match) ) ) return ret @@ -950,7 +951,9 @@ def extracted(name, skip_verify=skip_verify, saltenv=__env__) except Exception as exc: - msg = 'Failed to cache {0}: {1}'.format(source_match, exc.__str__()) + msg = 'Failed to cache {0}: {1}'.format( + salt.utils.url.redact_http_basic_auth(source_match), + exc.__str__()) log.exception(msg) ret['comment'] = msg return ret @@ -1181,7 +1184,8 @@ def extracted(name, if not ret['result']: ret['comment'] = \ '{0} does not match the desired source_hash {1}'.format( - source_match, source_sum['hsum'] + salt.utils.url.redact_http_basic_auth(source_match), + source_sum['hsum'] ) return ret diff --git a/salt/states/file.py b/salt/states/file.py index 3a2de6047c..cb02ee625f 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -6573,7 +6573,8 @@ def cached(name, and parsed.scheme in salt.utils.files.REMOTE_PROTOS: ret['comment'] = ( 'Unable to verify upstream hash of source file {0}, please set ' - 'source_hash or set skip_verify to True'.format(name) + 'source_hash or set skip_verify to True'.format( + salt.utils.url.redact_http_basic_auth(name)) ) return ret @@ -6712,13 +6713,14 @@ def cached(name, # for the 2017.7 release cycle. #source_hash=source_sum.get('hsum')) except Exception as exc: - ret['comment'] = exc.__str__() + ret['comment'] = salt.utils.url.redact_http_basic_auth(exc.__str__()) return ret if not local_copy: ret['comment'] = ( 'Failed to cache {0}, check minion log for more ' - 'information'.format(name) + 'information'.format( + salt.utils.url.redact_http_basic_auth(name)) ) return ret From 3c6418b98f3bd1e82ed77e8502ac9b0784446dea Mon Sep 17 00:00:00 2001 From: Alexandru Bleotu Date: Fri, 29 Sep 2017 19:45:56 -0400 Subject: [PATCH 444/633] pylint --- tests/unit/utils/test_vsan.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/unit/utils/test_vsan.py b/tests/unit/utils/test_vsan.py index ab7ccd5a21..814b3a73ec 100644 --- a/tests/unit/utils/test_vsan.py +++ b/tests/unit/utils/test_vsan.py @@ -149,10 +149,6 @@ class GetVsanDiskManagementSystemTestCase(TestCase, LoaderModuleMockMixin): 'sys': MagicMock(), 'ssl': MagicMock()}} - def setUp(self): - self.stub_mock = MagicMock() - self.si_mock = MagicMock(_stub=self.stub_mock) - def setUp(self): self.mock_si = MagicMock() self.mock_ret = MagicMock() @@ -200,7 +196,6 @@ class GetVsanDiskManagementSystemTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(ret, self.mock_ret) - @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') @skipIf(not HAS_PYVSAN, 'The \'vsan\' ext library is missing') @@ -362,7 +357,7 @@ class CreateDiskgroupTestCase(TestCase): 'Fake privilege') def test_initialize_disk_mapping_raise_vim_fault(self): - err = vim.fault.VimFault() + err = vim.fault.VimFault() err.msg = 'vim_fault' self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ MagicMock(side_effect=err) @@ -497,7 +492,7 @@ class AddCapacityToDiskGroupTestCase(TestCase): 'Fake privilege') def test_initialize_disk_mapping_raise_vim_fault(self): - err = vim.fault.VimFault() + err = vim.fault.VimFault() err.msg = 'vim_fault' self.mock_vsan_disk_mgmt_system.InitializeDiskMappings = \ MagicMock(side_effect=err) @@ -620,7 +615,7 @@ class RemoveCapacityFromDiskGroup(TestCase): def test_remove_disk_raise_no_permission(self): err = vim.fault.NoPermission() err.privilegeId = 'Fake privilege' - self.mock_host_vsan_system.RemoveDisk_Task= \ + self.mock_host_vsan_system.RemoveDisk_Task = \ MagicMock(side_effect=err) with self.assertRaises(VMwareApiError) as excinfo: vsan.remove_capacity_from_diskgroup( @@ -631,9 +626,9 @@ class RemoveCapacityFromDiskGroup(TestCase): 'Fake privilege') def test_remove_disk_raise_vim_fault(self): - err = vim.fault.VimFault() + err = vim.fault.VimFault() err.msg = 'vim_fault' - self.mock_host_vsan_system.RemoveDisk_Task= \ + self.mock_host_vsan_system.RemoveDisk_Task = \ MagicMock(side_effect=err) with self.assertRaises(VMwareApiError) as excinfo: vsan.remove_capacity_from_diskgroup( @@ -644,7 +639,7 @@ class RemoveCapacityFromDiskGroup(TestCase): def test_remove_disk_raise_runtime_fault(self): err = vmodl.RuntimeFault() err.msg = 'runtime_fault' - self.mock_host_vsan_system.RemoveDisk_Task= \ + self.mock_host_vsan_system.RemoveDisk_Task = \ MagicMock(side_effect=err) with self.assertRaises(VMwareRuntimeError) as excinfo: vsan.remove_capacity_from_diskgroup( @@ -744,7 +739,7 @@ class RemoveDiskgroup(TestCase): self.mock_si, self.mock_host_ref, self.mock_diskgroup) err = vim.fault.NoPermission() err.privilegeId = 'Fake privilege' - self.mock_host_vsan_system.RemoveDiskMapping_Task= \ + self.mock_host_vsan_system.RemoveDiskMapping_Task = \ MagicMock(side_effect=err) with self.assertRaises(VMwareApiError) as excinfo: vsan.remove_diskgroup( @@ -754,9 +749,9 @@ class RemoveDiskgroup(TestCase): 'Fake privilege') def test_remove_disk_mapping_raise_vim_fault(self): - err = vim.fault.VimFault() + err = vim.fault.VimFault() err.msg = 'vim_fault' - self.mock_host_vsan_system.RemoveDiskMapping_Task= \ + self.mock_host_vsan_system.RemoveDiskMapping_Task = \ MagicMock(side_effect=err) with self.assertRaises(VMwareApiError) as excinfo: vsan.remove_diskgroup( @@ -766,7 +761,7 @@ class RemoveDiskgroup(TestCase): def test_remove_disk_mapping_raise_runtime_fault(self): err = vmodl.RuntimeFault() err.msg = 'runtime_fault' - self.mock_host_vsan_system.RemoveDiskMapping_Task= \ + self.mock_host_vsan_system.RemoveDiskMapping_Task = \ MagicMock(side_effect=err) with self.assertRaises(VMwareRuntimeError) as excinfo: vsan.remove_diskgroup( From 5dba74d2cb6bbd9fe4b0d29f4e92ed26c04c2600 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Sat, 30 Sep 2017 22:41:30 +0200 Subject: [PATCH 445/633] Fix to module.run [WIP] DO NOT MERGE For @terminalmage to review --- salt/states/module.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/salt/states/module.py b/salt/states/module.py index 202999e7d8..3c8ce96b50 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -314,22 +314,31 @@ def _call_function(name, returner=None, **kwargs): :return: ''' argspec = salt.utils.args.get_function_argspec(__salt__[name]) + + # func_kw is initialized to a dictinary of keyword arguments the function to be run accepts func_kw = dict(zip(argspec.args[-len(argspec.defaults or []):], # pylint: disable=incompatible-py3-code argspec.defaults or [])) + + # func_args is initialized to a list of keyword arguments the function to be run accepts + func_args = argspec.args[:len(argspec.args or [] ) - len(argspec.defaults or [])] arg_type, na_type, kw_type = [], {}, False for funcset in reversed(kwargs.get('func_args') or []): if not isinstance(funcset, dict): - kw_type = True - if kw_type: - if isinstance(funcset, dict): - arg_type += funcset.values() - na_type.update(funcset) - else: - arg_type.append(funcset) + # We are just receiving a list of args to the function to be run, so just append + # those to the arg list that we will pass to the func. + arg_type.append(funcset) else: - func_kw.update(funcset) + for kwarg_key in funcset.keys(): + # We are going to pass in a keyword argument. The trick here is to make certain + # that if we find that in the *args* list that we pass it there and not as a kwarg + if kwarg_key in func_args: + arg_type.append(funcset[kwarg_key]) + continue + else: + # Otherwise, we're good and just go ahead and pass the keyword/value pair into + # the kwargs list to be run. + func_kw.update(funcset) arg_type.reverse() - _exp_prm = len(argspec.args or []) - len(argspec.defaults or []) _passed_prm = len(arg_type) missing = [] From 31d17c012475c1975763d48184d98efb86fee5b2 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Sun, 1 Oct 2017 00:39:37 +0200 Subject: [PATCH 446/633] Fix typo found by @s0undt3ch --- salt/states/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/states/module.py b/salt/states/module.py index 3c8ce96b50..de701af51c 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -319,7 +319,7 @@ def _call_function(name, returner=None, **kwargs): func_kw = dict(zip(argspec.args[-len(argspec.defaults or []):], # pylint: disable=incompatible-py3-code argspec.defaults or [])) - # func_args is initialized to a list of keyword arguments the function to be run accepts + # func_args is initialized to a list of positional arguments that the function to be run accepts func_args = argspec.args[:len(argspec.args or [] ) - len(argspec.defaults or [])] arg_type, na_type, kw_type = [], {}, False for funcset in reversed(kwargs.get('func_args') or []): From 49f25b9f192b12b2bbddbd5b6649302cf044a305 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Sun, 1 Oct 2017 10:59:09 +0200 Subject: [PATCH 447/633] Lint --- salt/states/module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/states/module.py b/salt/states/module.py index de701af51c..91e9fb8b57 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -319,8 +319,8 @@ def _call_function(name, returner=None, **kwargs): func_kw = dict(zip(argspec.args[-len(argspec.defaults or []):], # pylint: disable=incompatible-py3-code argspec.defaults or [])) - # func_args is initialized to a list of positional arguments that the function to be run accepts - func_args = argspec.args[:len(argspec.args or [] ) - len(argspec.defaults or [])] + # func_args is initialized to a list of positional arguments that the function to be run accepts + func_args = argspec.args[:len(argspec.args or []) - len(argspec.defaults or [])] arg_type, na_type, kw_type = [], {}, False for funcset in reversed(kwargs.get('func_args') or []): if not isinstance(funcset, dict): From 06eb9c30dd0aff21e05640874ebe168072069283 Mon Sep 17 00:00:00 2001 From: Ken Koch Date: Sun, 1 Oct 2017 07:32:33 -0400 Subject: [PATCH 448/633] Add support for tagging newly created DigitalOcean droplets. --- doc/topics/cloud/digitalocean.rst | 1 + salt/cloud/clouds/digitalocean.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/topics/cloud/digitalocean.rst b/doc/topics/cloud/digitalocean.rst index dd7c76d91f..129e1fcd3a 100644 --- a/doc/topics/cloud/digitalocean.rst +++ b/doc/topics/cloud/digitalocean.rst @@ -54,6 +54,7 @@ Set up an initial profile at ``/etc/salt/cloud.profiles`` or in the ipv6: True create_dns_record: True userdata_file: /etc/salt/cloud.userdata.d/setup + tags:tag1,tag2,tag3 Locations can be obtained using the ``--list-locations`` option for the ``salt-cloud`` command: diff --git a/salt/cloud/clouds/digitalocean.py b/salt/cloud/clouds/digitalocean.py index d5bcb4fb6f..0b9c088465 100644 --- a/salt/cloud/clouds/digitalocean.py +++ b/salt/cloud/clouds/digitalocean.py @@ -298,7 +298,8 @@ def create(vm_): 'size': get_size(vm_), 'image': get_image(vm_), 'region': get_location(vm_), - 'ssh_keys': [] + 'ssh_keys': [], + 'tags': [] } # backwards compat @@ -379,6 +380,13 @@ def create(vm_): raise SaltCloudConfigError("'ipv6' should be a boolean value.") kwargs['ipv6'] = ipv6 + tag_string = config.get_cloud_config_value( + 'tags', vm_, __opts__, search_global=False, default=False + ) + if tag_string: + for tag in tag_string.split(','): + kwargs['tags'].append(tag) + userdata_file = config.get_cloud_config_value( 'userdata_file', vm_, __opts__, search_global=False, default=None ) From 57a624d86bf6e47302adbfe39b50e90b19f2aa8b Mon Sep 17 00:00:00 2001 From: Ken Koch Date: Sun, 1 Oct 2017 07:32:33 -0400 Subject: [PATCH 449/633] Add support for tagging newly created DigitalOcean droplets. Signed-off-by: Ken Koch --- doc/topics/cloud/digitalocean.rst | 1 + salt/cloud/clouds/digitalocean.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/topics/cloud/digitalocean.rst b/doc/topics/cloud/digitalocean.rst index dd7c76d91f..129e1fcd3a 100644 --- a/doc/topics/cloud/digitalocean.rst +++ b/doc/topics/cloud/digitalocean.rst @@ -54,6 +54,7 @@ Set up an initial profile at ``/etc/salt/cloud.profiles`` or in the ipv6: True create_dns_record: True userdata_file: /etc/salt/cloud.userdata.d/setup + tags:tag1,tag2,tag3 Locations can be obtained using the ``--list-locations`` option for the ``salt-cloud`` command: diff --git a/salt/cloud/clouds/digitalocean.py b/salt/cloud/clouds/digitalocean.py index d5bcb4fb6f..0b9c088465 100644 --- a/salt/cloud/clouds/digitalocean.py +++ b/salt/cloud/clouds/digitalocean.py @@ -298,7 +298,8 @@ def create(vm_): 'size': get_size(vm_), 'image': get_image(vm_), 'region': get_location(vm_), - 'ssh_keys': [] + 'ssh_keys': [], + 'tags': [] } # backwards compat @@ -379,6 +380,13 @@ def create(vm_): raise SaltCloudConfigError("'ipv6' should be a boolean value.") kwargs['ipv6'] = ipv6 + tag_string = config.get_cloud_config_value( + 'tags', vm_, __opts__, search_global=False, default=False + ) + if tag_string: + for tag in tag_string.split(','): + kwargs['tags'].append(tag) + userdata_file = config.get_cloud_config_value( 'userdata_file', vm_, __opts__, search_global=False, default=None ) From d4551ab63fffcbd1c6fef017a46b4ebce337c528 Mon Sep 17 00:00:00 2001 From: amalleo25 Date: Sun, 1 Oct 2017 21:06:11 -0400 Subject: [PATCH 450/633] Update joyent.py Location parameter defined in the cloud provider config was ignored in the list_nodes function. This caused an issue when querying a node after creation. --- salt/cloud/clouds/joyent.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/cloud/clouds/joyent.py b/salt/cloud/clouds/joyent.py index 8f40c28657..f998c0f864 100644 --- a/salt/cloud/clouds/joyent.py +++ b/salt/cloud/clouds/joyent.py @@ -749,12 +749,13 @@ def list_nodes(full=False, call=None): log.error('Invalid response when listing Joyent nodes: {0}'.format(result[1])) else: - result = query(command='my/machines', location=DEFAULT_LOCATION, + location = get_location() + result = query(command='my/machines', location=location, method='GET') nodes = result[1] for node in nodes: if 'name' in node: - node['location'] = DEFAULT_LOCATION + node['location'] = location ret[node['name']] = reformat_node(item=node, full=full) return ret From fba9c9a935a6e81784e0b52a0caab65df4526b1a Mon Sep 17 00:00:00 2001 From: Kees Bos Date: Mon, 2 Oct 2017 09:14:49 +0200 Subject: [PATCH 451/633] Map __env__ in git_pillar before sanity checks Backport of commit 1de6791069552f80812dc4cab4c0ded0762030d3 --- salt/pillar/git_pillar.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/salt/pillar/git_pillar.py b/salt/pillar/git_pillar.py index 7cdba5b5a4..d183f63ac3 100644 --- a/salt/pillar/git_pillar.py +++ b/salt/pillar/git_pillar.py @@ -566,8 +566,15 @@ def ext_pillar(minion_id, repo, pillar_dirs): False ) for pillar_dir, env in six.iteritems(pillar.pillar_dirs): + # Map env if env == '__env__' before checking the env value + if env == '__env__': + env = opts.get('pillarenv') \ + or opts.get('environment') \ + or opts.get('git_pillar_base') + log.debug('__env__ maps to %s', env) + # If pillarenv is set, only grab pillars with that match pillarenv - if opts['pillarenv'] and env != opts['pillarenv'] and env != '__env__': + if opts['pillarenv'] and env != opts['pillarenv']: log.debug( 'env \'%s\' for pillar dir \'%s\' does not match ' 'pillarenv \'%s\', skipping', @@ -586,12 +593,6 @@ def ext_pillar(minion_id, repo, pillar_dirs): 'env \'%s\'', pillar_dir, env ) - if env == '__env__': - env = opts.get('pillarenv') \ - or opts.get('environment') \ - or opts.get('git_pillar_base') - log.debug('__env__ maps to %s', env) - pillar_roots = [pillar_dir] if __opts__['git_pillar_includes']: From 325a2f301e4630b6568b88c545373c156e5befe0 Mon Sep 17 00:00:00 2001 From: Nasenbaer Date: Mon, 17 Oct 2016 14:40:47 +0200 Subject: [PATCH 452/633] Activate jid_queue also for SingleMinions to workaround 0mq reconnection issues --- salt/minion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/minion.py b/salt/minion.py index 25e7fe28d2..1760fb4c8e 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -970,7 +970,7 @@ class Minion(MinionBase): # Flag meaning minion has finished initialization including first connect to the master. # True means the Minion is fully functional and ready to handle events. self.ready = False - self.jid_queue = jid_queue + self.jid_queue = jid_queue or [] if io_loop is None: if HAS_ZMQ: From 76cd070e2d6b231dad876dca7a9df8785efc757e Mon Sep 17 00:00:00 2001 From: Damian Wiest Date: Mon, 2 Oct 2017 10:15:02 -0500 Subject: [PATCH 453/633] Fixed Jinja typo in faq.rst A closing curly brace was missing in the first block of Jinja code under the "Restart using states" section. --- doc/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/faq.rst b/doc/faq.rst index 18674b370b..9b75fd307d 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -325,7 +325,7 @@ The following example works on UNIX-like operating systems: .. code-block:: jinja - {%- if grains['os'] != 'Windows' % + {%- if grains['os'] != 'Windows' %} Restart Salt Minion: cmd.run: - name: 'salt-call --local service.restart salt-minion' From 22792e56cc4f06e92b7973c4819ac4d2534153a2 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Mon, 2 Oct 2017 09:48:27 -0600 Subject: [PATCH 454/633] lint, style, and Windows fixes --- tests/unit/utils/test_sdb.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/unit/utils/test_sdb.py b/tests/unit/utils/test_sdb.py index 90b3922663..0bf4a1a915 100644 --- a/tests/unit/utils/test_sdb.py +++ b/tests/unit/utils/test_sdb.py @@ -8,6 +8,7 @@ from __future__ import absolute_import import os # Import Salt Testing Libs +from tests.support.paths import TMP from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import TestCase, skipIf from tests.support.mock import ( @@ -17,9 +18,8 @@ from tests.support.mock import ( # Import Salt Libs import salt.utils.sdb as sdb -import salt.exceptions -TEMP_DATABASE_FILE = '/tmp/salt-tests-tmpdir/test_sdb.sqlite' +TEMP_DATABASE_FILE = os.path.join(TMP, 'test_sdb.sqlite') SDB_OPTS = { 'extension_modules': '', @@ -57,9 +57,7 @@ class SdbTestCase(TestCase, LoaderModuleMockMixin): # test with SQLite database write and read def test_sqlite_get_found(self): - expected = dict(name='testone', - number=46, - ) + expected = {b'name': b'testone', b'number': 46} sdb.sdb_set('sdb://test_sdb_data/test1', expected, SDB_OPTS) resp = sdb.sdb_get('sdb://test_sdb_data/test1', SDB_OPTS) self.assertEqual(resp, expected) From 5d56a03a6723c046cd6d3f004e4b59bf396845ac Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 29 Sep 2017 17:15:18 -0500 Subject: [PATCH 455/633] Improve failures for module.run states Bare asserts are zero help in troubleshooting. This commit changes the tests that uses bare asserts such that they fail with a useful error mesage as well as the return data from the module.run call. --- tests/unit/states/test_module.py | 79 ++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/tests/unit/states/test_module.py b/tests/unit/states/test_module.py index 4588e20ca8..2d082fed2a 100644 --- a/tests/unit/states/test_module.py +++ b/tests/unit/states/test_module.py @@ -6,6 +6,7 @@ # Import Python Libs from __future__ import absolute_import from inspect import ArgSpec +import logging # Import Salt Libs import salt.states.module as module @@ -20,6 +21,8 @@ from tests.support.mock import ( patch ) +log = logging.getLogger(__name__) + CMD = 'foo.bar' @@ -91,8 +94,9 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): with patch.dict(module.__salt__, {}, clear=True): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): ret = module.run(**{CMD: None}) - assert ret['comment'] == "Unavailable function: {0}.".format(CMD) - assert not ret['result'] + if ret['comment'] != "Unavailable function: {0}.".format(CMD) \ + or ret['result']: + self.fail('module.run did not fail as expected: {0}'.format(ret)) def test_module_run_hidden_varargs(self): ''' @@ -111,8 +115,9 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): ''' with patch.dict(module.__opts__, {'test': True, 'use_superseded': ['module.run']}): ret = module.run(**{CMD: None}) - assert ret['comment'] == "Function {0} to be executed.".format(CMD) - assert ret['result'] + if ret['comment'] != "Function {0} to be executed.".format(CMD) \ + or not ret['result']: + self.fail('module.run failed: {0}'.format(ret)) def test_run_missing_arg(self): ''' @@ -122,7 +127,10 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): with patch.dict(module.__salt__, {CMD: _mocked_func_named}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): ret = module.run(**{CMD: None}) - assert ret['comment'] == "'{0}' failed: Function expects 1 parameters, got only 0".format(CMD) + expected_comment = \ + "'{0}' failed: Function expects 1 parameters, got only 0".format(CMD) + if ret['comment'] != expected_comment: + self.fail('module.run did not fail as expected: {0}'.format(ret)) def test_run_correct_arg(self): ''' @@ -132,16 +140,17 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): with patch.dict(module.__salt__, {CMD: _mocked_func_named}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): ret = module.run(**{CMD: ['Fred']}) - assert ret['comment'] == '{0}: Success'.format(CMD) - assert ret['result'] + if ret['comment'] != '{0}: Success'.format(CMD) or not ret['result']: + self.fail('module.run failed: {0}'.format(ret)) def test_run_unexpected_keywords(self): with patch.dict(module.__salt__, {CMD: _mocked_func_args}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): ret = module.run(**{CMD: [{'foo': 'bar'}]}) - assert ret['comment'] == "'{0}' failed: {1}() got an unexpected keyword argument " \ - "'foo'".format(CMD, module.__salt__[CMD].__name__) - assert not ret['result'] + expected_comment = "'{0}' failed: {1}() got an unexpected keyword argument " \ + "'foo'".format(CMD, module.__salt__[CMD].__name__) + if ret['comment'] != expected_comment or ret['result']: + self.fail('module.run did not fail as expected: {0}'.format(ret)) def test_run_args(self): ''' @@ -150,7 +159,17 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): ''' with patch.dict(module.__salt__, {CMD: _mocked_func_args}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): - assert module.run(**{CMD: ['foo', 'bar']})['result'] + try: + ret = module.run(**{CMD: ['foo', 'bar']}) + except Exception as exc: + log.exception('test_run_none_return: raised exception') + self.fail('module.run raised exception: {0}'.format(exc)) + if not ret['result']: + log.exception( + 'test_run_none_return: test failed, result: %s', + ret + ) + self.fail('module.run failed: {0}'.format(ret)) def test_run_none_return(self): ''' @@ -159,7 +178,17 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): ''' with patch.dict(module.__salt__, {CMD: _mocked_none_return}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): - assert module.run(**{CMD: None})['result'] + try: + ret = module.run(**{CMD: None}) + except Exception as exc: + log.exception('test_run_none_return: raised exception') + self.fail('module.run raised exception: {0}'.format(exc)) + if not ret['result']: + log.exception( + 'test_run_none_return: test failed, result: %s', + ret + ) + self.fail('module.run failed: {0}'.format(ret)) def test_run_typed_return(self): ''' @@ -169,7 +198,18 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): for val in [1, 0, 'a', '', (1, 2,), (), [1, 2], [], {'a': 'b'}, {}, True, False]: with patch.dict(module.__salt__, {CMD: _mocked_none_return}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): - assert module.run(**{CMD: [{'ret': val}]})['result'] + log.debug('test_run_typed_return: trying %s', val) + try: + ret = module.run(**{CMD: [{'ret': val}]}) + except Exception as exc: + log.exception('test_run_typed_return: raised exception') + self.fail('module.run raised exception: {0}'.format(exc)) + if not ret['result']: + log.exception( + 'test_run_typed_return: test failed, result: %s', + ret + ) + self.fail('module.run failed: {0}'.format(ret)) def test_run_batch_call(self): ''' @@ -182,7 +222,18 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): 'second': _mocked_none_return, 'third': _mocked_none_return}, clear=True): for f_name in module.__salt__: - assert module.run(**{f_name: None})['result'] + log.debug('test_run_batch_call: trying %s', f_name) + try: + ret = module.run(**{f_name: None}) + except Exception as exc: + log.exception('test_run_batch_call: raised exception') + self.fail('module.run raised exception: {0}'.format(exc)) + if not ret['result']: + log.exception( + 'test_run_batch_call: test failed, result: %s', + ret + ) + self.fail('module.run failed: {0}'.format(ret)) def test_module_run_module_not_available(self): ''' From 93eaba7c54d280a8598d083f8d6b5a1fea8dc95e Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 2 Oct 2017 11:40:00 -0500 Subject: [PATCH 456/633] Use six.iterkeys() instead of dict.keys() This avoids generating a new list on PY2. --- salt/states/module.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/states/module.py b/salt/states/module.py index 91e9fb8b57..2e907959f7 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -178,6 +178,7 @@ from __future__ import absolute_import import salt.loader import salt.utils import salt.utils.jid +from salt.ext import six from salt.ext.six.moves import range from salt.ext.six.moves import zip from salt.exceptions import SaltInvocationError @@ -315,7 +316,7 @@ def _call_function(name, returner=None, **kwargs): ''' argspec = salt.utils.args.get_function_argspec(__salt__[name]) - # func_kw is initialized to a dictinary of keyword arguments the function to be run accepts + # func_kw is initialized to a dictionary of keyword arguments the function to be run accepts func_kw = dict(zip(argspec.args[-len(argspec.defaults or []):], # pylint: disable=incompatible-py3-code argspec.defaults or [])) @@ -328,7 +329,7 @@ def _call_function(name, returner=None, **kwargs): # those to the arg list that we will pass to the func. arg_type.append(funcset) else: - for kwarg_key in funcset.keys(): + for kwarg_key in six.iterkeys(funcset): # We are going to pass in a keyword argument. The trick here is to make certain # that if we find that in the *args* list that we pass it there and not as a kwarg if kwarg_key in func_args: From 6c20e146c3a310699fa76933310a3e06ebbaa6ed Mon Sep 17 00:00:00 2001 From: garethgreenaway Date: Mon, 2 Oct 2017 10:10:53 -0700 Subject: [PATCH 457/633] Lint Fixes Lint Fixes --- tests/unit/utils/test_sdb.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/utils/test_sdb.py b/tests/unit/utils/test_sdb.py index 0bf4a1a915..c9724caca0 100644 --- a/tests/unit/utils/test_sdb.py +++ b/tests/unit/utils/test_sdb.py @@ -31,6 +31,7 @@ SDB_OPTS = { } } + @skipIf(NO_MOCK, NO_MOCK_REASON) class SdbTestCase(TestCase, LoaderModuleMockMixin): ''' @@ -41,7 +42,7 @@ class SdbTestCase(TestCase, LoaderModuleMockMixin): def tearDownClass(cls): try: os.unlink(TEMP_DATABASE_FILE) - except: + except OSError: pass def setup_loader_modules(self): @@ -52,7 +53,7 @@ class SdbTestCase(TestCase, LoaderModuleMockMixin): def test_sqlite_get_not_found(self): what = sdb.sdb_get( 'sdb://test_sdb_data/thisKeyDoesNotExist', SDB_OPTS) - self.assertEqual(what, None, 'what is "{!r}"'.format(what)) + self.assertEqual(what, None, 'what is "{0!r}"'.format(what)) # test with SQLite database write and read From dee0d318c1ffa3456ab951fed82f9be702283e8e Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Mon, 2 Oct 2017 11:14:09 -0600 Subject: [PATCH 458/633] use .get incase attrs are disabled on the filesystem --- salt/modules/file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index 45e73b8740..20edba8460 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -4358,7 +4358,7 @@ def check_perms(name, ret, user, group, mode, attrs=None, follow_symlinks=False) is_dir = os.path.isdir(name) if not salt.utils.platform.is_windows() and not is_dir and lsattr_cmd: # List attributes on file - perms['lattrs'] = ''.join(lsattr(name)[name]) + perms['lattrs'] = ''.join(lsattr(name).get('name', '')) # Remove attributes on file so changes can be enforced. if perms['lattrs']: chattr(name, operator='remove', attributes=perms['lattrs']) From bd3d457903e9b79fca10ce17252ea411c75fca1d Mon Sep 17 00:00:00 2001 From: Mahesh Balumuri Date: Mon, 2 Oct 2017 14:13:40 -0400 Subject: [PATCH 459/633] Fix TypeError: 'NoneType' object is not iterable during list_networks function. --- salt/cloud/clouds/azurearm.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/salt/cloud/clouds/azurearm.py b/salt/cloud/clouds/azurearm.py index 0397aaed44..51036c3eea 100644 --- a/salt/cloud/clouds/azurearm.py +++ b/salt/cloud/clouds/azurearm.py @@ -659,11 +659,12 @@ def list_subnets(call=None, kwargs=None): # pylint: disable=unused-argument for subnet in subnets: ret[subnet.name] = make_safe(subnet) ret[subnet.name]['ip_configurations'] = {} - for ip_ in subnet.ip_configurations: - comps = ip_.id.split('/') - name = comps[-1] - ret[subnet.name]['ip_configurations'][name] = make_safe(ip_) - ret[subnet.name]['ip_configurations'][name]['subnet'] = subnet.name + if subnet.ip_configurations: + for ip_ in subnet.ip_configurations: + comps = ip_.id.split('/') + name = comps[-1] + ret[subnet.name]['ip_configurations'][name] = make_safe(ip_) + ret[subnet.name]['ip_configurations'][name]['subnet'] = subnet.name ret[subnet.name]['resource_group'] = kwargs['resource_group'] return ret From 2bccf228baba98ef3a29b132ba94ac3c4cb9900d Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Mon, 2 Oct 2017 12:18:11 -0600 Subject: [PATCH 460/633] fix code-lint complaint --- tests/unit/utils/test_sdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_sdb.py b/tests/unit/utils/test_sdb.py index c9724caca0..91216d4c44 100644 --- a/tests/unit/utils/test_sdb.py +++ b/tests/unit/utils/test_sdb.py @@ -53,7 +53,7 @@ class SdbTestCase(TestCase, LoaderModuleMockMixin): def test_sqlite_get_not_found(self): what = sdb.sdb_get( 'sdb://test_sdb_data/thisKeyDoesNotExist', SDB_OPTS) - self.assertEqual(what, None, 'what is "{0!r}"'.format(what)) + self.assertEqual(what, None) # test with SQLite database write and read From 0cac15e502d1565c718ebb289fb95c6e67dcdf10 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Sat, 30 Sep 2017 22:41:30 +0200 Subject: [PATCH 461/633] Fix to module.run [WIP] DO NOT MERGE For @terminalmage to review --- salt/states/module.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/salt/states/module.py b/salt/states/module.py index 202999e7d8..3c8ce96b50 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -314,22 +314,31 @@ def _call_function(name, returner=None, **kwargs): :return: ''' argspec = salt.utils.args.get_function_argspec(__salt__[name]) + + # func_kw is initialized to a dictinary of keyword arguments the function to be run accepts func_kw = dict(zip(argspec.args[-len(argspec.defaults or []):], # pylint: disable=incompatible-py3-code argspec.defaults or [])) + + # func_args is initialized to a list of keyword arguments the function to be run accepts + func_args = argspec.args[:len(argspec.args or [] ) - len(argspec.defaults or [])] arg_type, na_type, kw_type = [], {}, False for funcset in reversed(kwargs.get('func_args') or []): if not isinstance(funcset, dict): - kw_type = True - if kw_type: - if isinstance(funcset, dict): - arg_type += funcset.values() - na_type.update(funcset) - else: - arg_type.append(funcset) + # We are just receiving a list of args to the function to be run, so just append + # those to the arg list that we will pass to the func. + arg_type.append(funcset) else: - func_kw.update(funcset) + for kwarg_key in funcset.keys(): + # We are going to pass in a keyword argument. The trick here is to make certain + # that if we find that in the *args* list that we pass it there and not as a kwarg + if kwarg_key in func_args: + arg_type.append(funcset[kwarg_key]) + continue + else: + # Otherwise, we're good and just go ahead and pass the keyword/value pair into + # the kwargs list to be run. + func_kw.update(funcset) arg_type.reverse() - _exp_prm = len(argspec.args or []) - len(argspec.defaults or []) _passed_prm = len(arg_type) missing = [] From a6c2d78518bf9e6b9d26cdbd4e245e762ab65def Mon Sep 17 00:00:00 2001 From: Mike Place Date: Sun, 1 Oct 2017 00:39:37 +0200 Subject: [PATCH 462/633] Fix typo found by @s0undt3ch --- salt/states/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/states/module.py b/salt/states/module.py index 3c8ce96b50..de701af51c 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -319,7 +319,7 @@ def _call_function(name, returner=None, **kwargs): func_kw = dict(zip(argspec.args[-len(argspec.defaults or []):], # pylint: disable=incompatible-py3-code argspec.defaults or [])) - # func_args is initialized to a list of keyword arguments the function to be run accepts + # func_args is initialized to a list of positional arguments that the function to be run accepts func_args = argspec.args[:len(argspec.args or [] ) - len(argspec.defaults or [])] arg_type, na_type, kw_type = [], {}, False for funcset in reversed(kwargs.get('func_args') or []): From 782e67c199afb51bec10e93fcd65f9af59972247 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Sun, 1 Oct 2017 10:59:09 +0200 Subject: [PATCH 463/633] Lint --- salt/states/module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/states/module.py b/salt/states/module.py index de701af51c..91e9fb8b57 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -319,8 +319,8 @@ def _call_function(name, returner=None, **kwargs): func_kw = dict(zip(argspec.args[-len(argspec.defaults or []):], # pylint: disable=incompatible-py3-code argspec.defaults or [])) - # func_args is initialized to a list of positional arguments that the function to be run accepts - func_args = argspec.args[:len(argspec.args or [] ) - len(argspec.defaults or [])] + # func_args is initialized to a list of positional arguments that the function to be run accepts + func_args = argspec.args[:len(argspec.args or []) - len(argspec.defaults or [])] arg_type, na_type, kw_type = [], {}, False for funcset in reversed(kwargs.get('func_args') or []): if not isinstance(funcset, dict): From c297ae55575a790637f1dd5d780ce324a027744d Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 29 Sep 2017 17:15:18 -0500 Subject: [PATCH 464/633] Improve failures for module.run states Bare asserts are zero help in troubleshooting. This commit changes the tests that uses bare asserts such that they fail with a useful error mesage as well as the return data from the module.run call. --- tests/unit/states/test_module.py | 79 ++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/tests/unit/states/test_module.py b/tests/unit/states/test_module.py index 4588e20ca8..2d082fed2a 100644 --- a/tests/unit/states/test_module.py +++ b/tests/unit/states/test_module.py @@ -6,6 +6,7 @@ # Import Python Libs from __future__ import absolute_import from inspect import ArgSpec +import logging # Import Salt Libs import salt.states.module as module @@ -20,6 +21,8 @@ from tests.support.mock import ( patch ) +log = logging.getLogger(__name__) + CMD = 'foo.bar' @@ -91,8 +94,9 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): with patch.dict(module.__salt__, {}, clear=True): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): ret = module.run(**{CMD: None}) - assert ret['comment'] == "Unavailable function: {0}.".format(CMD) - assert not ret['result'] + if ret['comment'] != "Unavailable function: {0}.".format(CMD) \ + or ret['result']: + self.fail('module.run did not fail as expected: {0}'.format(ret)) def test_module_run_hidden_varargs(self): ''' @@ -111,8 +115,9 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): ''' with patch.dict(module.__opts__, {'test': True, 'use_superseded': ['module.run']}): ret = module.run(**{CMD: None}) - assert ret['comment'] == "Function {0} to be executed.".format(CMD) - assert ret['result'] + if ret['comment'] != "Function {0} to be executed.".format(CMD) \ + or not ret['result']: + self.fail('module.run failed: {0}'.format(ret)) def test_run_missing_arg(self): ''' @@ -122,7 +127,10 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): with patch.dict(module.__salt__, {CMD: _mocked_func_named}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): ret = module.run(**{CMD: None}) - assert ret['comment'] == "'{0}' failed: Function expects 1 parameters, got only 0".format(CMD) + expected_comment = \ + "'{0}' failed: Function expects 1 parameters, got only 0".format(CMD) + if ret['comment'] != expected_comment: + self.fail('module.run did not fail as expected: {0}'.format(ret)) def test_run_correct_arg(self): ''' @@ -132,16 +140,17 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): with patch.dict(module.__salt__, {CMD: _mocked_func_named}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): ret = module.run(**{CMD: ['Fred']}) - assert ret['comment'] == '{0}: Success'.format(CMD) - assert ret['result'] + if ret['comment'] != '{0}: Success'.format(CMD) or not ret['result']: + self.fail('module.run failed: {0}'.format(ret)) def test_run_unexpected_keywords(self): with patch.dict(module.__salt__, {CMD: _mocked_func_args}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): ret = module.run(**{CMD: [{'foo': 'bar'}]}) - assert ret['comment'] == "'{0}' failed: {1}() got an unexpected keyword argument " \ - "'foo'".format(CMD, module.__salt__[CMD].__name__) - assert not ret['result'] + expected_comment = "'{0}' failed: {1}() got an unexpected keyword argument " \ + "'foo'".format(CMD, module.__salt__[CMD].__name__) + if ret['comment'] != expected_comment or ret['result']: + self.fail('module.run did not fail as expected: {0}'.format(ret)) def test_run_args(self): ''' @@ -150,7 +159,17 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): ''' with patch.dict(module.__salt__, {CMD: _mocked_func_args}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): - assert module.run(**{CMD: ['foo', 'bar']})['result'] + try: + ret = module.run(**{CMD: ['foo', 'bar']}) + except Exception as exc: + log.exception('test_run_none_return: raised exception') + self.fail('module.run raised exception: {0}'.format(exc)) + if not ret['result']: + log.exception( + 'test_run_none_return: test failed, result: %s', + ret + ) + self.fail('module.run failed: {0}'.format(ret)) def test_run_none_return(self): ''' @@ -159,7 +178,17 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): ''' with patch.dict(module.__salt__, {CMD: _mocked_none_return}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): - assert module.run(**{CMD: None})['result'] + try: + ret = module.run(**{CMD: None}) + except Exception as exc: + log.exception('test_run_none_return: raised exception') + self.fail('module.run raised exception: {0}'.format(exc)) + if not ret['result']: + log.exception( + 'test_run_none_return: test failed, result: %s', + ret + ) + self.fail('module.run failed: {0}'.format(ret)) def test_run_typed_return(self): ''' @@ -169,7 +198,18 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): for val in [1, 0, 'a', '', (1, 2,), (), [1, 2], [], {'a': 'b'}, {}, True, False]: with patch.dict(module.__salt__, {CMD: _mocked_none_return}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): - assert module.run(**{CMD: [{'ret': val}]})['result'] + log.debug('test_run_typed_return: trying %s', val) + try: + ret = module.run(**{CMD: [{'ret': val}]}) + except Exception as exc: + log.exception('test_run_typed_return: raised exception') + self.fail('module.run raised exception: {0}'.format(exc)) + if not ret['result']: + log.exception( + 'test_run_typed_return: test failed, result: %s', + ret + ) + self.fail('module.run failed: {0}'.format(ret)) def test_run_batch_call(self): ''' @@ -182,7 +222,18 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): 'second': _mocked_none_return, 'third': _mocked_none_return}, clear=True): for f_name in module.__salt__: - assert module.run(**{f_name: None})['result'] + log.debug('test_run_batch_call: trying %s', f_name) + try: + ret = module.run(**{f_name: None}) + except Exception as exc: + log.exception('test_run_batch_call: raised exception') + self.fail('module.run raised exception: {0}'.format(exc)) + if not ret['result']: + log.exception( + 'test_run_batch_call: test failed, result: %s', + ret + ) + self.fail('module.run failed: {0}'.format(ret)) def test_module_run_module_not_available(self): ''' From e21d8e9583bba816b6ebc8c9bed379fb4790dfa4 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 2 Oct 2017 11:40:00 -0500 Subject: [PATCH 465/633] Use six.iterkeys() instead of dict.keys() This avoids generating a new list on PY2. --- salt/states/module.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/states/module.py b/salt/states/module.py index 91e9fb8b57..2e907959f7 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -178,6 +178,7 @@ from __future__ import absolute_import import salt.loader import salt.utils import salt.utils.jid +from salt.ext import six from salt.ext.six.moves import range from salt.ext.six.moves import zip from salt.exceptions import SaltInvocationError @@ -315,7 +316,7 @@ def _call_function(name, returner=None, **kwargs): ''' argspec = salt.utils.args.get_function_argspec(__salt__[name]) - # func_kw is initialized to a dictinary of keyword arguments the function to be run accepts + # func_kw is initialized to a dictionary of keyword arguments the function to be run accepts func_kw = dict(zip(argspec.args[-len(argspec.defaults or []):], # pylint: disable=incompatible-py3-code argspec.defaults or [])) @@ -328,7 +329,7 @@ def _call_function(name, returner=None, **kwargs): # those to the arg list that we will pass to the func. arg_type.append(funcset) else: - for kwarg_key in funcset.keys(): + for kwarg_key in six.iterkeys(funcset): # We are going to pass in a keyword argument. The trick here is to make certain # that if we find that in the *args* list that we pass it there and not as a kwarg if kwarg_key in func_args: From fe28b0d4fb7c63bde2d5ec928cd85c384e9b11fe Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 2 Oct 2017 14:42:40 -0500 Subject: [PATCH 466/633] Only join cmd if it's not a string See https://github.com/saltstack/salt/pull/43807#discussion_r142009644 --- salt/modules/cmdmod.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index e5d058505f..8dd0ca6e77 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -378,7 +378,8 @@ def _run(cmd, msg = 'missing salt/utils/win_runas.py' raise CommandExecutionError(msg) - cmd = ' '.join(cmd) + if isinstance(cmd, (list, tuple)): + cmd = ' '.join(cmd) return win_runas(cmd, runas, password, cwd) From 2fa7fe5ee552bea19f2df3a28566c67105589645 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Mon, 2 Oct 2017 13:46:36 -0600 Subject: [PATCH 467/633] Add hash cache invalidation to azurefs --- salt/fileserver/azurefs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/salt/fileserver/azurefs.py b/salt/fileserver/azurefs.py index d1cdf1c33b..7eb919fa7d 100644 --- a/salt/fileserver/azurefs.py +++ b/salt/fileserver/azurefs.py @@ -265,6 +265,11 @@ def update(): os.unlink(lk_fn) except Exception: pass + try: + hash_cachedir = os.path.join(__opts__['cachedir'], 'azurefs', 'hashes') + shutil.rmtree(hash_cachedir) + except Exception: + log.exception('Problem occurred trying to invalidate hash cach for azurefs') def file_hash(load, fnd): From 2337904656e5b935ff330bf1cee5a54299439960 Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 2 Oct 2017 17:18:30 -0400 Subject: [PATCH 468/633] Add updated release notes to 2017.7.2 branch --- doc/topics/releases/2017.7.2.rst | 3162 ++++++++++++++++++++++++++++++ 1 file changed, 3162 insertions(+) create mode 100644 doc/topics/releases/2017.7.2.rst diff --git a/doc/topics/releases/2017.7.2.rst b/doc/topics/releases/2017.7.2.rst new file mode 100644 index 0000000000..ac8ef25146 --- /dev/null +++ b/doc/topics/releases/2017.7.2.rst @@ -0,0 +1,3162 @@ +=========================== +Salt 2017.7.2 Release Notes +=========================== + + +Changes for v2017.7.1..v2017.7.2 +-------------------------------- + +Extended changelog courtesy of Todd Stansell (https://github.com/tjstansell/salt-changelogs): + +*Generated at: 2017-10-02T21:10:14Z* + +Statistics +========== + +- Total Merges: **328** +- Total Issue references: **134** +- Total PR references: **391** + +Changes +======= + +- **PR** `#43868`_: (*rallytime*) Back-port `#43847`_ to 2017.7.2 + * Fix to module.run + +- **PR** `#43756`_: (*gtmanfred*) split build and install for pkg osx + @ *2017-09-26T20:51:28Z* + + * 88414d5 Merge pull request `#43756`_ from gtmanfred/2017.7.2 + * f7df41f split build and install for pkg osx + +- **PR** `#43585`_: (*rallytime*) Back-port `#43330`_ to 2017.7.2 + @ *2017-09-19T17:33:34Z* + + - **ISSUE** `#43077`_: (*Manoj2087*) Issue with deleting key via wheel + | refs: `#43330`_ + - **PR** `#43330`_: (*terminalmage*) Fix reactor regression + unify reactor config schema + | refs: `#43585`_ + * 89f6292 Merge pull request `#43585`_ from rallytime/`bp-43330`_ + * c4f693b Merge branch '2017.7.2' into `bp-43330`_ + +- **PR** `#43586`_: (*rallytime*) Back-port `#43526`_ to 2017.7.2 + @ *2017-09-19T15:36:27Z* + + - **ISSUE** `#43447`_: (*UtahDave*) When using Syndic with Multi Master the top level master doesn't reliably get returns from lower minion. + | refs: `#43526`_ + - **PR** `#43526`_: (*DmitryKuzmenko*) Forward events to all masters syndic connected to + | refs: `#43586`_ + * abb7fe4 Merge pull request `#43586`_ from rallytime/`bp-43526`_ + * e076e9b Forward events to all masters syndic connected to. + + * 7abd07f Simplify client logic + + * b5f1069 Improve the reactor documentation + + * 7a2f12b Include a better example for reactor in master conf file + + * 531cac6 Rewrite the reactor unit tests + + * 2a35ab7 Unify reactor configuration, fix caller reactors + + * 4afb179 Un-deprecate passing kwargs outside of 'kwarg' param + +- **PR** `#43551`_: (*twangboy*) Fix preinstall script on OSX for 2017.7.2 + @ *2017-09-18T18:35:35Z* + + * 3d3b093 Merge pull request `#43551`_ from twangboy/osx_fix_preinstall_2017.7.2 + * c3d9fb6 Merge branch '2017.7.2' into osx_fix_preinstall_2017.7.2 + +- **PR** `#43509`_: (*rallytime*) Back-port `#43333`_ to 2017.7.2 + @ *2017-09-15T21:21:40Z* + + - **ISSUE** `#2`_: (*thatch45*) salt job queries + - **PR** `#43333`_: (*damon-atkins*) Docs are wrong cache_dir (bool) and cache_file (str) cannot be passed as params + 1 bug + | refs: `#43509`_ + * 24691da Merge pull request `#43509`_ from rallytime/`bp-43333`_-2017.7.2 + * b3dbafb Update doco + + * 5cdcdbf Update win_pkg.py + + * c3e1666 Docs are wrong cache_dir (bool) and cache_file (str) cannot be passed on the cli (`#2`_) + + * f33395f Fix logic in `/etc/paths.d/salt` detection + +- **PR** `#43440`_: (*rallytime*) Back-port `#43421`_ to 2017.7.2 + @ *2017-09-11T20:59:53Z* + + - **PR** `#43421`_: (*gtmanfred*) Revert "Reduce fileclient.get_file latency by merging _file_find and … + | refs: `#43440`_ + * 8964cac Merge pull request `#43440`_ from rallytime/`bp-43421`_ + * ea6e661 Revert "Reduce fileclient.get_file latency by merging _file_find and _file_hash" + +- **PR** `#43377`_: (*rallytime*) Back-port `#43193`_ to 2017.7.2 + @ *2017-09-11T15:32:23Z* + + - **PR** `#43193`_: (*jettero*) Prevent spurious "Template does not exist" error + | refs: `#43377`_ + - **PR** `#39516`_: (*jettero*) Prevent spurious "Template does not exist" error + | refs: `#43193`_ + * 7fda186 Merge pull request `#43377`_ from rallytime/`bp-43193`_ + * 842b07f Prevent spurious "Template does not exist" error + +- **PR** `#43315`_: (*rallytime*) Back-port `#43283`_ to 2017.7.2 + @ *2017-09-05T20:04:25Z* + + - **ISSUE** `#42459`_: (*iavael*) Broken ldap groups retrieval in salt.auth.ldap after upgrade to 2017.7 + | refs: `#43283`_ + - **PR** `#43283`_: (*DmitryKuzmenko*) Fix ldap token groups auth. + | refs: `#43315`_ + * 85dba1e Merge pull request `#43315`_ from rallytime/`bp-43283`_ + * f29f5b0 Fix for tests: don't require 'groups' in the eauth token. + + * 56938d5 Fix ldap token groups auth. + +- **PR** `#43266`_: (*gtmanfred*) switch virtualbox cloud driver to use __utils__ + @ *2017-08-30T18:36:20Z* + + - **ISSUE** `#43259`_: (*mahesh21*) NameError: global name '__opts__' is not defined + | refs: `#43266`_ + * 26ff808 Merge pull request `#43266`_ from gtmanfred/virtualbox + * 382bf92 switch virtualbox cloud driver to use __utils__ + +- **PR** `#43073`_: (*Mapel88*) Fix bug `#42936`_ - win_iis module container settings + @ *2017-08-30T18:34:37Z* + + - **ISSUE** `#43110`_: (*Mapel88*) bug in iis_module - create_cert_binding + - **ISSUE** `#42936`_: (*Mapel88*) bug in win_iis module & state - container_setting + | refs: `#43073`_ + * ee209b1 Merge pull request `#43073`_ from Mapel88/patch-2 + * b1a3d15 Remove trailing whitespace for linter + + * 25c8190 Fix pylint errors + + * 1eba8c4 Fix pylint errors + + * 290d7b5 Fix plint errors + + * f4f3242 Fix plint errors + + * ec20e9a Fix bug `#43110`_ - win_iis module + + * 009ef66 Fix dictionary keys from string to int + + * dc793f9 Fix bug `#42936`_ - win_iis state + + * 13404a4 Fix bug `#42936`_ - win_iis module + +- **PR** `#43254`_: (*twangboy*) Fix `unit.modules.test_inspect_collector` on Windows + @ *2017-08-30T15:46:07Z* + + * ec1bedc Merge pull request `#43254`_ from twangboy/win_fix_test_inspect_collector + * b401340 Fix `unit.modules.test_inspect_collector` on Windows + +- **PR** `#43255`_: (*gtmanfred*) always return a dict object + @ *2017-08-30T14:47:15Z* + + - **ISSUE** `#43241`_: (*mirceaulinic*) Error whilst collecting napalm grains + | refs: `#43255`_ + * 1fc7307 Merge pull request `#43255`_ from gtmanfred/2017.7 + * 83b0bab opt_args needs to be a dict + +- **PR** `#43229`_: (*twangboy*) Bring changes from `#43228`_ to 2017.7 + @ *2017-08-30T14:26:55Z* + + - **PR** `#43228`_: (*twangboy*) Win fix pkg.install + | refs: `#43229`_ + * fa904ee Merge pull request `#43229`_ from twangboy/win_fix_pkg.install-2017.7 + * e007a1c Fix regex, add `.` + + * 23ec47c Add _ to regex search + + * b1788b1 Bring changes from `#43228`_ to 2017.7 + +- **PR** `#43251`_: (*twangboy*) Skips `unit.modules.test_groupadd` on Windows + @ *2017-08-30T13:56:36Z* + + * 25666f8 Merge pull request `#43251`_ from twangboy/win_skip_test_groupadd + * 5185071 Skips `unit.modules.test_groupadd` on Windows + +- **PR** `#43256`_: (*twangboy*) Skip mac tests for user and group + @ *2017-08-30T13:18:13Z* + + * a8e0962 Merge pull request `#43256`_ from twangboy/win_skip_mac_tests + * cec627a Skip mac tests for user and group + +- **PR** `#43226`_: (*lomeroe*) Fixes for issues in PR `#43166`_ + @ *2017-08-29T19:05:39Z* + + - **ISSUE** `#42279`_: (*dafyddj*) win_lgpo matches multiple policies due to startswith() + | refs: `#43116`_ `#43116`_ `#43166`_ `#43226`_ `#43156`_ + - **PR** `#43166`_: (*lomeroe*) Backport `#43116`_ to 2017.7 + | refs: `#43226`_ + - **PR** `#43156`_: (*lomeroe*) Backport `#43116`_ to 2017.7 + | refs: `#43166`_ + - **PR** `#43116`_: (*lomeroe*) Fix 42279 in develop + | refs: `#43166`_ `#43156`_ + - **PR** `#39773`_: (*twangboy*) Make win_file use the win_dacl salt util + | refs: `#43226`_ + * ac2189c Merge pull request `#43226`_ from lomeroe/fix_43166 + * 0c424dc Merge branch '2017.7' into fix_43166 + + * 324cfd8d correcting bad format statement in search for policy to be disabled (fix for `#43166`_) verify that file exists before attempting to remove (fix for commits from `#39773`_) + +- **PR** `#43227`_: (*twangboy*) Fix `unit.fileserver.test_gitfs` for Windows + @ *2017-08-29T19:03:36Z* + + * 6199fb4 Merge pull request `#43227`_ from twangboy/win_fix_unit_test_gitfs + * c956d24 Fix is_windows detection when USERNAME missing + + * 869e8cc Fix `unit.fileserver.test_gitfs` for Windows + +- **PR** `#43217`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-28T16:36:28Z* + + - **ISSUE** `#43101`_: (*aogier*) genesis.bootstrap fails if no pkg AND exclude_pkgs (which can't be a string) + | refs: `#43103`_ + - **ISSUE** `#42642`_: (*githubcdr*) state.augeas + | refs: `#42669`_ `#43202`_ + - **ISSUE** `#42329`_: (*jagguli*) State git.latest does not pull latest tags + | refs: `#42663`_ + - **PR** `#43202`_: (*garethgreenaway*) Reverting previous augeas module changes + - **PR** `#43103`_: (*aogier*) genesis.bootstrap deboostrap fix + - **PR** `#42663`_: (*jagguli*) Check remote tags before deciding to do a fetch `#42329`_ + * 6adc03e Merge pull request `#43217`_ from rallytime/merge-2017.7 + * 3911df2 Merge branch '2016.11' into '2017.7' + + * 5308c27 Merge pull request `#43202`_ from garethgreenaway/42642_2016_11_augeas_module_revert_fix + + * ef7e93e Reverting this change due to it breaking other uses. + + * f16b724 Merge pull request `#43103`_ from aogier/43101-genesis-bootstrap + + * db94f3b better formatting + + * e5cc667 tests: fix a leftover and simplify some parts + + * 13e5997 lint + + * 216ced6 allow comma-separated pkgs lists, quote args, test deb behaviour + + * d8612ae fix debootstrap and enhance packages selection/deletion via cmdline + + * 4863771 Merge pull request `#42663`_ from StreetHawkInc/fix_git_tag_check + + * 2b5af5b Remove refs/tags prefix from remote tags + + * 3f2e96e Convert set to list for serializer + + * 2728e5d Only include new tags in changes + + * 4b1df2f Exclude annotated tags from checks + + * 389c037 Check remote tags before deciding to do a fetch `#42329`_ + +- **PR** `#43201`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-25T22:56:46Z* + + - **ISSUE** `#43198`_: (*corywright*) disk.format_ needs to be aliased to disk.format + | refs: `#43199`_ + - **ISSUE** `#43143`_: (*abulford*) git.detached does not fetch if rev is missing from local + | refs: `#43178`_ + - **ISSUE** `#495`_: (*syphernl*) mysql.* without having MySQL installed/configured gives traceback + | refs: `#43196`_ + - **PR** `#43199`_: (*corywright*) Add `disk.format` alias for `disk.format_` + - **PR** `#43196`_: (*gtmanfred*) Pin request install to version for npm tests + - **PR** `#43179`_: (*terminalmage*) Fix missed deprecation + - **PR** `#43178`_: (*terminalmage*) git.detached: Fix traceback when rev is a SHA and is not present locally + - **PR** `#43173`_: (*Ch3LL*) Add New Release Branch Strategy to Contribution Docs + - **PR** `#43171`_: (*terminalmage*) Add warning about adding new functions to salt/utils/__init__.py + * a563a94 Merge pull request `#43201`_ from rallytime/merge-2017.7 + * d40eba6 Merge branch '2016.11' into '2017.7' + + * 4193e7f Merge pull request `#43199`_ from corywright/disk-format-alias + + * f00d3a9 Add `disk.format` alias for `disk.format_` + + * 5471f9f Merge pull request `#43196`_ from gtmanfred/2016.11 + + * ccd2241 Pin request install to version + + * ace2715 Merge pull request `#43178`_ from terminalmage/issue43143 + + * 2640833 git.detached: Fix traceback when rev is a SHA and is not present locally + + * 12e9507 Merge pull request `#43179`_ from terminalmage/old-deprecation + + * 3adf8ad Fix missed deprecation + + * b595440 Merge pull request `#43171`_ from terminalmage/salt-utils-warning + + * 7b5943a Add warning about adding new functions to salt/utils/__init__.py + + * 4f273ca Merge pull request `#43173`_ from Ch3LL/add_branch_docs + + * 1b24244 Add New Release Branch Strategy to Contribution Docs + +- **PR** `#42997`_: (*twangboy*) Fix `unit.test_test_module_names` for Windows + @ *2017-08-25T21:19:11Z* + + * ce04ab4 Merge pull request `#42997`_ from twangboy/win_fix_test_module_names + * 2722e95 Use os.path.join to create paths + +- **PR** `#43006`_: (*SuperPommeDeTerre*) Try to fix `#26995`_ + @ *2017-08-25T21:16:07Z* + + - **ISSUE** `#26995`_: (*jbouse*) Issue with artifactory.downloaded and snapshot artifacts + | refs: `#43006`_ `#43006`_ + * c0279e4 Merge pull request `#43006`_ from SuperPommeDeTerre/SuperPommeDeTerre-patch-`#26995`_ + * 30dd6f5 Merge remote-tracking branch 'upstream/2017.7' into SuperPommeDeTerre-patch-`#26995`_ + + * f42ae9b Merge branch 'SuperPommeDeTerre-patch-`#26995`_' of https://github.com/SuperPommeDeTerre/salt into SuperPommeDeTerre-patch-`#26995`_ + + * 50ee3d5 Merge remote-tracking branch 'remotes/origin/2017.7' into SuperPommeDeTerre-patch-`#26995`_ + + * 0b666e1 Fix typo. + + * 1b8729b Fix for `#26995`_ + + * e314102 Fix typo. + + * db11e19 Fix for `#26995`_ + +- **PR** `#43184`_: (*terminalmage*) docker.compare_container: Perform boolean comparison when one side's value is null/None + @ *2017-08-25T18:42:11Z* + + - **ISSUE** `#43162`_: (*MorphBonehunter*) docker_container.running interference with restart_policy + | refs: `#43184`_ + * b6c5314 Merge pull request `#43184`_ from terminalmage/issue43162 + * 081f42a docker.compare_container: Perform boolean comparison when one side's value is null/None + +- **PR** `#43165`_: (*mirceaulinic*) Improve napalm state output in debug mode + @ *2017-08-24T23:05:37Z* + + * 688125b Merge pull request `#43165`_ from cloudflare/fix-napalm-ret + * c10717d Lint and fix + + * 1cd33cb Simplify the loaded_ret logic + + * 0bbea6b Document the new compliance_report arg + + * 3a90610 Include compliance reports + + * 3634055 Improve napalm state output in debug mode + +- **PR** `#43155`_: (*terminalmage*) Resolve image ID during container comparison + @ *2017-08-24T22:09:47Z* + + * a6a327b Merge pull request `#43155`_ from terminalmage/issue43001 + * 0186835 Fix docstring in test + + * a0bb654 Fixing lint issues + + * d5b2a0b Resolve image ID during container comparison + +- **PR** `#43170`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-24T19:22:26Z* + + - **PR** `#43151`_: (*ushmodin*) state.sls hangs on file.recurse with clean: True on windows + - **PR** `#42969`_: (*ushmodin*) state.sls hangs on file.recurse with clean: True on windows + | refs: `#43151`_ + * c071fd4 Merge pull request `#43170`_ from rallytime/merge-2017.7 + * 3daad5a Merge branch '2016.11' into '2017.7' + + * 669b376 Merge pull request `#43151`_ from ushmodin/2016.11 + + * c5841e2 state.sls hangs on file.recurse with clean: True on windows + +- **PR** `#43168`_: (*rallytime*) Back-port `#43041`_ to 2017.7 + @ *2017-08-24T19:07:23Z* + + - **ISSUE** `#43040`_: (*darcoli*) gitFS ext_pillar with branch name __env__ results in empty pillars + | refs: `#43041`_ `#43041`_ + - **PR** `#43041`_: (*darcoli*) Do not try to match pillarenv with __env__ + | refs: `#43168`_ + * 034c325 Merge pull request `#43168`_ from rallytime/`bp-43041`_ + * d010b74 Do not try to match pillarenv with __env__ + +- **PR** `#43172`_: (*rallytime*) Move new utils/__init__.py funcs to utils.files.py + @ *2017-08-24T19:05:30Z* + + - **PR** `#43056`_: (*damon-atkins*) safe_filename_leaf(file_basename) and safe_filepath(file_path_name) + | refs: `#43172`_ + * d48938e Merge pull request `#43172`_ from rallytime/move-utils-funcs + * 5385c79 Move new utils/__init__.py funcs to utils.files.py + +- **PR** `#43061`_: (*pabloh007*) Have docker.save use the image name when valid if not use image id, i… + @ *2017-08-24T16:32:02Z* + + - **ISSUE** `#43043`_: (*pabloh007*) docker.save and docker.load problem + | refs: `#43061`_ `#43061`_ + * e60f586 Merge pull request `#43061`_ from pabloh007/fix-save-image-name-id + * 0ffc57d Have docker.save use the image name when valid if not use image id, issue when loading and image is savid with id issue `#43043`_ + +- **PR** `#43166`_: (*lomeroe*) Backport `#43116`_ to 2017.7 + | refs: `#43226`_ + @ *2017-08-24T15:01:23Z* + + - **ISSUE** `#42279`_: (*dafyddj*) win_lgpo matches multiple policies due to startswith() + | refs: `#43116`_ `#43116`_ `#43166`_ `#43226`_ `#43156`_ + - **PR** `#43156`_: (*lomeroe*) Backport `#43116`_ to 2017.7 + | refs: `#43166`_ + - **PR** `#43116`_: (*lomeroe*) Fix 42279 in develop + | refs: `#43166`_ `#43156`_ + * 9da5754 Merge pull request `#43166`_ from lomeroe/`bp-43116`_-2017.7 + * af181b3 correct fopen calls from salt.utils for 2017.7 + + * f74480f lint fix + + * ecd446f track xml namespace to ensure policies w/duplicate IDs or Names do not conflict + + * 9f3047c add additional checks for ADM policies that have the same ADMX policy ID (`#42279`_) + +- **PR** `#43056`_: (*damon-atkins*) safe_filename_leaf(file_basename) and safe_filepath(file_path_name) + | refs: `#43172`_ + @ *2017-08-23T17:35:02Z* + + * 44b3cae Merge pull request `#43056`_ from damon-atkins/2017.7 + * 08ded15 more lint + + * 6e9c095 fix typo + + * ee41171 lint fixes + + * 8c864f0 fix missing imports + + * 964cebd safe_filename_leaf(file_basename) and safe_filepath(file_path_name) + +- **PR** `#43146`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-23T16:56:10Z* + + - **ISSUE** `#43036`_: (*mcarlton00*) Linux VMs in Bhyve aren't displayed properly in grains + | refs: `#43037`_ + - **PR** `#43100`_: (*vutny*) [DOCS] Add missing `utils` sub-dir listed for `extension_modules` + - **PR** `#43037`_: (*mcarlton00*) Issue `#43036`_ Bhyve virtual grain in Linux VMs + - **PR** `#42986`_: (*renner*) Notify systemd synchronously (via NOTIFY_SOCKET) + * 6ca9131 Merge pull request `#43146`_ from rallytime/merge-2017.7 + * bcbe180 Merge branch '2016.11' into '2017.7' + + * ae9d2b7 Merge pull request `#42986`_ from renner/systemd-notify + + * 79c53f3 Fallback to systemd_notify_call() in case of socket.error + + * f176547 Notify systemd synchronously (via NOTIFY_SOCKET) + + * b420fbe Merge pull request `#43037`_ from mcarlton00/fix-bhyve-grains + + * 73315f0 Issue `#43036`_ Bhyve virtual grain in Linux VMs + + * 0a86f2d Merge pull request `#43100`_ from vutny/doc-add-missing-utils-ext + + * af743ff [DOCS] Add missing `utils` sub-dir listed for `extension_modules` + +- **PR** `#43123`_: (*twangboy*) Fix `unit.utils.test_which` for Windows + @ *2017-08-23T16:01:39Z* + + * 03f6521 Merge pull request `#43123`_ from twangboy/win_fix_test_which + * ed97cff Fix `unit.utils.test_which` for Windows + +- **PR** `#43142`_: (*rallytime*) Back-port `#43068`_ to 2017.7 + @ *2017-08-23T15:56:48Z* + + - **ISSUE** `#42505`_: (*ikogan*) selinux.fcontext_policy_present exception looking for selinux.filetype_id_to_string + | refs: `#43068`_ + - **PR** `#43068`_: (*ixs*) Mark selinux._filetype_id_to_string as public function + | refs: `#43142`_ + * 5a4fc07 Merge pull request `#43142`_ from rallytime/`bp-43068`_ + * efc1c8c Mark selinux._filetype_id_to_string as public function + +- **PR** `#43038`_: (*twangboy*) Fix `unit.utils.test_url` for Windows + @ *2017-08-23T13:35:25Z* + + * 0467a0e Merge pull request `#43038`_ from twangboy/win_unit_utils_test_url + * 7f5ee55 Fix `unit.utils.test_url` for Windows + +- **PR** `#43097`_: (*twangboy*) Fix `group.present` for Windows + @ *2017-08-23T13:19:56Z* + + * e9ccaa6 Merge pull request `#43097`_ from twangboy/win_fix_group + * 43b0360 Fix lint + + * 9ffe315 Add kwargs + + * 4f4e34c Fix group state for Windows + +- **PR** `#43115`_: (*rallytime*) Back-port `#42067`_ to 2017.7 + @ *2017-08-22T20:09:52Z* + + - **PR** `#42067`_: (*vitaliyf*) Removed several uses of name.split('.')[0] in SoftLayer driver. + | refs: `#43115`_ + * 8140855 Merge pull request `#43115`_ from rallytime/`bp-42067`_ + * 8a6ad0a Fixed typo. + + * 9a5ae2b Removed several uses of name.split('.')[0] in SoftLayer driver. + +- **PR** `#42962`_: (*twangboy*) Fix `unit.test_doc test` for Windows + @ *2017-08-22T18:06:23Z* + + * 1e1a810 Merge pull request `#42962`_ from twangboy/win_unit_test_doc + * 201ceae Fix lint, remove debug statement + + * 37029c1 Fix unit.test_doc test + +- **PR** `#42995`_: (*twangboy*) Fix malformed requisite for Windows + @ *2017-08-22T16:50:01Z* + + * d347d1c Merge pull request `#42995`_ from twangboy/win_fix_invalid_requisite + * 93390de Fix malformed requisite for Windows + +- **PR** `#43108`_: (*rallytime*) Back-port `#42988`_ to 2017.7 + @ *2017-08-22T16:49:27Z* + + - **PR** `#42988`_: (*thusoy*) Fix broken negation in iptables + | refs: `#43108`_ + * 1c7992a Merge pull request `#43108`_ from rallytime/`bp-42988`_ + * 1a987cb Fix broken negation in iptables + +- **PR** `#43107`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-22T16:11:25Z* + + - **ISSUE** `#42869`_: (*abednarik*) Git Module : Failed to update repository + | refs: `#43064`_ + - **ISSUE** `#42041`_: (*lorengordon*) pkg.list_repo_pkgs fails to find pkgs with spaces around yum repo enabled value + | refs: `#43054`_ + - **ISSUE** `#15171`_: (*JensRantil*) Maximum recursion limit hit related to requisites + | refs: `#42985`_ + - **PR** `#43092`_: (*blarghmatey*) Fixed issue with silently passing all tests in Testinfra module + - **PR** `#43064`_: (*terminalmage*) Fix race condition in git.latest + - **PR** `#43060`_: (*twangboy*) Osx update pkg scripts + - **PR** `#43054`_: (*lorengordon*) Uses ConfigParser to read yum config files + - **PR** `#42985`_: (*DmitryKuzmenko*) Properly handle `prereq` having lost requisites. + - **PR** `#42045`_: (*arount*) Fix: salt.modules.yumpkg: ConfigParser to read ini like files. + | refs: `#43054`_ + * c6993f4 Merge pull request `#43107`_ from rallytime/merge-2017.7 + * 328dd6a Merge branch '2016.11' into '2017.7' + + * e2bf2f4 Merge pull request `#42985`_ from DSRCorporation/bugs/15171_recursion_limit + + * 651b1ba Properly handle `prereq` having lost requisites. + + * e513333 Merge pull request `#43092`_ from mitodl/2016.11 + + * d4b113a Fixed issue with silently passing all tests in Testinfra module + + * 77a443c Merge pull request `#43060`_ from twangboy/osx_update_pkg_scripts + + * ef8a14c Remove /opt/salt instead of /opt/salt/bin + + * 2dd62aa Add more information to the description + + * f44f5b7 Only stop services if they are running + + * 3b62bf9 Remove salt from the path + + * ebdca3a Update pkg-scripts + + * 1b1b6da Merge pull request `#43064`_ from terminalmage/issue42869 + + * 093c0c2 Fix race condition in git.latest + + * 96e8e83 Merge pull request `#43054`_ from lorengordon/fix/yumpkg/config-parser + + * 3b2cb81 fix typo in salt.modules.yumpkg + + * 38add0e break if leading comments are all fetched + + * d7f65dc fix configparser import & log if error was raised + + * ca1b1bb use configparser to parse yum repo file + +- **PR** `#42996`_: (*twangboy*) Fix `unit.test_stateconf` for Windows + @ *2017-08-21T22:43:58Z* + + * f9b4976 Merge pull request `#42996`_ from twangboy/win_fix_test_stateconf + * 92dc3c0 Use os.sep for path + +- **PR** `#43024`_: (*twangboy*) Fix `unit.utils.test_find` for Windows + @ *2017-08-21T22:38:10Z* + + * 19fc644 Merge pull request `#43024`_ from twangboy/win_unit_utils_test_find + * fbe54c9 Remove unused import six (lint) + + * b04d1a2 Fix `unit.utils.test_find` for Windows + +- **PR** `#43088`_: (*gtmanfred*) allow docker util to be reloaded with reload_modules + @ *2017-08-21T22:14:37Z* + + * 1a53116 Merge pull request `#43088`_ from gtmanfred/2017.7 + * 373a9a0 allow docker util to be reloaded with reload_modules + +- **PR** `#43091`_: (*blarghmatey*) Fixed issue with silently passing all tests in Testinfra module + @ *2017-08-21T22:06:22Z* + + * 83e528f Merge pull request `#43091`_ from mitodl/2017.7 + * b502560 Fixed issue with silently passing all tests in Testinfra module + +- **PR** `#41994`_: (*twangboy*) Fix `unit.modules.test_cmdmod` on Windows + @ *2017-08-21T21:53:01Z* + + * 5482524 Merge pull request `#41994`_ from twangboy/win_unit_test_cmdmod + * a5f7288 Skip test that uses pwd, not available on Windows + +- **PR** `#42933`_: (*garethgreenaway*) Fixes to osquery module + @ *2017-08-21T20:48:31Z* + + - **ISSUE** `#42873`_: (*TheVakman*) osquery Data Empty Upon Return / Reporting Not Installed + | refs: `#42933`_ + * b33c4ab Merge pull request `#42933`_ from garethgreenaway/42873_2017_7_osquery_fix + * 8915e62 Removing an import that is not needed. + + * 74bc377 Updating the other function that uses cmd.run_all + + * e6a4619 Better approach without using python_shell=True. + + * 5ac41f4 When running osquery commands through cmd.run we should pass python_shell=True to ensure everything is formatted right. `#42873`_ + +- **PR** `#43093`_: (*gtmanfred*) Fix ec2 list_nodes_full to work on 2017.7 + @ *2017-08-21T20:21:21Z* + + * 53c2115 Merge pull request `#43093`_ from gtmanfred/ec2 + * c7cffb5 This block isn't necessary + + * b7283bc _vm_provider_driver isn't needed anymore + +- **PR** `#43087`_: (*rallytime*) Back-port `#42174`_ to 2017.7 + @ *2017-08-21T18:40:18Z* + + - **ISSUE** `#43085`_: (*brejoc*) Patch for Kubernetes module missing from 2017.7 and 2017.7.1 + | refs: `#43087`_ + - **PR** `#42174`_: (*mcalmer*) kubernetes: provide client certificate authentication + | refs: `#43087`_ + * 32f9ade Merge pull request `#43087`_ from rallytime/`bp-42174`_ + * cf65636 add support for certificate authentication to kubernetes module + +- **PR** `#43029`_: (*terminalmage*) Normalize the salt caching API + @ *2017-08-21T16:54:58Z* + + * 882fcd8 Merge pull request `#43029`_ from terminalmage/fix-func-alias + * f8f74a3 Update localfs cache tests to reflect changes to func naming + + * c4ae79b Rename other refs to cache.ls with cache.list + + * ee59d12 Normalize the salt caching API + +- **PR** `#43039`_: (*gtmanfred*) catch ImportError for kubernetes.client import + @ *2017-08-21T14:32:38Z* + + - **ISSUE** `#42843`_: (*brejoc*) Kubernetes module won't work with Kubernetes Python client > 1.0.2 + | refs: `#42845`_ + - **PR** `#42845`_: (*brejoc*) API changes for Kubernetes version 2.0.0 + | refs: `#43039`_ + * dbee735 Merge pull request `#43039`_ from gtmanfred/kube + * 7e269cb catch ImportError for kubernetes.client import + +- **PR** `#43058`_: (*rallytime*) Update release version number for jenkins.run function + @ *2017-08-21T14:13:34Z* + + * c56a849 Merge pull request `#43058`_ from rallytime/fix-release-num + * d7eef70 Update release version number for jenkins.run function + +- **PR** `#43051`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-18T17:05:57Z* + + - **ISSUE** `#42992`_: (*pabloh007*) docker.save flag push does is ignored + - **ISSUE** `#42627`_: (*taigrrr8*) salt-cp no longer works. Was working a few months back. + | refs: `#42890`_ + - **ISSUE** `#40490`_: (*alxwr*) saltstack x509 incompatible to m2crypto 0.26.0 + | refs: `#42760`_ + - **PR** `#43048`_: (*rallytime*) Back-port `#43031`_ to 2016.11 + - **PR** `#43033`_: (*rallytime*) Back-port `#42760`_ to 2016.11 + - **PR** `#43032`_: (*rallytime*) Back-port `#42547`_ to 2016.11 + - **PR** `#43031`_: (*gtmanfred*) use a ruby gem that doesn't have dependencies + | refs: `#43048`_ + - **PR** `#43027`_: (*pabloh007*) Fixes ignore push flag for docker.push module issue `#42992`_ + - **PR** `#43026`_: (*rallytime*) Back-port `#43020`_ to 2016.11 + - **PR** `#43023`_: (*terminalmage*) Fixes/improvements to Jenkins state/module + - **PR** `#43021`_: (*terminalmage*) Use socket.AF_INET6 to get the correct value instead of doing an OS check + - **PR** `#43020`_: (*gtmanfred*) test with gem that appears to be abandoned + | refs: `#43026`_ + - **PR** `#43019`_: (*rallytime*) Update bootstrap script to latest stable: v2017.08.17 + - **PR** `#43014`_: (*Ch3LL*) Change AF_INET6 family for mac in test_host_to_ips + | refs: `#43021`_ + - **PR** `#43009`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + - **PR** `#42954`_: (*Ch3LL*) [2016.3] Bump latest and previous versions + - **PR** `#42949`_: (*Ch3LL*) Add Security Notice to 2016.3.7 Release Notes + - **PR** `#42942`_: (*Ch3LL*) [2016.3] Add clean_id function to salt.utils.verify.py + - **PR** `#42890`_: (*DmitryKuzmenko*) Make chunked mode in salt-cp optional + - **PR** `#42760`_: (*AFriemann*) Catch TypeError thrown by m2crypto when parsing missing subjects in c… + | refs: `#43033`_ + - **PR** `#42547`_: (*blarghmatey*) Updated testinfra modules to work with more recent versions + | refs: `#43032`_ + * 7b0c947 Merge pull request `#43051`_ from rallytime/merge-2017.7 + * 153a463 Lint: Add missing blank line + + * 84829a6 Merge branch '2016.11' into '2017.7' + + * 43aa46f Merge pull request `#43048`_ from rallytime/`bp-43031`_ + + * 35e4504 use a ruby gem that doesn't have dependencies + + * ad89ff3 Merge pull request `#43023`_ from terminalmage/fix-jenkins-xml-caching + + * 33fd8ff Update jenkins.py + + * fc306fc Add missing colon in `if` statement + + * 822eabc Catch exceptions raised when making changes to jenkins + + * 91b583b Improve and correct execption raising + + * f096917 Raise an exception if we fail to cache the config xml + + * 2957467 Merge pull request `#43026`_ from rallytime/`bp-43020`_ + + * 0eb15a1 test with gem that appears to be abandoned + + * 4150b09 Merge pull request `#43033`_ from rallytime/`bp-42760`_ + + * 3e3f7f5 Catch TypeError thrown by m2crypto when parsing missing subjects in certificate files. + + * b124d36 Merge pull request `#43032`_ from rallytime/`bp-42547`_ + + * ea4d7f4 Updated testinfra modules to work with more recent versions + + * a88386a Merge pull request `#43027`_ from pabloh007/fix-docker-save-push-2016-11 + + * d0fd949 Fixes ignore push flag for docker.push module issue `#42992`_ + + * 51d1684 Merge pull request `#42890`_ from DSRCorporation/bugs/42627_salt-cp + + * cfddbf1 Apply code review: update the doc + + * afedd3b Typos and version fixes in the doc. + + * 9fedf60 Fixed 'test_valid_docs' test. + + * 9993886 Make chunked mode in salt-cp optional (disabled by default). + + * b3c253c Merge pull request `#43009`_ from rallytime/merge-2016.11 + + * 566ba4f Merge branch '2016.3' into '2016.11' + + * 13b8637 Merge pull request `#42942`_ from Ch3LL/2016.3.6_follow_up + + * f281e17 move additional minion config options to 2016.3.8 release notes + + * 168604b remove merge conflict + + * 8a07d95 update release notes with cve number + + * 149633f Add release notes for 2016.3.7 release + + * 7a4cddc Add clean_id function to salt.utils.verify.py + + * bbb1b29 Merge pull request `#42954`_ from Ch3LL/latest_2016.3 + + * b551e66 [2016.3] Bump latest and previous versions + + * 5d5edc5 Merge pull request `#42949`_ from Ch3LL/2016.3.7_docs + + * d75d374 Add Security Notice to 2016.3.7 Release Notes + + * 37c63e7 Merge pull request `#43021`_ from terminalmage/fix-network-test + + * 4089b7b Use socket.AF_INET6 to get the correct value instead of doing an OS check + + * 8f64232 Merge pull request `#43019`_ from rallytime/bootstrap_2017.08.17 + + * 2f762b3 Update bootstrap script to latest stable: v2017.08.17 + + * ff1caeee Merge pull request `#43014`_ from Ch3LL/fix_network_mac + + * b8eee44 Change AF_INET6 family for mac in test_host_to_ips + +- **PR** `#43035`_: (*rallytime*) [2017.7] Merge forward from 2017.7.1 to 2017.7 + @ *2017-08-18T12:58:17Z* + + - **PR** `#42948`_: (*Ch3LL*) [2017.7.1] Add clean_id function to salt.utils.verify.py + | refs: `#43035`_ + - **PR** `#42945`_: (*Ch3LL*) [2017.7] Add clean_id function to salt.utils.verify.py + | refs: `#43035`_ + * d15b0ca Merge pull request `#43035`_ from rallytime/merge-2017.7 + * 756128a Merge branch '2017.7.1' into '2017.7' + + * ab1b099 Merge pull request `#42948`_ from Ch3LL/2017.7.0_follow_up + +- **PR** `#43034`_: (*rallytime*) Back-port `#43002`_ to 2017.7 + @ *2017-08-17T23:18:16Z* + + - **ISSUE** `#42989`_: (*blbradley*) GitFS GitPython performance regression in 2017.7.1 + | refs: `#43002`_ `#43002`_ + - **PR** `#43002`_: (*the-glu*) Try to fix `#42989`_ + | refs: `#43034`_ + * bcbb973 Merge pull request `#43034`_ from rallytime/`bp-43002`_ + * 350c076 Try to fix `#42989`_ by doing sslVerify and refspecs for origin remote only if there is no remotes + +- **PR** `#42958`_: (*gtmanfred*) runit module should also be loaded as runit + @ *2017-08-17T22:30:23Z* + + - **ISSUE** `#42375`_: (*dragonpaw*) salt.modules.*.__virtualname__ doens't work as documented. + | refs: `#42523`_ `#42958`_ + * 9182f55 Merge pull request `#42958`_ from gtmanfred/2017.7 + * fd68746 runit module should also be loaded as runit + +- **PR** `#43031`_: (*gtmanfred*) use a ruby gem that doesn't have dependencies + | refs: `#43048`_ + @ *2017-08-17T22:26:25Z* + + * 5985cc4 Merge pull request `#43031`_ from gtmanfred/test_gem + * ba80a7d use a ruby gem that doesn't have dependencies + +- **PR** `#43030`_: (*rallytime*) Small cleanup to dockermod.save + @ *2017-08-17T22:26:00Z* + + * 246176b Merge pull request `#43030`_ from rallytime/dockermod-minor-change + * d6a5e85 Small cleanup to dockermod.save + +- **PR** `#42993`_: (*pabloh007*) Fixes ignored push flag for docker.push module issue `#42992`_ + @ *2017-08-17T18:50:37Z* + + - **ISSUE** `#42992`_: (*pabloh007*) docker.save flag push does is ignored + * 1600011 Merge pull request `#42993`_ from pabloh007/fix-docker-save-push + * fe7554c Fixes ignored push flag for docker.push module issue `#42992`_ + +- **PR** `#42967`_: (*terminalmage*) Fix bug in on_header callback when no Content-Type is found in headers + @ *2017-08-17T18:48:52Z* + + - **ISSUE** `#42941`_: (*danlsgiga*) pkg.installed fails on installing from HTTPS rpm source + | refs: `#42967`_ + * 9009a97 Merge pull request `#42967`_ from terminalmage/issue42941 + * b838460 Fix bug in on_header callback when no Content-Type is found in headers + +- **PR** `#43016`_: (*gtmanfred*) service should return false on exception + @ *2017-08-17T18:08:05Z* + + - **ISSUE** `#43008`_: (*fillarios*) states.service.running always succeeds when watched state has changes + | refs: `#43016`_ + * 58f070d Merge pull request `#43016`_ from gtmanfred/service + * 21c264f service should return false on exception + +- **PR** `#43020`_: (*gtmanfred*) test with gem that appears to be abandoned + | refs: `#43026`_ + @ *2017-08-17T16:40:41Z* + + * 973d288 Merge pull request `#43020`_ from gtmanfred/test_gem + * 0a1f40a test with gem that appears to be abandoned + +- **PR** `#42999`_: (*garethgreenaway*) Fixes to slack engine + @ *2017-08-17T15:46:24Z* + + * 9cd0607 Merge pull request `#42999`_ from garethgreenaway/slack_engine_allow_editing_messages + * 0ece2a8 Fixing a bug that prevented editing Slack messages and having the commands resent to the Slack engine. + +- **PR** `#43010`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-17T15:10:29Z* + + - **ISSUE** `#42803`_: (*gmcwhistler*) master_type: str, not working as expected, parent salt-minion process dies. + | refs: `#42848`_ + - **ISSUE** `#42753`_: (*grichmond-salt*) SaltReqTimeout Error on Some Minions when One Master in a Multi-Master Configuration is Unavailable + | refs: `#42848`_ + - **ISSUE** `#42644`_: (*stamak*) nova salt-cloud -P Private IPs returned, but not public. Checking for misidentified IPs + | refs: `#42940`_ + - **ISSUE** `#38839`_: (*DaveOHenry*) Invoking runner.cloud.action via reactor sls fails + | refs: `#42291`_ + - **PR** `#42968`_: (*vutny*) [DOCS] Fix link to Salt Cloud Feature Matrix + - **PR** `#42959`_: (*rallytime*) Back-port `#42883`_ to 2016.11 + - **PR** `#42952`_: (*Ch3LL*) [2016.11] Bump latest and previous versions + - **PR** `#42950`_: (*Ch3LL*) Add Security Notice to 2016.11.7 Release Notes + - **PR** `#42944`_: (*Ch3LL*) [2016.11] Add clean_id function to salt.utils.verify.py + - **PR** `#42940`_: (*gtmanfred*) create new ip address before checking list of allocated ips + - **PR** `#42919`_: (*rallytime*) Back-port `#42871`_ to 2016.11 + - **PR** `#42918`_: (*rallytime*) Back-port `#42848`_ to 2016.11 + - **PR** `#42883`_: (*rallytime*) Fix failing boto tests + | refs: `#42959`_ + - **PR** `#42871`_: (*amalleo25*) Update joyent.rst + | refs: `#42919`_ + - **PR** `#42861`_: (*twangboy*) Fix pkg.install salt-minion using salt-call + - **PR** `#42848`_: (*DmitryKuzmenko*) Execute fire_master asynchronously in the main minion thread. + | refs: `#42918`_ + - **PR** `#42836`_: (*aneeshusa*) Backport salt.utils.versions from develop to 2016.11 + - **PR** `#42835`_: (*aneeshusa*) Fix typo in utils/versions.py module + | refs: `#42836`_ + - **PR** `#42798`_: (*s-sebastian*) Update return data before calling returners + - **PR** `#42291`_: (*vutny*) Fix `#38839`_: remove `state` from Reactor runner kwags + * 31627a9 Merge pull request `#43010`_ from rallytime/merge-2017.7 + * 8a0f948 Merge branch '2016.11' into '2017.7' + + * 1ee9499 Merge pull request `#42968`_ from vutny/doc-salt-cloud-ref + + * 44ed53b [DOCS] Fix link to Salt Cloud Feature Matrix + + * 923f974 Merge pull request `#42291`_ from vutny/`fix-38839`_ + + * 5f8f98a Fix `#38839`_: remove `state` from Reactor runner kwags + + * c20bc7d Merge pull request `#42940`_ from gtmanfred/2016.11 + + * 253e216 fix IP address spelling + + * bd63074 create new ip address before checking list of allocated ips + + * d6496ec Merge pull request `#42959`_ from rallytime/`bp-42883`_ + + * c6b9ca4 Lint fix: add missing space + + * 5597b1a Skip 2 failing tests in Python 3 due to upstream bugs + + * a0b19bd Update account id value in boto_secgroup module unit test + + * 60b406e @mock_elb needs to be changed to @mock_elb_deprecated as well + + * 6ae1111 Replace @mock_ec2 calls with @mock_ec2_deprecated calls + + * 6366e05 Merge pull request `#42944`_ from Ch3LL/2016.11.6_follow_up + + * 7e0a20a Add release notes for 2016.11.7 release + + * 63823f8 Add clean_id function to salt.utils.verify.py + + * 49d339c Merge pull request `#42952`_ from Ch3LL/latest_2016.11 + + * 74e7055 [2016.11] Bump latest and previous versions + + * b0d2e05 Merge pull request `#42950`_ from Ch3LL/2016.11.7_docs + + * a6f902d Add Security Notice to 2016.11.77 Release Notes + + * c0ff69f Merge pull request `#42836`_ from lyft/backport-utils.versions-to-2016.11 + + * 86ce700 Backport salt.utils.versions from develop to 2016.11 + + * 64a79dd Merge pull request `#42919`_ from rallytime/`bp-42871`_ + + * 4e46c96 Update joyent.rst + + * bea8ec1 Merge pull request `#42918`_ from rallytime/`bp-42848`_ + + * cdb4812 Make lint happier. + + * 62eca9b Execute fire_master asynchronously in the main minion thread. + + * 52bce32 Merge pull request `#42861`_ from twangboy/win_pkg_install_salt + + * 0d3789f Fix pkg.install salt-minion using salt-call + + * b9f4f87 Merge pull request `#42798`_ from s-sebastian/2016.11 + + * 1cc8659 Update return data before calling returners + +- **PR** `#42884`_: (*Giandom*) Convert to dict type the pillar string value passed from slack + @ *2017-08-16T22:30:43Z* + + - **ISSUE** `#42842`_: (*Giandom*) retreive kwargs passed with slack engine + | refs: `#42884`_ + * 82be9dc Merge pull request `#42884`_ from Giandom/2017.7.1-fix-slack-engine-pillar-args + * 80fd733 Update slack.py + +- **PR** `#42963`_: (*twangboy*) Fix `unit.test_fileclient` for Windows + @ *2017-08-16T14:18:18Z* + + * 42bd553 Merge pull request `#42963`_ from twangboy/win_unit_test_fileclient + * e9febe4 Fix unit.test_fileclient + +- **PR** `#42964`_: (*twangboy*) Fix `salt.utils.recursive_copy` for Windows + @ *2017-08-16T14:17:27Z* + + * 7dddeee Merge pull request `#42964`_ from twangboy/win_fix_recursive_copy + * 121cd4e Fix `salt.utils.recursive_copy` for Windows + +- **PR** `#42946`_: (*mirceaulinic*) extension_modules should default to $CACHE_DIR/proxy/extmods + @ *2017-08-15T21:26:36Z* + + - **ISSUE** `#42943`_: (*mirceaulinic*) `extension_modules` defaulting to `/var/cache/minion` although running under proxy minion + | refs: `#42946`_ + * 6da4d1d Merge pull request `#42946`_ from cloudflare/px_extmods_42943 + * 73f9135 extension_modules should default to /proxy/extmods + +- **PR** `#42945`_: (*Ch3LL*) [2017.7] Add clean_id function to salt.utils.verify.py + | refs: `#43035`_ + @ *2017-08-15T18:04:20Z* + + * 95645d4 Merge pull request `#42945`_ from Ch3LL/2017.7.0_follow_up + * dcd9204 remove extra doc + + * 693a504 update release notes with cve number + +- **PR** `#42812`_: (*terminalmage*) Update custom YAML loader tests to properly test unicode literals + @ *2017-08-15T17:50:22Z* + + - **ISSUE** `#42427`_: (*grichmond-salt*) Issue Passing Variables created from load_json as Inline Pillar Between States + | refs: `#42435`_ + - **PR** `#42435`_: (*terminalmage*) Modify our custom YAML loader to treat unicode literals as unicode strings + | refs: `#42812`_ + * 47ff9d5 Merge pull request `#42812`_ from terminalmage/yaml-loader-tests + * 9d8486a Add test for custom YAML loader with unicode literal strings + + * a0118bc Remove bytestrings and use textwrap.dedent for readability + +- **PR** `#42953`_: (*Ch3LL*) [2017.7] Bump latest and previous versions + @ *2017-08-15T17:23:28Z* + + * 5d0c219 Merge pull request `#42953`_ from Ch3LL/latest_2017.7 + * cbecf65 [2017.7] Bump latest and previous versions + +- **PR** `#42951`_: (*Ch3LL*) Add Security Notice to 2017.7.1 Release Notes + @ *2017-08-15T16:49:56Z* + + * 730e71d Merge pull request `#42951`_ from Ch3LL/2017.7.1_docs + * 1d8f827 Add Security Notice to 2017.7.1 Release Notes + +- **PR** `#42868`_: (*carsonoid*) Stub out required functions in redis_cache + @ *2017-08-15T14:33:54Z* + + * c1c8cb9 Merge pull request `#42868`_ from carsonoid/redisjobcachefix + * 885bee2 Stub out required functions for redis cache + +- **PR** `#42810`_: (*amendlik*) Ignore error values when listing Windows SNMP community strings + @ *2017-08-15T03:55:15Z* + + * e192d6e Merge pull request `#42810`_ from amendlik/win-snmp-community + * dc20e46 Ignore error values when listing Windows SNMP community strings + +- **PR** `#42920`_: (*cachedout*) pid_race + @ *2017-08-15T03:49:10Z* + + * a1817f1 Merge pull request `#42920`_ from cachedout/pid_race + * 5e930b8 If we catch the pid file in a transistory state, return None + +- **PR** `#42925`_: (*terminalmage*) Add debug logging to troubleshoot test failures + @ *2017-08-15T03:47:51Z* + + * 11a33fe Merge pull request `#42925`_ from terminalmage/f26-debug-logging + * 8165f46 Add debug logging to troubleshoot test failures + +- **PR** `#42913`_: (*twangboy*) Change service shutdown timeouts for salt-minion service (Windows) + @ *2017-08-14T20:55:24Z* + + * a537197 Merge pull request `#42913`_ from twangboy/win_change_timeout + * ffb23fb Remove the line that wipes out the cache + + * a3becf8 Change service shutdown timeouts + +- **PR** `#42800`_: (*skizunov*) Fix exception when master_type=disable + @ *2017-08-14T20:53:38Z* + + * ca0555f Merge pull request `#42800`_ from skizunov/develop6 + * fa58220 Fix exception when master_type=disable + +- **PR** `#42679`_: (*mirceaulinic*) Add multiprocessing option for NAPALM proxy + @ *2017-08-14T20:45:06Z* + + * 3af264b Merge pull request `#42679`_ from cloudflare/napalm-multiprocessing + * 9c4566d multiprocessing option tagged for 2017.7.2 + + * 37bca1b Add multiprocessing option for NAPALM proxy + + * a2565ba Add new napalm option: multiprocessing + +- **PR** `#42657`_: (*nhavens*) back-port `#42612`_ to 2017.7 + @ *2017-08-14T19:42:26Z* + + - **ISSUE** `#42611`_: (*nhavens*) selinux.boolean state does not return changes + | refs: `#42612`_ + - **PR** `#42612`_: (*nhavens*) fix for issue `#42611`_ + | refs: `#42657`_ + * 4fcdab3 Merge pull request `#42657`_ from nhavens/2017.7 + * d73c4b5 back-port `#42612`_ to 2017.7 + +- **PR** `#42709`_: (*whiteinge*) Add token_expire_user_override link to auth runner docstring + @ *2017-08-14T19:03:06Z* + + * d2b6ce3 Merge pull request `#42709`_ from whiteinge/doc-token_expire_user_override + * c7ea631 Add more docs on the token_expire param + + * 4a9f6ba Add token_expire_user_override link to auth runner docstring + +- **PR** `#42848`_: (*DmitryKuzmenko*) Execute fire_master asynchronously in the main minion thread. + | refs: `#42918`_ + @ *2017-08-14T18:28:38Z* + + - **ISSUE** `#42803`_: (*gmcwhistler*) master_type: str, not working as expected, parent salt-minion process dies. + | refs: `#42848`_ + - **ISSUE** `#42753`_: (*grichmond-salt*) SaltReqTimeout Error on Some Minions when One Master in a Multi-Master Configuration is Unavailable + | refs: `#42848`_ + * c6a7bf0 Merge pull request `#42848`_ from DSRCorporation/bugs/42753_mmaster_timeout + * 7f5412c Make lint happier. + + * ff66b7a Execute fire_master asynchronously in the main minion thread. + +- **PR** `#42911`_: (*gtmanfred*) cloud driver isn't a provider + @ *2017-08-14T17:47:16Z* + + * 6a3279e Merge pull request `#42911`_ from gtmanfred/2017.7 + * 99046b4 cloud driver isn't a provider + +- **PR** `#42860`_: (*skizunov*) hash_and_stat_file should return a 2-tuple + @ *2017-08-14T15:44:54Z* + + * 4456f73 Merge pull request `#42860`_ from skizunov/develop7 + * 5f85a03 hash_and_stat_file should return a 2-tuple + +- **PR** `#42889`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-14T14:16:20Z* + + - **ISSUE** `#41976`_: (*abulford*) dockerng network states do not respect test=True + | refs: `#41977`_ `#41977`_ + - **ISSUE** `#41770`_: (*Ch3LL*) NPM v5 incompatible with salt.modules.cache_list + | refs: `#42856`_ + - **ISSUE** `#475`_: (*thatch45*) Change yaml to use C bindings + | refs: `#42856`_ + - **PR** `#42886`_: (*sarcasticadmin*) Adding missing output flags to salt cli docs + - **PR** `#42882`_: (*gtmanfred*) make sure cmd is not run when npm isn't installed + - **PR** `#42877`_: (*terminalmage*) Add virtual func for cron state module + - **PR** `#42864`_: (*whiteinge*) Make syndic_log_file respect root_dir setting + - **PR** `#42859`_: (*terminalmage*) Add note about git CLI requirement for GitPython to GitFS tutorial + - **PR** `#42856`_: (*gtmanfred*) skip cache_clean test if npm version is >= 5.0.0 + - **PR** `#42788`_: (*amendlik*) Remove waits and retries from Saltify deployment + - **PR** `#41977`_: (*abulford*) Fix dockerng.network_* ignoring of tests=True + * c6ca7d6 Merge pull request `#42889`_ from rallytime/merge-2017.7 + * fb7117f Use salt.utils.versions.LooseVersion instead of distutils + + * 29ff19c Merge branch '2016.11' into '2017.7' + + * c15d003 Merge pull request `#41977`_ from redmatter/fix-dockerng-network-ignores-test + + * 1cc2aa5 Fix dockerng.network_* ignoring of tests=True + + * 3b9c3c5 Merge pull request `#42886`_ from sarcasticadmin/adding_docs_salt_outputs + + * 744bf95 Adding missing output flags to salt cli + + * e5b98c8 Merge pull request `#42882`_ from gtmanfred/2016.11 + + * da3402a make sure cmd is not run when npm isn't installed + + * 5962c95 Merge pull request `#42788`_ from amendlik/saltify-timeout + + * 928b523 Remove waits and retries from Saltify deployment + + * 227ecdd Merge pull request `#42877`_ from terminalmage/add-cron-state-virtual + + * f1de196 Add virtual func for cron state module + + * ab9f6ce Merge pull request `#42859`_ from terminalmage/gitpython-git-cli-note + + * 35e05c9 Add note about git CLI requirement for GitPython to GitFS tutorial + + * 682b4a8 Merge pull request `#42856`_ from gtmanfred/2016.11 + + * b458b89 skip cache_clean test if npm version is >= 5.0.0 + + * 01ea854 Merge pull request `#42864`_ from whiteinge/syndic-log-root_dir + + * 4b1f55d Make syndic_log_file respect root_dir setting + +- **PR** `#42898`_: (*mirceaulinic*) Minor eos doc correction + @ *2017-08-14T13:42:21Z* + + * 4b6fe2e Merge pull request `#42898`_ from mirceaulinic/patch-11 + * 93be79a Index eos under the installation instructions list + + * f903e7b Minor eos doc correction + +- **PR** `#42883`_: (*rallytime*) Fix failing boto tests + | refs: `#42959`_ + @ *2017-08-11T20:29:12Z* + + * 1764878 Merge pull request `#42883`_ from rallytime/fix-boto-tests + * 6a7bf99 Lint fix: add missing space + + * 4364322 Skip 2 failing tests in Python 3 due to upstream bugs + + * 7f46603 Update account id value in boto_secgroup module unit test + + * 7c1d493 @mock_elb needs to be changed to @mock_elb_deprecated as well + + * 3055e17 Replace @mock_ec2 calls with @mock_ec2_deprecated calls + +- **PR** `#42885`_: (*terminalmage*) Move weird tearDown test to an actual tearDown + @ *2017-08-11T19:14:42Z* + + * b21778e Merge pull request `#42885`_ from terminalmage/fix-f26-tests + * 462d653 Move weird tearDown test to an actual tearDown + +- **PR** `#42887`_: (*rallytime*) Remove extraneous "deprecated" notation + @ *2017-08-11T18:34:25Z* + + - **ISSUE** `#42870`_: (*boltronics*) webutil.useradd marked as deprecated:: 2016.3.0 by mistake? + | refs: `#42887`_ + * 9868ab6 Merge pull request `#42887`_ from rallytime/`fix-42870`_ + * 71e7581 Remove extraneous "deprecated" notation + +- **PR** `#42881`_: (*gtmanfred*) fix vmware for python 3.4.2 in salt.utils.vmware + @ *2017-08-11T17:52:29Z* + + * da71f2a Merge pull request `#42881`_ from gtmanfred/vmware + * 05ecc6a fix vmware for python 3.4.2 in salt.utils.vmware + +- **PR** `#42845`_: (*brejoc*) API changes for Kubernetes version 2.0.0 + | refs: `#43039`_ + @ *2017-08-11T14:04:30Z* + + - **ISSUE** `#42843`_: (*brejoc*) Kubernetes module won't work with Kubernetes Python client > 1.0.2 + | refs: `#42845`_ + * c7750d5 Merge pull request `#42845`_ from brejoc/updates-for-kubernetes-2.0.0 + * 81674aa Version info in :optdepends: not needed anymore + + * 7199550 Not depending on specific K8s version anymore + + * d8f7d7a API changes for Kubernetes version 2.0.0 + +- **PR** `#42678`_: (*frankiexyz*) Add eos.rst in the installation guide + @ *2017-08-11T13:58:37Z* + + * 459fded Merge pull request `#42678`_ from frankiexyz/2017.7 + * 1598571 Add eos.rst in the installation guide + +- **PR** `#42778`_: (*gtmanfred*) make sure to use the correct out_file + @ *2017-08-11T13:44:48Z* + + - **ISSUE** `#42646`_: (*gmacon*) SPM fails to install multiple packages + | refs: `#42778`_ + * 4ce96eb Merge pull request `#42778`_ from gtmanfred/spm + * 7ef691e make sure to use the correct out_file + +- **PR** `#42857`_: (*gtmanfred*) use older name if _create_unverified_context is unvailable + @ *2017-08-11T13:37:59Z* + + - **ISSUE** `#480`_: (*zyluo*) PEP8 types clean-up + | refs: `#42857`_ + * 3d05d89 Merge pull request `#42857`_ from gtmanfred/vmware + * c1f673e use older name if _create_unverified_context is unvailable + +- **PR** `#42866`_: (*twangboy*) Change to GitPython version 2.1.1 + @ *2017-08-11T13:23:52Z* + + * 7e8cfff Merge pull request `#42866`_ from twangboy/osx_downgrade_gitpython + * 28053a8 Change GitPython version to 2.1.1 + +- **PR** `#42855`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-10T21:40:39Z* + + - **ISSUE** `#42747`_: (*whiteinge*) Outputters mutate data which can be a problem for Runners and perhaps other things + | refs: `#42748`_ + - **ISSUE** `#42731`_: (*infoveinx*) http.query template_data render exception + | refs: `#42804`_ + - **ISSUE** `#42690`_: (*ChristianBeer*) git.latest state with remote set fails on first try + | refs: `#42694`_ + - **ISSUE** `#42683`_: (*rgcosma*) Gluster module broken in 2017.7 + | refs: `#42806`_ + - **ISSUE** `#42600`_: (*twangboy*) Unable to set 'Not Configured' using win_lgpo execution module + | refs: `#42744`_ `#42794`_ `#42795`_ + - **PR** `#42851`_: (*terminalmage*) Backport `#42651`_ to 2016.11 + - **PR** `#42838`_: (*twangboy*) Document requirements for win_pki + - **PR** `#42829`_: (*twangboy*) Fix passing version in pkgs as shown in docs + - **PR** `#42826`_: (*terminalmage*) Fix misspelling of "versions" + - **PR** `#42806`_: (*rallytime*) Update doc references in glusterfs.volume_present + - **PR** `#42805`_: (*rallytime*) Back-port `#42552`_ to 2016.11 + - **PR** `#42804`_: (*rallytime*) Back-port `#42784`_ to 2016.11 + - **PR** `#42795`_: (*lomeroe*) backport `#42744`_ to 2016.11 + - **PR** `#42786`_: (*Ch3LL*) Fix typo for template_dict in http docs + - **PR** `#42784`_: (*gtmanfred*) only read file if ret is not a string in http.query + | refs: `#42804`_ + - **PR** `#42764`_: (*amendlik*) Fix infinite loop with salt-cloud and Windows nodes + - **PR** `#42748`_: (*whiteinge*) Workaround Orchestrate problem that highstate outputter mutates data + - **PR** `#42744`_: (*lomeroe*) fix `#42600`_ in develop + | refs: `#42794`_ `#42795`_ + - **PR** `#42694`_: (*gtmanfred*) allow adding extra remotes to a repository + - **PR** `#42651`_: (*gtmanfred*) python2- prefix for fedora 26 packages + - **PR** `#42552`_: (*remijouannet*) update consul module following this documentation https://www.consul.… + | refs: `#42805`_ + * 3ce1863 Merge pull request `#42855`_ from rallytime/merge-2017.7 + * 08bbcf5 Merge branch '2016.11' into '2017.7' + + * 2dde1f7 Merge pull request `#42851`_ from terminalmage/`bp-42651`_ + + * a3da86e fix syntax + + * 6ecdbce make sure names are correct + + * f83b553 add py3 for versionlock + + * 21934f6 python2- prefix for fedora 26 packages + + * c746f79 Merge pull request `#42806`_ from rallytime/`fix-42683`_ + + * 8c8640d Update doc references in glusterfs.volume_present + + * 27a8a26 Merge pull request `#42829`_ from twangboy/win_pkg_fix_install + + * 83b9b23 Add winrepo to docs about supporting versions in pkgs + + * 81fefa6 Add ability to pass version in pkgs list + + * 3c3ac6a Merge pull request `#42838`_ from twangboy/win_doc_pki + + * f0a1d06 Standardize PKI Client + + * 7de687a Document requirements for win_pki + + * b3e2ae3 Merge pull request `#42805`_ from rallytime/`bp-42552`_ + + * 5a91c1f update consul module following this documentation https://www.consul.io/api/acl.html + + * d2ee793 Merge pull request `#42804`_ from rallytime/`bp-42784`_ + + * dbd29e4 only read file if it is not a string + + * 4cbf805 Merge pull request `#42826`_ from terminalmage/fix-spelling + + * 00f9314 Fix misspelling of "versions" + + * de997ed Merge pull request `#42786`_ from Ch3LL/fix_typo + + * 90a2fb6 Fix typo for template_dict in http docs + + * bf6153e Merge pull request `#42795`_ from lomeroe/`bp-42744`__201611 + + * 695f8c1 fix `#42600`_ in develop + + * 61fad97 Merge pull request `#42748`_ from whiteinge/save-before-output + + * de60b77 Workaround Orchestrate problem that highstate outputter mutates data + + * a4e3e7e Merge pull request `#42764`_ from amendlik/cloud-win-loop + + * f3dcfca Fix infinite loops on failed Windows deployments + + * da85326 Merge pull request `#42694`_ from gtmanfred/2016.11 + + * 1a0457a allow adding extra remotes to a repository + +- **PR** `#42808`_: (*terminalmage*) Fix regression in yum/dnf version specification + @ *2017-08-10T15:59:22Z* + + - **ISSUE** `#42774`_: (*rossengeorgiev*) pkg.installed succeeds, but fails when you specify package version + | refs: `#42808`_ + * f954f4f Merge pull request `#42808`_ from terminalmage/issue42774 + * c69f17d Add integration test for `#42774`_ + + * 78d826d Fix regression in yum/dnf version specification + +- **PR** `#42807`_: (*rallytime*) Update modules --> states in kubernetes doc module + @ *2017-08-10T14:10:40Z* + + - **ISSUE** `#42639`_: (*amnonbc*) k8s module needs a way to manage configmaps + | refs: `#42807`_ + * d9b0f44 Merge pull request `#42807`_ from rallytime/`fix-42639`_ + * 152eb88 Update modules --> states in kubernetes doc module + +- **PR** `#42841`_: (*Mapel88*) Fix bug `#42818`_ in win_iis module + @ *2017-08-10T13:44:21Z* + + - **ISSUE** `#42818`_: (*Mapel88*) Bug in win_iis module - "create_cert_binding" + | refs: `#42841`_ + * b8c7bda Merge pull request `#42841`_ from Mapel88/patch-1 + * 497241f Fix bug `#42818`_ in win_iis module + +- **PR** `#42782`_: (*rallytime*) Add a cmp compatibility function utility + @ *2017-08-09T22:37:29Z* + + - **ISSUE** `#42697`_: (*Ch3LL*) [Python3] NameError when running salt-run manage.versions + | refs: `#42782`_ + * 135f952 Merge pull request `#42782`_ from rallytime/`fix-42697`_ + * d707f94 Update all other calls to "cmp" function + + * 5605104 Add a cmp compatibility function utility + +- **PR** `#42784`_: (*gtmanfred*) only read file if ret is not a string in http.query + | refs: `#42804`_ + @ *2017-08-08T17:20:13Z* + + * ac75222 Merge pull request `#42784`_ from gtmanfred/http + * d397c90 only read file if it is not a string + +- **PR** `#42794`_: (*lomeroe*) Backport `#42744`_ to 2017.7 + @ *2017-08-08T17:16:31Z* + + - **ISSUE** `#42600`_: (*twangboy*) Unable to set 'Not Configured' using win_lgpo execution module + | refs: `#42744`_ `#42794`_ `#42795`_ + - **PR** `#42744`_: (*lomeroe*) fix `#42600`_ in develop + | refs: `#42794`_ `#42795`_ + * 44995b1 Merge pull request `#42794`_ from lomeroe/`bp-42744`_ + * 0acffc6 fix `#42600`_ in develop + +- **PR** `#42708`_: (*cro*) Do not change the arguments of the function when memoizing + @ *2017-08-08T13:47:01Z* + + - **ISSUE** `#42707`_: (*cro*) Service module and state fails on FreeBSD + | refs: `#42708`_ + * dcf474c Merge pull request `#42708`_ from cro/dont_change_args_during_memoize + * a260e91 Do not change the arguments of the function when memoizing + +- **PR** `#42783`_: (*rallytime*) Sort lists before comparing them in python 3 unit test + @ *2017-08-08T13:25:15Z* + + - **PR** `#42206`_: (*rallytime*) [PY3] Fix test that is flaky in Python 3 + | refs: `#42783`_ + * ddb671b Merge pull request `#42783`_ from rallytime/fix-flaky-py3-test + * 998834f Sort lists before compairing them in python 3 unit test + +- **PR** `#42721`_: (*hibbert*) Allow no ip sg + @ *2017-08-07T22:07:18Z* + + * d69822f Merge pull request `#42721`_ from hibbert/allow_no_ip_sg + * f582568 allow_no_ip_sg: Allow user to not supply ipaddress or securitygroups when running boto_efs.create_mount_target + +- **PR** `#42769`_: (*terminalmage*) Fix domainname parameter input translation + @ *2017-08-07T20:46:07Z* + + - **ISSUE** `#42538`_: (*marnovdm*) docker_container.running issue since 2017.7.0: passing domainname gives Error 500: json: cannot unmarshal array into Go value of type string + | refs: `#42769`_ + * bf7938f Merge pull request `#42769`_ from terminalmage/issue42538 + * 665de2d Fix domainname parameter input translation + +- **PR** `#42388`_: (*The-Loeki*) pillar.items pillar_env & pillar_override are never used + @ *2017-08-07T17:51:48Z* + + * 7bf2cdb Merge pull request `#42388`_ from The-Loeki/patch-1 + * 664f4b5 pillar.items pillar_env & pillar_override are never used + +- **PR** `#42770`_: (*rallytime*) [2017.7] Merge forward from 2017.7.1 to 2017.7 + @ *2017-08-07T16:21:45Z* + + * 9a8c9eb Merge pull request `#42770`_ from rallytime/merge-2017.7.1-into-2017.7 + * 6d17c9d Merge branch '2017.7.1' into '2017.7' + +- **PR** `#42768`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-08-07T16:21:17Z* + + - **ISSUE** `#42686`_: (*gilbsgilbs*) Unable to set multiple RabbitMQ tags + | refs: `#42693`_ `#42693`_ + - **ISSUE** `#42642`_: (*githubcdr*) state.augeas + | refs: `#42669`_ `#43202`_ + - **ISSUE** `#41433`_: (*sbojarski*) boto_cfn.present fails when reporting error for failed state + | refs: `#42574`_ + - **PR** `#42693`_: (*gilbsgilbs*) Fix RabbitMQ tags not properly set. + - **PR** `#42669`_: (*garethgreenaway*) [2016.11] Fixes to augeas module + - **PR** `#42655`_: (*whiteinge*) Reenable cpstats for rest_cherrypy + - **PR** `#42629`_: (*xiaoanyunfei*) tornado api + - **PR** `#42623`_: (*terminalmage*) Fix unicode constructor in custom YAML loader + - **PR** `#42574`_: (*sbojarski*) Fixed error reporting in "boto_cfn.present" function. + - **PR** `#33806`_: (*cachedout*) Work around upstream cherrypy bug + | refs: `#42655`_ + * c765e52 Merge pull request `#42768`_ from rallytime/merge-2017.7 + * 0f75482 Merge branch '2016.11' into '2017.7' + + * 7b2119f Merge pull request `#42669`_ from garethgreenaway/42642_2016_11_augeas_module_fix + + * 2441308 Updating the call to shlex_split to pass the posix=False argument so that quotes are preserved. + + * 3072576 Merge pull request `#42629`_ from xiaoanyunfei/tornadoapi + + * 1e13383 tornado api + + * f0f00fc Merge pull request `#42655`_ from whiteinge/rest_cherrypy-reenable-stats + + * deb6316 Fix lint errors + + * 6bd91c8 Reenable cpstats for rest_cherrypy + + * 21cf15f Merge pull request `#42693`_ from gilbsgilbs/fix-rabbitmq-tags + + * 78fccdc Cast to list in case tags is a tuple. + + * 287b57b Fix RabbitMQ tags not properly set. + + * f2b0c9b Merge pull request `#42574`_ from sbojarski/boto-cfn-error-reporting + + * 5c945f1 Fix debug message in "boto_cfn._validate" function. + + * 181a1be Fixed error reporting in "boto_cfn.present" function. + + * bc1effc Merge pull request `#42623`_ from terminalmage/fix-unicode-constructor + + * fcf4588 Fix unicode constructor in custom YAML loader + +- **PR** `#42651`_: (*gtmanfred*) python2- prefix for fedora 26 packages + @ *2017-08-07T14:35:04Z* + + * 3f5827f Merge pull request `#42651`_ from gtmanfred/2017.7 + * 8784899 fix syntax + + * 178cc1b make sure names are correct + + * f179b97 add py3 for versionlock + + * 1958d18 python2- prefix for fedora 26 packages + +- **PR** `#42689`_: (*hibbert*) boto_efs_fix_tags: Fix `#42688`_ invalid type for parameter tags + @ *2017-08-06T17:47:07Z* + + - **ISSUE** `#42688`_: (*hibbert*) salt.modules.boto_efs module Invalid type for parameter Tags - type: , valid types: , + | refs: `#42689`_ + * 791248e Merge pull request `#42689`_ from hibbert/boto_efs_fix_tags + * 157fb28 boto_efs_fix_tags: Fix `#42688`_ invalid type for parameter tags + +- **PR** `#42745`_: (*terminalmage*) docker.compare_container: treat null oom_kill_disable as False + @ *2017-08-05T15:28:20Z* + + - **ISSUE** `#42705`_: (*hbruch*) salt.states.docker_container.running replaces container on subsequent runs if oom_kill_disable unsupported + | refs: `#42745`_ + * 1b34076 Merge pull request `#42745`_ from terminalmage/issue42705 + * 710bdf6 docker.compare_container: treat null oom_kill_disable as False + +- **PR** `#42704`_: (*whiteinge*) Add import to work around likely multiprocessing scoping bug + @ *2017-08-04T23:03:13Z* + + - **ISSUE** `#42649`_: (*tehsu*) local_batch no longer working in 2017.7.0, 500 error + | refs: `#42704`_ + * 5d5b220 Merge pull request `#42704`_ from whiteinge/expr_form-warn-scope-bug + * 03b675a Add import to work around likely multiprocessing scoping bug + +- **PR** `#42743`_: (*kkoppel*) Fix docker.compare_container for containers with links + @ *2017-08-04T16:00:33Z* + + - **ISSUE** `#42741`_: (*kkoppel*) docker_container.running keeps re-creating containers with links to other containers + | refs: `#42743`_ + * 888e954 Merge pull request `#42743`_ from kkoppel/fix-issue-42741 + * de6d3cc Update dockermod.py + + * 58b997c Added a helper function that removes container names from container HostConfig:Links values to enable compare_container() to make the correct decision about differences in links. + +- **PR** `#42710`_: (*gtmanfred*) use subtraction instead of or + @ *2017-08-04T15:14:14Z* + + - **ISSUE** `#42668`_: (*UtahDave*) Minions under syndics don't respond to MoM + | refs: `#42710`_ + - **ISSUE** `#42545`_: (*paul-mulvihill*) Salt-api failing to return results for minions connected via syndics. + | refs: `#42710`_ + * 03a7f9b Merge pull request `#42710`_ from gtmanfred/syndic + * 683561a use subtraction instead of or + +- **PR** `#42670`_: (*gtmanfred*) render kubernetes docs + @ *2017-08-03T20:30:56Z* + + * 005182b Merge pull request `#42670`_ from gtmanfred/kube + * bca1790 add version added info + + * 4bbfc75 render kubernetes docs + +- **PR** `#42712`_: (*twangboy*) Remove master config file from minion-only installer + @ *2017-08-03T20:25:02Z* + + * df354dd Merge pull request `#42712`_ from twangboy/win_build_pkg + * 8604312 Remove master conf in minion install + +- **PR** `#42714`_: (*cachedout*) Set fact gathering style to 'old' for test_junos + @ *2017-08-03T13:39:40Z* + + * bb1dfd4 Merge pull request `#42714`_ from cachedout/workaround_jnpr_test_bug + * 834d6c6 Set fact gathering style to 'old' for test_junos + +- **PR** `#42481`_: (*twangboy*) Fix `unit.test_crypt` for Windows + @ *2017-08-01T18:10:50Z* + + * 4c1d931 Merge pull request `#42481`_ from twangboy/win_unit_test_crypt + * 1025090 Remove chown mock, fix path seps + +- **PR** `#42654`_: (*morganwillcock*) Disable ZFS in the core grain for NetBSD + @ *2017-08-01T17:52:36Z* + + * 8bcefb5 Merge pull request `#42654`_ from morganwillcock/zfsgrain + * 49023de Disable ZFS grain on NetBSD + +- **PR** `#42453`_: (*gtmanfred*) don't pass user to makedirs on windows + @ *2017-07-31T19:57:57Z* + + - **ISSUE** `#42421`_: (*bartuss7*) archive.extracted on Windows failed when dir not exist + | refs: `#42453`_ + * 5baf265 Merge pull request `#42453`_ from gtmanfred/makedirs + * 559d432 fix tests + + * afa7a13 use logic from file.directory for makedirs + +- **PR** `#42603`_: (*twangboy*) Add runas_passwd as a global for states + @ *2017-07-31T19:49:49Z* + + * fb81e78 Merge pull request `#42603`_ from twangboy/win_fix_runas + * 0c9e400 Remove deprecation, add logic to state.py + + * 464ec34 Fix another instance of runas_passwd + + * 18d6ce4 Add global vars to cmd.call + + * 6c71ab6 Remove runas and runas_password after state run + + * 4ea264e Change to runas_password in docs + + * 61aba35 Deprecate password, make runas_password a named arg + + * 41f0f75 Add new var to list, change to runas_password + + * b9c91eb Add runas_passwd as a global for states + +- **PR** `#42541`_: (*Mareo*) Avoid confusing warning when using file.line + @ *2017-07-31T19:41:58Z* + + * 75ba23c Merge pull request `#42541`_ from epita/fix-file-line-warning + * 2fd172e Avoid confusing warning when using file.line + +- **PR** `#42625`_: (*twangboy*) Fix the list function in the win_wua execution module + @ *2017-07-31T19:27:16Z* + + * 3d328eb Merge pull request `#42625`_ from twangboy/fix_win_wua + * 1340c15 Add general usage instructions + + * 19f34bd Fix docs, formatting + + * b17495c Fix problem with list when install=True + +- **PR** `#42602`_: (*garethgreenaway*) Use superseded and deprecated configuration from pillar + @ *2017-07-31T18:53:06Z* + + - **ISSUE** `#42514`_: (*rickh563*) `module.run` does not work as expected in 2017.7.0 + | refs: `#42602`_ + * 25094ad Merge pull request `#42602`_ from garethgreenaway/42514_2017_7_superseded_deprecated_from_pillar + * 2e132da Slight update to formatting + + * 74bae13 Small update to something I missed in the first commit. Updating tests to also test for pillar values. + + * 928a480 Updating the superseded and deprecated decorators to work when specified as pillar values. + +- **PR** `#42621`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-28T19:45:51Z* + + - **ISSUE** `#42456`_: (*gdubroeucq*) Use yum lib + | refs: `#42586`_ + - **ISSUE** `#41982`_: (*abulford*) dockerng.network_* matches too easily + | refs: `#41988`_ `#41988`_ `#42006`_ `#42006`_ + - **PR** `#42586`_: (*gdubroeucq*) [Fix] yumpkg.py: add option to the command "check-update" + - **PR** `#42515`_: (*gtmanfred*) Allow not interpreting backslashes in the repl + - **PR** `#41988`_: (*abulford*) Fix dockerng.network_* name matching + | refs: `#42006`_ + * b7cd30d Merge pull request `#42621`_ from rallytime/merge-2017.7 + * 58dcb58 Merge branch '2016.11' into '2017.7' + + * cbf752c Merge pull request `#42515`_ from gtmanfred/backslash + + * cc4e456 Allow not interpreting backslashes in the repl + + * 5494958 Merge pull request `#42586`_ from gdubroeucq/2016.11 + + * 9c0b5cc Remove extra newline + + * d2ef448 yumpkg.py: clean + + * a96f7c0 yumpkg.py: add option to the command "check-update" + + * 6b45deb Merge pull request `#41988`_ from redmatter/fix-dockerng-network-matching + + * 9eea796 Add regression tests for `#41982`_ + + * 3369f00 Fix broken unit test test_network_absent + + * 0ef6cf6 Add trace logging of dockerng.networks result + + * 515c612 Fix dockerng.network_* name matching + +- **PR** `#42618`_: (*rallytime*) Back-port `#41690`_ to 2017.7 + @ *2017-07-28T19:27:11Z* + + - **ISSUE** `#34245`_: (*Talkless*) ini.options_present always report state change + | refs: `#41690`_ + - **PR** `#41690`_: (*m03*) Fix issue `#34245`_ with ini.options_present reporting changes + | refs: `#42618`_ + * d48749b Merge pull request `#42618`_ from rallytime/`bp-41690`_ + * 22c6a7c Improve output precision + + * ee4ea6b Fix `#34245`_ ini.options_present reporting changes + +- **PR** `#42619`_: (*rallytime*) Back-port `#42589`_ to 2017.7 + @ *2017-07-28T19:26:36Z* + + - **ISSUE** `#42588`_: (*ixs*) salt-ssh fails when using scan roster and detected minions are uncached + | refs: `#42589`_ + - **PR** `#42589`_: (*ixs*) Fix ssh-salt calls with scan roster for uncached clients + | refs: `#42619`_ + * e671242 Merge pull request `#42619`_ from rallytime/`bp-42589`_ + * cd5eb93 Fix ssh-salt calls with scan roster for uncached clients + +- **PR** `#42006`_: (*abulford*) Fix dockerng.network_* name matching + @ *2017-07-28T15:52:52Z* + + - **ISSUE** `#41982`_: (*abulford*) dockerng.network_* matches too easily + | refs: `#41988`_ `#41988`_ `#42006`_ `#42006`_ + - **PR** `#41988`_: (*abulford*) Fix dockerng.network_* name matching + | refs: `#42006`_ + * 7d385f8 Merge pull request `#42006`_ from redmatter/fix-dockerng-network-matching-2017.7 + * f83960c Lint: Remove extra line at end of file. + + * c7d364e Add regression tests for `#41982`_ + + * d31f291 Fix broken unit test test_network_absent + + * d42f781 Add trace logging of docker.networks result + + * 8c00c63 Fix dockerng.network_* name matching + +- **PR** `#42616`_: (*amendlik*) Sync cloud modules + @ *2017-07-28T15:40:36Z* + + - **ISSUE** `#12587`_: (*Katafalkas*) salt-cloud custom functions/actions + | refs: `#42616`_ + * ee8aee1 Merge pull request `#42616`_ from amendlik/sync-clouds + * ab21bd9 Sync cloud modules when saltutil.sync_all is run + +- **PR** `#42601`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-27T22:32:07Z* + + - **ISSUE** `#1036125`_: (**) + - **ISSUE** `#42477`_: (*aikar*) Invalid ssh_interface value prevents salt-cloud provisioning without reason of why + | refs: `#42479`_ + - **ISSUE** `#42405`_: (*felrivero*) The documentation is incorrectly compiled (PILLAR section) + | refs: `#42516`_ + - **ISSUE** `#42403`_: (*astronouth7303*) [2017.7] Pillar empty when state is applied from orchestrate + | refs: `#42433`_ + - **ISSUE** `#42375`_: (*dragonpaw*) salt.modules.*.__virtualname__ doens't work as documented. + | refs: `#42523`_ `#42958`_ + - **ISSUE** `#42371`_: (*tsaridas*) Minion unresponsive after trying to failover + | refs: `#42387`_ + - **ISSUE** `#41955`_: (*root360-AndreasUlm*) rabbitmq 3.6.10 changed output => rabbitmq-module broken + | refs: `#41968`_ + - **ISSUE** `#23516`_: (*dkiser*) BUG: cron job scheduler sporadically works + | refs: `#42077`_ + - **PR** `#42573`_: (*rallytime*) Back-port `#42433`_ to 2016.11 + - **PR** `#42571`_: (*twangboy*) Avoid loading system PYTHON* environment vars + - **PR** `#42551`_: (*binocvlar*) Remove '-s' (--script) argument to parted within align_check function + - **PR** `#42527`_: (*twangboy*) Document changes to Windows Update in Windows 10/Server 2016 + - **PR** `#42523`_: (*rallytime*) Add a mention of the True/False returns with __virtual__() + - **PR** `#42516`_: (*rallytime*) Add info about top file to pillar walk-through example to include edit.vim + - **PR** `#42479`_: (*gtmanfred*) validate ssh_interface for ec2 + - **PR** `#42433`_: (*terminalmage*) Only force saltenv/pillarenv to be a string when not None + | refs: `#42573`_ + - **PR** `#42414`_: (*vutny*) DOCS: unify hash sum with hash type format + - **PR** `#42387`_: (*DmitryKuzmenko*) Fix race condition in usage of weakvaluedict + - **PR** `#42339`_: (*isbm*) Bugfix: Jobs scheduled to run at a future time stay pending for Salt minions (bsc`#1036125`_) + - **PR** `#42077`_: (*vutny*) Fix scheduled job run on Master if `when` parameter is a list + | refs: `#42107`_ + - **PR** `#41973`_: (*vutny*) Fix Master/Minion scheduled jobs based on Cron expressions + | refs: `#42077`_ + - **PR** `#41968`_: (*root360-AndreasUlm*) Fix rabbitmqctl output sanitizer for version 3.6.10 + * e2dd443 Merge pull request `#42601`_ from rallytime/merge-2017.7 + * 36a1bcf Merge branch '2016.11' into '2017.7' + + * 4b16109 Merge pull request `#42339`_ from isbm/isbm-jobs-scheduled-in-a-future-bsc1036125 + + * bbba84c Bugfix: Jobs scheduled to run at a future time stay pending for Salt minions (bsc`#1036125`_) + + * 6c5a7c6 Merge pull request `#42077`_ from vutny/fix-jobs-scheduled-with-whens + + * b1960ce Fix scheduled job run on Master if `when` parameter is a list + + * f9cb536 Merge pull request `#42414`_ from vutny/unify-hash-params-format + + * d1f2a93 DOCS: unify hash sum with hash type format + + * 535c922 Merge pull request `#42523`_ from rallytime/`fix-42375`_ + + * 685c2cc Add information about returning a tuple with an error message + + * fa46651 Add a mention of the True/False returns with __virtual__() + + * 0df0e7e Merge pull request `#42527`_ from twangboy/win_wua + + * 0373791 Correct capatlization + + * af3bcc9 Document changes to Windows Update in 10/2016 + + * 69b0658 Merge pull request `#42551`_ from binocvlar/fix-lack-of-align-check-output + + * c4fabaa Remove '-s' (--script) argument to parted within align_check function + + * 9e0b4e9 Merge pull request `#42573`_ from rallytime/`bp-42433`_ + + * 0293429 Only force saltenv/pillarenv to be a string when not None + + * e931ed2 Merge pull request `#42571`_ from twangboy/win_add_pythonpath + + * d55a44d Avoid loading user site packages + + * 9af1eb2 Ignore any PYTHON* environment vars already on the system + + * 4e2fb03 Add pythonpath to batch files and service + + * de2f397 Merge pull request `#42387`_ from DSRCorporation/bugs/42371_KeyError_WeakValueDict + + * e721c7e Don't use `key in weakvaluedict` because it could lie. + + * 641a9d7 Merge pull request `#41968`_ from root360-AndreasUlm/fix-rabbitmqctl-output-handler + + * 76fd941 added tests for rabbitmq 3.6.10 output handler + + * 3602af1 Fix rabbitmqctl output handler for 3.6.10 + + * 66fede3 Merge pull request `#42479`_ from gtmanfred/interface + + * c32c1b2 fix pylint + + * 99ec634 validate ssh_interface for ec2 + + * a925c70 Merge pull request `#42516`_ from rallytime/`fix-42405`_ + + * e3a6717 Add info about top file to pillar walk-through example to include edit.vim + +- **PR** `#42290`_: (*isbm*) Backport of `#42270`_ + @ *2017-07-27T22:30:05Z* + + * 22eea38 Merge pull request `#42290`_ from isbm/isbm-module_run_parambug_42270_217 + * e38d432 Fix docs + + * 1e8a56e Describe function tagging + + * 1d72332 Describe function batching + + * 1391a05 Bugfix: syntax error in the example + + * 8c71257 Call unnamed parameters properly + + * 94c97a8 Update and correct the error message + + * ea83513 Bugfix: args gets ignored alongside named parameters + + * 74689e3 Add ability to use tagged functions in the same set + +- **PR** `#42251`_: (*twangboy*) Fix `unit.modules.test_win_ip` for Windows + @ *2017-07-27T19:22:03Z* + + * 4c20f1c Merge pull request `#42251`_ from twangboy/unit_win_test_win_ip + * 97261bf Fix win_inet_pton check for malformatted ip addresses + +- **PR** `#42255`_: (*twangboy*) Fix `unit.modules.test_win_system` for Windows + @ *2017-07-27T19:12:42Z* + + * 2985e4c Merge pull request `#42255`_ from twangboy/win_unit_test_win_system + * acc0345 Fix unit tests + +- **PR** `#42528`_: (*twangboy*) Namespace `cmp_to_key` in the pkg state for Windows + @ *2017-07-27T18:30:23Z* + + * a573386 Merge pull request `#42528`_ from twangboy/win_fix_pkg_state + * a040443 Move functools import inside pylint escapes + + * 118d513 Remove namespaced function `cmp_to_key` + + * a02c91a Namespace `cmp_to_key` in the pkg state for Windows + +- **PR** `#42534`_: (*jmarinaro*) Fixes AttributeError thrown by chocolatey state + @ *2017-07-27T17:59:50Z* + + - **ISSUE** `#42521`_: (*rickh563*) chocolatey.installed broken on 2017.7.0 + | refs: `#42534`_ + * 62ae12b Merge pull request `#42534`_ from jmarinaro/2017.7 + * b242d2d Fixes AttributeError thrown by chocolatey state Fixes `#42521`_ + +- **PR** `#42557`_: (*justincbeard*) Fixing output so --force-color and --no-color override master and min… + @ *2017-07-27T17:07:33Z* + + - **ISSUE** `#40354`_: (*exc414*) CentOS 6.8 Init Script - Sed unterminated address regex + | refs: `#42557`_ + - **ISSUE** `#37312`_: (*gtmanfred*) CLI flags should take overload settings in the config files + | refs: `#42557`_ + * 52605c2 Merge pull request `#42557`_ from justincbeard/bugfix_37312 + * ee3bc6e Fixing output so --force-color and --no-color override master and minion config color value + +- **PR** `#42567`_: (*skizunov*) Fix disable_ config option + @ *2017-07-27T17:05:00Z* + + * ab33517 Merge pull request `#42567`_ from skizunov/develop3 + * 0f0b7e3 Fix disable_ config option + +- **PR** `#42577`_: (*twangboy*) Compile scripts with -E -s params for Salt on Mac + @ *2017-07-26T22:44:37Z* + + * 30bb941 Merge pull request `#42577`_ from twangboy/mac_scripts + * 69d5973 Compile scripts with -E -s params for python + +- **PR** `#42524`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-26T22:41:06Z* + + - **ISSUE** `#42417`_: (*clem-compilatio*) salt-cloud - openstack - "no more floating IP addresses" error - but public_ip in node + | refs: `#42509`_ + - **ISSUE** `#42413`_: (*goten4*) Invalid error message when proxy_host is set and tornado not installed + | refs: `#42424`_ + - **ISSUE** `#42357`_: (*Giandom*) Salt pillarenv problem with slack engine + | refs: `#42443`_ `#42444`_ + - **ISSUE** `#42198`_: (*shengis*) state sqlite3.row_absent fail with "parameters are of unsupported type" + | refs: `#42200`_ + - **PR** `#42509`_: (*clem-compilatio*) Fix _assign_floating_ips in openstack.py + - **PR** `#42464`_: (*garethgreenaway*) [2016.11] Small fix to modules/git.py + - **PR** `#42443`_: (*garethgreenaway*) [2016.11] Fix to slack engine + - **PR** `#42424`_: (*goten4*) Fix error message when tornado or pycurl is not installed + - **PR** `#42200`_: (*shengis*) Fix `#42198`_ + * 60cd078 Merge pull request `#42524`_ from rallytime/merge-2017.7 + * 14d8d79 Merge branch '2016.11' into '2017.7' + + * 1bd5bbc Merge pull request `#42509`_ from clem-compilatio/`fix-42417`_ + + * 72924b0 Fix _assign_floating_ips in openstack.py + + * 4bf35a7 Merge pull request `#42464`_ from garethgreenaway/2016_11_remove_tmp_identity_file + + * ff24102 Uncomment the line that removes the temporary identity file. + + * e2120db Merge pull request `#42443`_ from garethgreenaway/42357_pass_args_kwargs_correctly + + * 635810b Updating the slack engine in 2016.11 to pass the args and kwrags correctly to LocalClient + + * 8262cc9 Merge pull request `#42200`_ from shengis/sqlite3_fix_row_absent_2016.11 + + * 407b8f4 Fix `#42198`_ If where_args is not set, not using it in the delete request. + + * d9df97e Merge pull request `#42424`_ from goten4/2016.11 + + * 1c0574d Fix error message when tornado or pycurl is not installed + +- **PR** `#42575`_: (*rallytime*) [2017.7] Merge forward from 2017.7.1 to 2017.7 + @ *2017-07-26T22:39:10Z* + + * 2acde83 Merge pull request `#42575`_ from rallytime/merge-2017.7.1-into-2017.7 + * 63bb0fb pass in empty kwarg for reactor + + * 2868061 update chunk, not kwarg in chunk + + * 46715e9 Merge branch '2017.7.1' into '2017.7' + +- **PR** `#42555`_: (*Ch3LL*) add changelog to 2017.7.1 release notes + @ *2017-07-26T14:57:43Z* + + * 1d93e92 Merge pull request `#42555`_ from Ch3LL/7.1_add_changelog + * fb69e71 add changelog to 2017.7.1 release notes + +- **PR** `#42266`_: (*twangboy*) Fix `unit.states.test_file` for Windows + @ *2017-07-25T20:26:32Z* + + * 07c2793 Merge pull request `#42266`_ from twangboy/win_unit_states_test_file + * 669aaee Mock file exists properly + + * a4231c9 Fix ret mock for linux + + * 0c484f8 Fix unit tests on Windows + +- **PR** `#42484`_: (*shengis*) Fix a potential Exception with an explicit error message + @ *2017-07-25T18:34:12Z* + + * df417ea Merge pull request `#42484`_ from shengis/fix-explicit-error-msg-x509-sign-remote + * 0b548c7 Fix a potential Exception with an explicit error message + +- **PR** `#42529`_: (*gtmanfred*) Fix joyent for python3 + @ *2017-07-25T16:37:48Z* + + - **ISSUE** `#41720`_: (*rallytime*) [Py3] Some salt-cloud drivers do not work using Python 3 + | refs: `#42529`_ + - **PR** `#396`_: (*mb0*) add file state template context and defaults + | refs: `#42529`_ + * 0f25ec7 Merge pull request `#42529`_ from gtmanfred/2017.7 + * b7ebb4d these drivers do not actually have an issue. + + * e90ca7a use salt encoding for joyent on 2017.7 + +- **PR** `#42465`_: (*garethgreenaway*) [2017.7] Small fix to modules/git.py + @ *2017-07-24T17:24:55Z* + + * 488457c Merge pull request `#42465`_ from garethgreenaway/2017_7_remove_tmp_identity_file + * 1920dc6 Uncomment the line that removes the temporary identity file. + +- **PR** `#42107`_: (*vutny*) [2017.7] Fix scheduled jobs if `when` parameter is a list + @ *2017-07-24T17:04:12Z* + + - **ISSUE** `#23516`_: (*dkiser*) BUG: cron job scheduler sporadically works + | refs: `#42077`_ + - **PR** `#42077`_: (*vutny*) Fix scheduled job run on Master if `when` parameter is a list + | refs: `#42107`_ + - **PR** `#41973`_: (*vutny*) Fix Master/Minion scheduled jobs based on Cron expressions + | refs: `#42077`_ + * 4f04499 Merge pull request `#42107`_ from vutny/2017.7-fix-jobs-scheduled-with-whens + * 905be49 [2017.7] Fix scheduled jobs if `when` parameter is a list + +- **PR** `#42506`_: (*terminalmage*) Add PER_REMOTE_ONLY to init_remotes call in git_pillar runner + @ *2017-07-24T16:59:21Z* + + * 6eaa076 Merge pull request `#42506`_ from terminalmage/fix-git-pillar-runner + * 6352f44 Add PER_REMOTE_ONLY to init_remotes call in git_pillar runner + +- **PR** `#42502`_: (*shengis*) Fix azurerm query to show IPs + @ *2017-07-24T15:54:45Z* + + * b88e645 Merge pull request `#42502`_ from shengis/fix_azurerm_request_ips + * 92f1890 Fix azurerm query to show IPs + +- **PR** `#42180`_: (*twangboy*) Fix `unit.modules.test_timezone` for Windows + @ *2017-07-24T14:46:16Z* + + * c793d83 Merge pull request `#42180`_ from twangboy/win_unit_test_timezone + * 832a3d8 Skip tests that use os.symlink on Windows + +- **PR** `#42474`_: (*whiteinge*) Cmd arg kwarg parsing test + @ *2017-07-24T14:13:30Z* + + - **PR** `#39646`_: (*terminalmage*) Handle deprecation of passing string args to load_args_and_kwargs + | refs: `#42474`_ + * 083ff00 Merge pull request `#42474`_ from whiteinge/cmd-arg-kwarg-parsing-test + * 0cc0c09 Lint fixes + + * 6609373 Add back support for string kwargs + + * 622ff5b Add LocalClient.cmd test for arg/kwarg parsing + + * 9f4eb80 Add a test.arg variant that cleans the pub kwargs by default + +- **PR** `#42425`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-21T22:43:41Z* + + - **ISSUE** `#42333`_: (*b3hni4*) Getting "invalid type of dict, a list is required" when trying to configure engines in master config file + | refs: `#42352`_ + - **ISSUE** `#32400`_: (*rallytime*) Document Default Config Values + | refs: `#42319`_ + - **PR** `#42370`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + - **PR** `#42368`_: (*twangboy*) Remove build and dist directories before install (2016.11) + - **PR** `#42360`_: (*Ch3LL*) [2016.11] Update version numbers in doc config for 2017.7.0 release + - **PR** `#42359`_: (*Ch3LL*) [2016.3] Update version numbers in doc config for 2017.7.0 release + - **PR** `#42356`_: (*meaksh*) Allow to check whether a function is available on the AliasesLoader wrapper + - **PR** `#42352`_: (*CorvinM*) Multiple documentation fixes + - **PR** `#42350`_: (*twangboy*) Fixes problem with Version and OS Release related grains on certain versions of Python (2016.11) + - **PR** `#42319`_: (*rallytime*) Add more documentation for config options that are missing from master/minion docs + * c91a5e5 Merge pull request `#42425`_ from rallytime/merge-2017.7 + * ea457aa Remove ALIASES block from template util + + * c673b64 Merge branch '2016.11' into '2017.7' + + * 42bb1a6 Merge pull request `#42350`_ from twangboy/win_fix_ver_grains_2016.11 + + * 8c04840 Detect Server OS with a desktop release name + + * 0a72e56 Merge pull request `#42356`_ from meaksh/2016.11-AliasesLoader-wrapper-fix + + * 915d942 Allow to check whether a function is available on the AliasesLoader wrapper + + * 10eb7b7 Merge pull request `#42368`_ from twangboy/win_fix_build_2016.11 + + * a7c910c Remove build and dist directories before install + + * 016189f Merge pull request `#42370`_ from rallytime/merge-2016.11 + + * 0aa5dde Merge branch '2016.3' into '2016.11' + + * e9b0f20 Merge pull request `#42359`_ from Ch3LL/doc-update-2016.3 + + * dc85b5e [2016.3] Update version numbers in doc config for 2017.7.0 release + + * f06a6f1 Merge pull request `#42360`_ from Ch3LL/doc-update-2016.11 + + * b90b7a7 [2016.11] Update version numbers in doc config for 2017.7.0 release + + * e0595b0 Merge pull request `#42319`_ from rallytime/config-docs + + * b40f980 Add more documentation for config options that are missing from master/minion docs + + * 7894040 Merge pull request `#42352`_ from CorvinM/issue42333 + + * 526b6ee Multiple documentation fixes + +- **PR** `#42444`_: (*garethgreenaway*) [2017.7] Fix to slack engine + @ *2017-07-21T22:03:48Z* + + - **ISSUE** `#42357`_: (*Giandom*) Salt pillarenv problem with slack engine + | refs: `#42443`_ `#42444`_ + * 10e4d92 Merge pull request `#42444`_ from garethgreenaway/42357_2017_7_pass_args_kwargs_correctly + * f411cfc Updating the slack engine in 2017.7 to pass the args and kwrags correctly to LocalClient + +- **PR** `#42461`_: (*rallytime*) Bump warning version from Oxygen to Fluorine in roster cache + @ *2017-07-21T21:33:25Z* + + * 723be49 Merge pull request `#42461`_ from rallytime/bump-roster-cache-deprecations + * c0df013 Bump warning version from Oxygen to Fluorine in roster cache + +- **PR** `#42436`_: (*garethgreenaway*) Fixes to versions function in manage runner + @ *2017-07-21T19:41:07Z* + + - **ISSUE** `#42374`_: (*tyhunt99*) [2017.7.0] salt-run mange.versions throws exception if minion is offline or unresponsive + | refs: `#42436`_ + * 0952160 Merge pull request `#42436`_ from garethgreenaway/42374_manage_runner_minion_offline + * 0fd3949 Updating the versions function inside the manage runner to account for when a minion is offline and we are unable to determine it's version. + +- **PR** `#42435`_: (*terminalmage*) Modify our custom YAML loader to treat unicode literals as unicode strings + | refs: `#42812`_ + @ *2017-07-21T19:40:34Z* + + - **ISSUE** `#42427`_: (*grichmond-salt*) Issue Passing Variables created from load_json as Inline Pillar Between States + | refs: `#42435`_ + * 54193ea Merge pull request `#42435`_ from terminalmage/issue42427 + * 31273c7 Modify our custom YAML loader to treat unicode literals as unicode strings + +- **PR** `#42399`_: (*rallytime*) Update old "ref" references to "rev" in git.detached state + @ *2017-07-21T19:38:59Z* + + - **ISSUE** `#42381`_: (*zebooka*) Git.detached broken in 2017.7.0 + | refs: `#42399`_ + - **ISSUE** `#38878`_: (*tomlaredo*) [Naming consistency] git.latest "rev" option VS git.detached "ref" option + | refs: `#38898`_ + - **PR** `#38898`_: (*terminalmage*) git.detached: rename ref to rev for consistency + | refs: `#42399`_ + * 0b31791 Merge pull request `#42399`_ from rallytime/`fix-42381`_ + * d9d94fe Update old "ref" references to "rev" in git.detached state + +- **PR** `#42031`_: (*skizunov*) Fix: Reactor emits critical error + @ *2017-07-21T19:38:34Z* + + - **ISSUE** `#42400`_: (*Enquier*) Conflict in execution of passing pillar data to orch/reactor event executions 2017.7.0 + | refs: `#42031`_ + * bd4adb4 Merge pull request `#42031`_ from skizunov/develop3 + * 540977b Fix: Reactor emits critical error + +- **PR** `#42027`_: (*gtmanfred*) import salt.minion for EventReturn for Windows + @ *2017-07-21T19:37:03Z* + + - **ISSUE** `#41949`_: (*jrporcaro*) Event returner doesn't work with Windows Master + | refs: `#42027`_ + * 3abf7ad Merge pull request `#42027`_ from gtmanfred/2017.7 + * fd4458b import salt.minion for EventReturn for Windows + +- **PR** `#42454`_: (*terminalmage*) Document future renaming of new rand_str jinja filter + @ *2017-07-21T18:47:51Z* + + * 994d3dc Merge pull request `#42454`_ from terminalmage/jinja-docs-2017.7 + * 98b6614 Document future renaming of new rand_str jinja filter + +- **PR** `#42452`_: (*Ch3LL*) update windows urls to new py2/py3 naming scheme + @ *2017-07-21T17:20:47Z* + + * 4480075 Merge pull request `#42452`_ from Ch3LL/fix_url_windows + * 3f4a918 update windows urls to new py2/py3 naming scheme + +- **PR** `#42411`_: (*seedickcode*) Fix file.managed check_cmd file not found - Issue `#42404`_ + @ *2017-07-20T21:59:17Z* + + - **ISSUE** `#42404`_: (*gabekahen*) [2017.7] file.managed with cmd_check "No such file or directory" + | refs: `#42411`_ + - **ISSUE** `#33708`_: (*pepinje*) visudo check command leaves cache file in /tmp + | refs: `#42411`_ `#38063`_ + - **PR** `#38063`_: (*llua*) tmp file clean up in file.manage - fix for `#33708`_ + | refs: `#42411`_ + * 33e90be Merge pull request `#42411`_ from seedickcode/check_cmd_fix + * 4ae3911 Fix file.managed check_cmd file not found - Issue `#42404`_ + +- **PR** `#42409`_: (*twangboy*) Add Scripts to build Py3 on Mac + @ *2017-07-20T21:36:34Z* + + * edde313 Merge pull request `#42409`_ from twangboy/mac_py3_scripts + * ac0e04a Remove build and dist, sign pkgs + + * 9d66e27 Fix hard coded pip path + + * 7b8d6cb Add support for Py3 + + * aa4eed9 Update Python and other reqs + +- **PR** `#42433`_: (*terminalmage*) Only force saltenv/pillarenv to be a string when not None + | refs: `#42573`_ + @ *2017-07-20T21:32:24Z* + + - **ISSUE** `#42403`_: (*astronouth7303*) [2017.7] Pillar empty when state is applied from orchestrate + | refs: `#42433`_ + * 82982f9 Merge pull request `#42433`_ from terminalmage/issue42403 +- **PR** `#42408`_: (*CorvinM*) Fix documentation misformat in salt.states.file.replace + @ *2017-07-20T00:45:43Z* + + * a71938c Merge pull request `#42408`_ from CorvinM/file-replace-doc-fix + * 246a2b3 Fix documentation misformat in salt.states.file.replace + +- **PR** `#42347`_: (*twangboy*) Fixes problem with Version and OS Release related grains on certain versions of Python + @ *2017-07-19T17:05:43Z* + + * d385dfd Merge pull request `#42347`_ from twangboy/win_fix_ver_grains + * ef1f663 Detect server OS with a desktop release name + +- **PR** `#42366`_: (*twangboy*) Remove build and dist directories before install + @ *2017-07-19T16:37:41Z* + + * eb9e420 Merge pull request `#42366`_ from twangboy/win_fix_build + * 0946002 Add blank line after delete + + * f7c0bb4 Remove build and dist directories before install + +- **PR** `#42373`_: (*Ch3LL*) Add initial 2017.7.1 Release Notes File + @ *2017-07-19T16:28:46Z* + + * af7820f Merge pull request `#42373`_ from Ch3LL/add_2017.7.1 + * ce1c1b6 Add initial 2017.7.1 Release Notes File + +- **PR** `#42150`_: (*twangboy*) Fix `unit.modules.test_pip` for Windows + @ *2017-07-19T16:01:17Z* + + * 59e012b Merge pull request `#42150`_ from twangboy/win_unit_test_pip + * 4ee2420 Fix unit tests for test_pip + +- **PR** `#42154`_: (*twangboy*) Fix `unit.modules.test_reg_win` for Windows + @ *2017-07-19T16:00:38Z* + + * ade25c6 Merge pull request `#42154`_ from twangboy/win_unit_test_reg + * 00d9a52 Fix problem with handling REG_QWORD in list values + +- **PR** `#42182`_: (*twangboy*) Fix `unit.modules.test_useradd` for Windows + @ *2017-07-19T15:55:33Z* + + * 0759367 Merge pull request `#42182`_ from twangboy/win_unit_test_useradd + * 8260a71 Disable tests that require pwd in Windows + +- **PR** `#42364`_: (*twangboy*) Windows Package notes for 2017.7.0 + @ *2017-07-18T19:24:45Z* + + * a175c40 Merge pull request `#42364`_ from twangboy/release_notes_2017.7.0 + * 96517d1 Add note about patched windows packages + +- **PR** `#42361`_: (*Ch3LL*) [2017.7] Update version numbers in doc config for 2017.7.0 release + @ *2017-07-18T19:23:22Z* + + * 4dfe50e Merge pull request `#42361`_ from Ch3LL/doc-update-2017.7 + * dc5bb30 [2017.7] Update version numbers in doc config for 2017.7.0 release + +- **PR** `#42363`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-18T18:40:48Z* + + - **ISSUE** `#42295`_: (*lubyou*) file.absent fails on windows if the file to be removed has the "readonly" attribute set + | refs: `#42308`_ + - **ISSUE** `#42267`_: (*gzcwnk*) salt-ssh not creating ssh keys automatically as per documentation + | refs: `#42314`_ + - **ISSUE** `#42240`_: (*casselt*) empty_password in user.present always changes password, even with test=True + | refs: `#42289`_ + - **ISSUE** `#42232`_: (*astronouth7303*) Half of dnsutil refers to dig + | refs: `#42235`_ + - **ISSUE** `#42194`_: (*jryberg*) pkg version: latest are now broken, appending -latest to filename + | refs: `#42275`_ + - **ISSUE** `#42152`_: (*dubb-b*) salt-cloud errors on Rackspace driver using -out=yaml + | refs: `#42282`_ + - **ISSUE** `#42137`_: (*kiemlicz*) cmd.run with multiple commands - random order of execution + | refs: `#42181`_ + - **ISSUE** `#42116`_: (*terminalmage*) CLI pillar override regression in 2017.7.0rc1 + | refs: `#42119`_ + - **ISSUE** `#42115`_: (*nomeelnoj*) Installing EPEL repo breaks salt-cloud + | refs: `#42163`_ + - **ISSUE** `#42114`_: (*clallen*) saltenv bug in pillar.get execution module function + | refs: `#42121`_ + - **ISSUE** `#41936`_: (*michaelkarrer81*) git.latest identity does not set the correct user for the private key file on the minion + | refs: `#41945`_ + - **ISSUE** `#41721`_: (*sazaro*) state.sysrc broken when setting the value to YES or NO + | refs: `#42269`_ + - **ISSUE** `#41116`_: (*hrumph*) FAQ has wrong instructions for upgrading Windows minion. + | refs: `#42264`_ + - **ISSUE** `#39365`_: (*dglloyd*) service.running fails if sysv script has no status command and enable: True + | refs: `#39366`_ + - **ISSUE** `#1`_: (*thatch45*) Enable regex on the salt cli + - **PR** `#42353`_: (*terminalmage*) is_windows is a function, not a propery/attribute + - **PR** `#42314`_: (*rallytime*) Add clarification to salt ssh docs about key auto-generation. + - **PR** `#42308`_: (*lubyou*) Force file removal on Windows. Fixes `#42295`_ + - **PR** `#42289`_: (*CorvinM*) Multiple empty_password fixes for state.user + - **PR** `#42282`_: (*rallytime*) Handle libcloud objects that throw RepresenterErrors with --out=yaml + - **PR** `#42275`_: (*terminalmage*) pkg.installed: pack name/version into pkgs argument + - **PR** `#42269`_: (*rallytime*) Add some clarity to "multiple quotes" section of yaml docs + - **PR** `#42264`_: (*rallytime*) Update minion restart section in FAQ doc for windows + - **PR** `#42262`_: (*rallytime*) Back-port `#42224`_ to 2016.11 + - **PR** `#42261`_: (*rallytime*) Some minor doc fixes for dnsutil module so they'll render correctly + - **PR** `#42253`_: (*gtmanfred*) Only use unassociated ips when unable to allocate + - **PR** `#42252`_: (*UtahDave*) simple docstring updates + - **PR** `#42235`_: (*astronouth7303*) Abolish references to `dig` in examples. + - **PR** `#42224`_: (*tdutrion*) Remove duplicate instruction in Openstack Rackspace config example + | refs: `#42262`_ + - **PR** `#42215`_: (*twangboy*) Add missing config to example + - **PR** `#42211`_: (*terminalmage*) Only pass a saltenv in orchestration if one was explicitly passed (2016.11) + - **PR** `#42181`_: (*garethgreenaway*) fixes to state.py for names parameter + - **PR** `#42176`_: (*rallytime*) Back-port `#42109`_ to 2016.11 + - **PR** `#42175`_: (*rallytime*) Back-port `#39366`_ to 2016.11 + - **PR** `#42173`_: (*rallytime*) Back-port `#37424`_ to 2016.11 + - **PR** `#42172`_: (*rallytime*) [2016.11] Merge forward from 2016.3 to 2016.11 + - **PR** `#42164`_: (*Ch3LL*) Fix kerberos create_keytab doc + - **PR** `#42163`_: (*vutny*) Fix `#42115`_: parse libcloud "rc" version correctly + - **PR** `#42155`_: (*phsteve*) Fix docs for puppet.plugin_sync + - **PR** `#42142`_: (*Ch3LL*) Update builds available for rc1 + - **PR** `#42141`_: (*rallytime*) Back-port `#42098`_ to 2016.11 + - **PR** `#42140`_: (*rallytime*) Back-port `#42097`_ to 2016.11 + - **PR** `#42123`_: (*vutny*) DOCS: describe importing custom util classes + - **PR** `#42121`_: (*terminalmage*) Fix pillar.get when saltenv is passed + - **PR** `#42119`_: (*terminalmage*) Fix regression in CLI pillar override for salt-call + - **PR** `#42109`_: (*arthurlogilab*) [doc] Update aws.rst - add Debian default username + | refs: `#42176`_ + - **PR** `#42098`_: (*twangboy*) Change repo_ng to repo-ng + | refs: `#42141`_ + - **PR** `#42097`_: (*gtmanfred*) require large timediff for ipv6 warning + | refs: `#42140`_ + - **PR** `#42095`_: (*terminalmage*) Add debug logging to dockerng.login + - **PR** `#42094`_: (*terminalmage*) Prevent command from showing in exception when output_loglevel=quiet + - **PR** `#41945`_: (*garethgreenaway*) Fixes to modules/git.py + - **PR** `#41543`_: (*cri-epita*) Fix user creation with empty password + | refs: `#42289`_ `#42289`_ + - **PR** `#39366`_: (*dglloyd*) Pass sig to service.status in after_toggle + | refs: `#42175`_ + - **PR** `#38965`_: (*toanju*) salt-cloud will use list_floating_ips for OpenStack + | refs: `#42253`_ + - **PR** `#37424`_: (*kojiromike*) Avoid Early Convert ret['comment'] to String + | refs: `#42173`_ + - **PR** `#34280`_: (*kevinanderson1*) salt-cloud will use list_floating_ips for Openstack + | refs: `#38965`_ + * 587138d Merge pull request `#42363`_ from rallytime/merge-2017.7 + * 7aa31ff Merge branch '2016.11' into '2017.7' + + * b256001 Merge pull request `#42353`_ from terminalmage/fix-git-test + + * 14cf6ce is_windows is a function, not a propery/attribute + + * 866a1fe Merge pull request `#42264`_ from rallytime/`fix-41116`_ + + * bd63888 Add mono-spacing to salt-minion reference for consistency + + * 30d62f4 Update minion restart section in FAQ doc for windows + + * 9a70708 Merge pull request `#42275`_ from terminalmage/issue42194 + + * 6638749 pkg.installed: pack name/version into pkgs argument + + * e588f23 Merge pull request `#42269`_ from rallytime/`fix-41721`_ + + * f2250d4 Add a note about using different styles of quotes. + + * 38d9b3d Add some clarity to "multiple quotes" section of yaml docs + + * 5aaa214 Merge pull request `#42282`_ from rallytime/`fix-42152`_ + + * f032223 Handle libcloud objects that throw RepresenterErrors with --out=yaml + + * fb5697a Merge pull request `#42308`_ from lubyou/42295-fix-file-absent-windows + + * 026ccf4 Force file removal on Windows. Fixes `#42295`_ + + * da2a8a5 Merge pull request `#42314`_ from rallytime/`fix-42267`_ + + * c406046 Add clarification to salt ssh docs about key auto-generation. + + * acadd54 Merge pull request `#41945`_ from garethgreenaway/41936_allow_identity_files_with_user + + * 44841e5 Moving the call to cp.get_file inside the with block to ensure the umask is preserved when we grab the file. + + * f9ba60e Merge pull request `#1`_ from terminalmage/pr-41945 + + * 1b60261 Restrict set_umask to mkstemp call only + + * 68549f3 Fixing umask to we can set files as executable. + + * 4949bf3 Updating to swap on the new salt.utils.files.set_umask context_manager + + * 8faa9f6 Updating PR with requested changes. + + * 494765e Updating the git module to allow an identity file to be used when passing the user parameter + + * f90e04a Merge pull request `#42289`_ from CorvinM/`bp-41543`_ + + * 357dc22 Fix user creation with empty password + + * a91a3f8 Merge pull request `#42123`_ from vutny/fix-master-utils-import + + * 6bb8b8f Add missing doc for ``utils_dirs`` Minion config option + + * f1bc58f Utils: add example of module import + + * e2aa511 Merge pull request `#42261`_ from rallytime/minor-doc-fix + + * 8c76bbb Some minor doc fixes for dnsutil module so they'll render correctly + + * 3e9dfbc Merge pull request `#42262`_ from rallytime/`bp-42224`_ + + * c31ded3 Remove duplicate instruction in Openstack Rackspace config example + + * 7780579 Merge pull request `#42181`_ from garethgreenaway/42137_backport_fix_from_2017_7 + + * a34970b Back porting the fix for 2017.7 that ensures the order of the names parameter. + + * 7253786 Merge pull request `#42253`_ from gtmanfred/2016.11 + + * 53e2576 Only use unassociated ips when unable to allocate + + * b2a4698 Merge pull request `#42252`_ from UtahDave/2016.11local + + * e6a9563 simple doc updates + + * 781fe13 Merge pull request `#42235`_ from astronouth7303/patch-1-2016.3 + + * 4cb51bd Make note of dig partial requirement. + + * 08e7d83 Abolish references to `dig` in examples. + + * 83cbd76 Merge pull request `#42215`_ from twangboy/win_iis_docs + + * c07e220 Add missing config to example + + * 274946a Merge pull request `#42211`_ from terminalmage/issue40928 + + * 22a18fa Only pass a saltenv in orchestration if one was explicitly passed (2016.11) + + * 89261cf Merge pull request `#42173`_ from rallytime/`bp-37424`_ + + * 01addb6 Avoid Early Convert ret['comment'] to String + + * 3b17fb7 Merge pull request `#42175`_ from rallytime/`bp-39366`_ + + * 53f7b98 Pass sig to service.status in after_toggle + + * ea16f47 Merge pull request `#42172`_ from rallytime/merge-2016.11 + + * b1fa332 Merge branch '2016.3' into '2016.11' + + * 8fa1fa5 Merge pull request `#42155`_ from phsteve/doc-fix-puppet + + * fb2cb78 Fix docs for puppet.plugin_sync so code-block renders properly and sync is spelled consistently + + * 6307b98 Merge pull request `#42176`_ from rallytime/`bp-42109`_ + + * 686926d Update aws.rst - add Debian default username + + * 28c4e4c Merge pull request `#42095`_ from terminalmage/docker-login-debugging + + * bd27870 Add debug logging to dockerng.login + + * 2b754bc Merge pull request `#42119`_ from terminalmage/issue42116 + + * 9a26894 Add integration test for 42116 + + * 1bb42bb Fix regression when CLI pillar override is used with salt-call + + * 8c0a83c Merge pull request `#42121`_ from terminalmage/issue42114 + + * d142912 Fix pillar.get when saltenv is passed + + * 687992c Merge pull request `#42094`_ from terminalmage/quiet-exception + + * 47d61f4 Prevent command from showing in exception when output_loglevel=quiet + + * dad2551 Merge pull request `#42163`_ from vutny/`fix-42115`_ + + * b27b1e3 Fix `#42115`_: parse libcloud "rc" version correctly + + * 2a8ae2b Merge pull request `#42164`_ from Ch3LL/fix_kerb_doc + + * 7c0fb24 Fix kerberos create_keytab doc + + * 678d4d4 Merge pull request `#42141`_ from rallytime/`bp-42098`_ + + * bd80243 Change repo_ng to repo-ng + + * c8afd7a Merge pull request `#42140`_ from rallytime/`bp-42097`_ + + * 9c4e132 Import datetime + + * 1435bf1 require large timediff for ipv6 warning + + * c239664 Merge pull request `#42142`_ from Ch3LL/change_builds + + * e1694af Update builds available for rc1 + +- **PR** `#42340`_: (*isbm*) Bugfix: Jobs scheduled to run at a future time stay pending for Salt … + @ *2017-07-18T18:13:36Z* + + - **ISSUE** `#1036125`_: (**) + * 55b7a5c Merge pull request `#42340`_ from isbm/isbm-jobs-scheduled-in-a-future-2017.7-bsc1036125 + * 774d204 Bugfix: Jobs scheduled to run at a future time stay pending for Salt minions (bsc`#1036125`_) + +- **PR** `#42327`_: (*mirceaulinic*) Default skip_verify to False + @ *2017-07-18T18:04:36Z* + + * e72616c Merge pull request `#42327`_ from mirceaulinic/patch-10 + * c830573 Trailing whitespaces + + * c83e6fc Default skip_verify to False + +- **PR** `#42179`_: (*rallytime*) Fix some documentation issues found in jinja filters doc topic + @ *2017-07-18T18:01:57Z* + + - **ISSUE** `#42151`_: (*sjorge*) Doc errors in jinja doc for develop branch + | refs: `#42179`_ `#42179`_ + * ba799b2 Merge pull request `#42179`_ from rallytime/`fix-42151`_ + * 798d292 Add note about "to_bytes" jinja filter issues when using yaml_jinja renderer + + * 1bbff57 Fix some documentation issues found in jinja filters doc topic + +- **PR** `#42087`_: (*abulford*) Make result=true if Docker volume already exists + @ *2017-07-17T18:41:47Z* + + - **ISSUE** `#42076`_: (*abulford*) dockerng.volume_present test looks as though it would cause a change + | refs: `#42086`_ `#42086`_ `#42087`_ `#42087`_ + - **PR** `#42086`_: (*abulford*) Make result=true if Docker volume already exists + | refs: `#42087`_ + * 8dbb938 Merge pull request `#42087`_ from redmatter/fix-dockerng-volume-present-result-2017.7 + * 2e1dc95 Make result=true if Docker volume already exists + +- **PR** `#42186`_: (*rallytime*) Use long_range function for IPv6Network hosts() function + @ *2017-07-17T18:39:35Z* + + - **ISSUE** `#42166`_: (*sjorge*) [2017.7.0rc1] jinja filter network_hosts fails on large IPv6 networks + | refs: `#42186`_ + * c84d6db Merge pull request `#42186`_ from rallytime/`fix-42166`_ + * b8bcc0d Add note to various network_hosts docs about long_run for IPv6 networks + + * 1186274 Use long_range function for IPv6Network hosts() function + +- **PR** `#42210`_: (*terminalmage*) Only pass a saltenv in orchestration if one was explicitly passed (2017.7) + @ *2017-07-17T18:22:39Z* + + * e7b79e0 Merge pull request `#42210`_ from terminalmage/issue40928-2017.7 + * 771ade5 Only pass a saltenv in orchestration if one was explicitly passed (2017.7) + +- **PR** `#42236`_: (*mirceaulinic*) New option for napalm proxy/minion: provider + @ *2017-07-17T18:19:56Z* + + * 0e49021 Merge pull request `#42236`_ from cloudflare/napalm-provider + * 1ac69bd Document the provider option and rearrange the doc + + * 4bf4b14 New option for napalm proxy/minion: provider + +- **PR** `#42257`_: (*twangboy*) Fix `unit.pillar.test_git` for Windows + @ *2017-07-17T17:51:42Z* + + * 3ec5bb1 Merge pull request `#42257`_ from twangboy/win_unit_pillar_test_git + * 45be326 Add error-handling function to shutil.rmtree + +- **PR** `#42258`_: (*twangboy*) Fix `unit.states.test_environ` for Windows + @ *2017-07-17T17:50:38Z* + + * 3639562 Merge pull request `#42258`_ from twangboy/win_unit_states_tests_environ + * 55b278c Mock the reg.read_value function + +- **PR** `#42265`_: (*rallytime*) Gate boto_elb tests if proper version of moto isn't installed + @ *2017-07-17T17:47:52Z* + + * 894bdd2 Merge pull request `#42265`_ from rallytime/gate-moto-version + * 78cdee5 Gate boto_elb tests if proper version of moto isn't installed + +- **PR** `#42277`_: (*twangboy*) Fix `unit.states.test_winrepo` for Windows + @ *2017-07-17T17:37:07Z* + + * baf04f2 Merge pull request `#42277`_ from twangboy/win_unit_states_test_winrepo + * ed89cd0 Use os.sep for path seps + +- **PR** `#42309`_: (*terminalmage*) Change "TBD" in versionadded to "2017.7.0" + @ *2017-07-17T17:11:45Z* + + * be6b211 Merge pull request `#42309`_ from terminalmage/fix-versionadded + * 603f5b7 Change "TBD" in versionadded to "2017.7.0" + +- **PR** `#42206`_: (*rallytime*) [PY3] Fix test that is flaky in Python 3 + | refs: `#42783`_ + @ *2017-07-17T17:09:53Z* + + * acd29f9 Merge pull request `#42206`_ from rallytime/fix-flaky-test + * 2be4865 [PY3] Fix test that is flaky in Python 3 + +- **PR** `#42126`_: (*rallytime*) [2017.7] Merge forward from 2016.11 to 2017.7 + @ *2017-07-17T17:07:19Z* + + * 8f1cb28 Merge pull request `#42126`_ from rallytime/merge-2017.7 +* 8b35b36 Merge branch '2016.11' into '2017.7' + + +- **PR** `#42078`_: (*damon-atkins*) pkg.install and pkg.remove fix version number input. + @ *2017-07-05T06:04:57Z* + + * 4780d78 Merge pull request `#42078`_ from damon-atkins/fix_convert_flt_str_version_on_cmd_line + * 09d37dd Fix comment typo + + * 7167549 Handle version=None when converted to a string it becomes 'None' parm should default to empty string rather than None, it would fix better with existing code. + + * 4fb2bb1 Fix typo + + * cf55c33 pkg.install and pkg.remove on the command line take number version numbers, store them within a float. However version is a string, to support versions numbers like 1.3.4 + +- **PR** `#42105`_: (*Ch3LL*) Update releasecanddiate doc with new 2017.7.0rc1 Release + @ *2017-07-04T03:14:42Z* + + * 46d575a Merge pull request `#42105`_ from Ch3LL/update_rc + * d4e7b91 Update releasecanddiate doc with new 2017.7.0rc1 Release + +- **PR** `#42099`_: (*rallytime*) Remove references in docs to pip install salt-cloud + @ *2017-07-03T22:13:44Z* + + - **ISSUE** `#41885`_: (*astronouth7303*) Recommended pip installation outdated? + | refs: `#42099`_ + * d38548b Merge pull request `#42099`_ from rallytime/`fix-41885`_ + * c2822e0 Remove references in docs to pip install salt-cloud + +- **PR** `#42086`_: (*abulford*) Make result=true if Docker volume already exists + | refs: `#42087`_ + @ *2017-07-03T15:48:33Z* + + - **ISSUE** `#42076`_: (*abulford*) dockerng.volume_present test looks as though it would cause a change + | refs: `#42086`_ `#42086`_ `#42087`_ `#42087`_ + * 81d606a Merge pull request `#42086`_ from redmatter/fix-dockerng-volume-present-result + * 8d54968 Make result=true if Docker volume already exists + +- **PR** `#42021`_: (*gtmanfred*) Set concurrent to True when running states with sudo + @ *2017-06-30T21:02:15Z* + + - **ISSUE** `#25842`_: (*shikhartanwar*) Running salt-minion as non-root user to execute sudo commands always returns an error + | refs: `#42021`_ + * 7160697 Merge pull request `#42021`_ from gtmanfred/2016.11 + * 26beb18 Set concurrent to True when running states with sudo + +- **PR** `#42029`_: (*terminalmage*) Mock socket.getaddrinfo in unit.utils.network_test.NetworkTestCase.test_host_to_ips + @ *2017-06-30T20:58:56Z* + + * b784fbb Merge pull request `#42029`_ from terminalmage/host_to_ips + * 26f848e Mock socket.getaddrinfo in unit.utils.network_test.NetworkTestCase.test_host_to_ips + +- **PR** `#42055`_: (*dmurphy18*) Upgrade support for gnupg v2.1 and higher + @ *2017-06-30T20:54:02Z* + + * e067020 Merge pull request `#42055`_ from dmurphy18/handle_gnupgv21 + * e20cea6 Upgrade support for gnupg v2.1 and higher + +- **PR** `#42048`_: (*Ch3LL*) Add initial 2016.11.7 Release Notes + @ *2017-06-30T16:00:05Z* + + * 74ba2ab Merge pull request `#42048`_ from Ch3LL/add_11.7 + * 1de5e00 Add initial 2016.11.7 Release Notes + + +.. _`#1`: https://github.com/saltstack/salt/issues/1 +.. _`#1036125`: https://github.com/saltstack/salt/issues/1036125 +.. _`#12587`: https://github.com/saltstack/salt/issues/12587 +.. _`#15171`: https://github.com/saltstack/salt/issues/15171 +.. _`#2`: https://github.com/saltstack/salt/issues/2 +.. _`#23516`: https://github.com/saltstack/salt/issues/23516 +.. _`#25842`: https://github.com/saltstack/salt/issues/25842 +.. _`#26995`: https://github.com/saltstack/salt/issues/26995 +.. _`#32400`: https://github.com/saltstack/salt/issues/32400 +.. _`#33708`: https://github.com/saltstack/salt/issues/33708 +.. _`#33806`: https://github.com/saltstack/salt/pull/33806 +.. _`#34245`: https://github.com/saltstack/salt/issues/34245 +.. _`#34280`: https://github.com/saltstack/salt/pull/34280 +.. _`#37312`: https://github.com/saltstack/salt/issues/37312 +.. _`#37424`: https://github.com/saltstack/salt/pull/37424 +.. _`#38063`: https://github.com/saltstack/salt/pull/38063 +.. _`#38839`: https://github.com/saltstack/salt/issues/38839 +.. _`#38878`: https://github.com/saltstack/salt/issues/38878 +.. _`#38898`: https://github.com/saltstack/salt/pull/38898 +.. _`#38965`: https://github.com/saltstack/salt/pull/38965 +.. _`#39365`: https://github.com/saltstack/salt/issues/39365 +.. _`#39366`: https://github.com/saltstack/salt/pull/39366 +.. _`#39516`: https://github.com/saltstack/salt/pull/39516 +.. _`#396`: https://github.com/saltstack/salt/pull/396 +.. _`#39646`: https://github.com/saltstack/salt/pull/39646 +.. _`#39773`: https://github.com/saltstack/salt/pull/39773 +.. _`#40354`: https://github.com/saltstack/salt/issues/40354 +.. _`#40490`: https://github.com/saltstack/salt/issues/40490 +.. _`#41116`: https://github.com/saltstack/salt/issues/41116 +.. _`#41433`: https://github.com/saltstack/salt/issues/41433 +.. _`#41543`: https://github.com/saltstack/salt/pull/41543 +.. _`#41690`: https://github.com/saltstack/salt/pull/41690 +.. _`#41720`: https://github.com/saltstack/salt/issues/41720 +.. _`#41721`: https://github.com/saltstack/salt/issues/41721 +.. _`#41770`: https://github.com/saltstack/salt/issues/41770 +.. _`#41885`: https://github.com/saltstack/salt/issues/41885 +.. _`#41936`: https://github.com/saltstack/salt/issues/41936 +.. _`#41945`: https://github.com/saltstack/salt/pull/41945 +.. _`#41949`: https://github.com/saltstack/salt/issues/41949 +.. _`#41955`: https://github.com/saltstack/salt/issues/41955 +.. _`#41968`: https://github.com/saltstack/salt/pull/41968 +.. _`#41973`: https://github.com/saltstack/salt/pull/41973 +.. _`#41976`: https://github.com/saltstack/salt/issues/41976 +.. _`#41977`: https://github.com/saltstack/salt/pull/41977 +.. _`#41982`: https://github.com/saltstack/salt/issues/41982 +.. _`#41988`: https://github.com/saltstack/salt/pull/41988 +.. _`#41994`: https://github.com/saltstack/salt/pull/41994 +.. _`#42006`: https://github.com/saltstack/salt/pull/42006 +.. _`#42021`: https://github.com/saltstack/salt/pull/42021 +.. _`#42027`: https://github.com/saltstack/salt/pull/42027 +.. _`#42029`: https://github.com/saltstack/salt/pull/42029 +.. _`#42031`: https://github.com/saltstack/salt/pull/42031 +.. _`#42041`: https://github.com/saltstack/salt/issues/42041 +.. _`#42045`: https://github.com/saltstack/salt/pull/42045 +.. _`#42048`: https://github.com/saltstack/salt/pull/42048 +.. _`#42055`: https://github.com/saltstack/salt/pull/42055 +.. _`#42067`: https://github.com/saltstack/salt/pull/42067 +.. _`#42076`: https://github.com/saltstack/salt/issues/42076 +.. _`#42077`: https://github.com/saltstack/salt/pull/42077 +.. _`#42078`: https://github.com/saltstack/salt/pull/42078 +.. _`#42086`: https://github.com/saltstack/salt/pull/42086 +.. _`#42087`: https://github.com/saltstack/salt/pull/42087 +.. _`#42094`: https://github.com/saltstack/salt/pull/42094 +.. _`#42095`: https://github.com/saltstack/salt/pull/42095 +.. _`#42097`: https://github.com/saltstack/salt/pull/42097 +.. _`#42098`: https://github.com/saltstack/salt/pull/42098 +.. _`#42099`: https://github.com/saltstack/salt/pull/42099 +.. _`#42105`: https://github.com/saltstack/salt/pull/42105 +.. _`#42107`: https://github.com/saltstack/salt/pull/42107 +.. _`#42109`: https://github.com/saltstack/salt/pull/42109 +.. _`#42114`: https://github.com/saltstack/salt/issues/42114 +.. _`#42115`: https://github.com/saltstack/salt/issues/42115 +.. _`#42116`: https://github.com/saltstack/salt/issues/42116 +.. _`#42119`: https://github.com/saltstack/salt/pull/42119 +.. _`#42121`: https://github.com/saltstack/salt/pull/42121 +.. _`#42123`: https://github.com/saltstack/salt/pull/42123 +.. _`#42126`: https://github.com/saltstack/salt/pull/42126 +.. _`#42137`: https://github.com/saltstack/salt/issues/42137 +.. _`#42140`: https://github.com/saltstack/salt/pull/42140 +.. _`#42141`: https://github.com/saltstack/salt/pull/42141 +.. _`#42142`: https://github.com/saltstack/salt/pull/42142 +.. _`#42150`: https://github.com/saltstack/salt/pull/42150 +.. _`#42151`: https://github.com/saltstack/salt/issues/42151 +.. _`#42152`: https://github.com/saltstack/salt/issues/42152 +.. _`#42154`: https://github.com/saltstack/salt/pull/42154 +.. _`#42155`: https://github.com/saltstack/salt/pull/42155 +.. _`#42163`: https://github.com/saltstack/salt/pull/42163 +.. _`#42164`: https://github.com/saltstack/salt/pull/42164 +.. _`#42166`: https://github.com/saltstack/salt/issues/42166 +.. _`#42172`: https://github.com/saltstack/salt/pull/42172 +.. _`#42173`: https://github.com/saltstack/salt/pull/42173 +.. _`#42174`: https://github.com/saltstack/salt/pull/42174 +.. _`#42175`: https://github.com/saltstack/salt/pull/42175 +.. _`#42176`: https://github.com/saltstack/salt/pull/42176 +.. _`#42179`: https://github.com/saltstack/salt/pull/42179 +.. _`#42180`: https://github.com/saltstack/salt/pull/42180 +.. _`#42181`: https://github.com/saltstack/salt/pull/42181 +.. _`#42182`: https://github.com/saltstack/salt/pull/42182 +.. _`#42186`: https://github.com/saltstack/salt/pull/42186 +.. _`#42194`: https://github.com/saltstack/salt/issues/42194 +.. _`#42198`: https://github.com/saltstack/salt/issues/42198 +.. _`#42200`: https://github.com/saltstack/salt/pull/42200 +.. _`#42206`: https://github.com/saltstack/salt/pull/42206 +.. _`#42210`: https://github.com/saltstack/salt/pull/42210 +.. _`#42211`: https://github.com/saltstack/salt/pull/42211 +.. _`#42215`: https://github.com/saltstack/salt/pull/42215 +.. _`#42224`: https://github.com/saltstack/salt/pull/42224 +.. _`#42232`: https://github.com/saltstack/salt/issues/42232 +.. _`#42235`: https://github.com/saltstack/salt/pull/42235 +.. _`#42236`: https://github.com/saltstack/salt/pull/42236 +.. _`#42240`: https://github.com/saltstack/salt/issues/42240 +.. _`#42251`: https://github.com/saltstack/salt/pull/42251 +.. _`#42252`: https://github.com/saltstack/salt/pull/42252 +.. _`#42253`: https://github.com/saltstack/salt/pull/42253 +.. _`#42255`: https://github.com/saltstack/salt/pull/42255 +.. _`#42257`: https://github.com/saltstack/salt/pull/42257 +.. _`#42258`: https://github.com/saltstack/salt/pull/42258 +.. _`#42261`: https://github.com/saltstack/salt/pull/42261 +.. _`#42262`: https://github.com/saltstack/salt/pull/42262 +.. _`#42264`: https://github.com/saltstack/salt/pull/42264 +.. _`#42265`: https://github.com/saltstack/salt/pull/42265 +.. _`#42266`: https://github.com/saltstack/salt/pull/42266 +.. _`#42267`: https://github.com/saltstack/salt/issues/42267 +.. _`#42269`: https://github.com/saltstack/salt/pull/42269 +.. _`#42270`: https://github.com/saltstack/salt/issues/42270 +.. _`#42275`: https://github.com/saltstack/salt/pull/42275 +.. _`#42277`: https://github.com/saltstack/salt/pull/42277 +.. _`#42279`: https://github.com/saltstack/salt/issues/42279 +.. _`#42282`: https://github.com/saltstack/salt/pull/42282 +.. _`#42289`: https://github.com/saltstack/salt/pull/42289 +.. _`#42290`: https://github.com/saltstack/salt/pull/42290 +.. _`#42291`: https://github.com/saltstack/salt/pull/42291 +.. _`#42295`: https://github.com/saltstack/salt/issues/42295 +.. _`#42308`: https://github.com/saltstack/salt/pull/42308 +.. _`#42309`: https://github.com/saltstack/salt/pull/42309 +.. _`#42314`: https://github.com/saltstack/salt/pull/42314 +.. _`#42319`: https://github.com/saltstack/salt/pull/42319 +.. _`#42327`: https://github.com/saltstack/salt/pull/42327 +.. _`#42329`: https://github.com/saltstack/salt/issues/42329 +.. _`#42333`: https://github.com/saltstack/salt/issues/42333 +.. _`#42339`: https://github.com/saltstack/salt/pull/42339 +.. _`#42340`: https://github.com/saltstack/salt/pull/42340 +.. _`#42347`: https://github.com/saltstack/salt/pull/42347 +.. _`#42350`: https://github.com/saltstack/salt/pull/42350 +.. _`#42352`: https://github.com/saltstack/salt/pull/42352 +.. _`#42353`: https://github.com/saltstack/salt/pull/42353 +.. _`#42356`: https://github.com/saltstack/salt/pull/42356 +.. _`#42357`: https://github.com/saltstack/salt/issues/42357 +.. _`#42359`: https://github.com/saltstack/salt/pull/42359 +.. _`#42360`: https://github.com/saltstack/salt/pull/42360 +.. _`#42361`: https://github.com/saltstack/salt/pull/42361 +.. _`#42363`: https://github.com/saltstack/salt/pull/42363 +.. _`#42364`: https://github.com/saltstack/salt/pull/42364 +.. _`#42366`: https://github.com/saltstack/salt/pull/42366 +.. _`#42368`: https://github.com/saltstack/salt/pull/42368 +.. _`#42370`: https://github.com/saltstack/salt/pull/42370 +.. _`#42371`: https://github.com/saltstack/salt/issues/42371 +.. _`#42373`: https://github.com/saltstack/salt/pull/42373 +.. _`#42374`: https://github.com/saltstack/salt/issues/42374 +.. _`#42375`: https://github.com/saltstack/salt/issues/42375 +.. _`#42381`: https://github.com/saltstack/salt/issues/42381 +.. _`#42387`: https://github.com/saltstack/salt/pull/42387 +.. _`#42388`: https://github.com/saltstack/salt/pull/42388 +.. _`#42399`: https://github.com/saltstack/salt/pull/42399 +.. _`#42400`: https://github.com/saltstack/salt/issues/42400 +.. _`#42403`: https://github.com/saltstack/salt/issues/42403 +.. _`#42404`: https://github.com/saltstack/salt/issues/42404 +.. _`#42405`: https://github.com/saltstack/salt/issues/42405 +.. _`#42408`: https://github.com/saltstack/salt/pull/42408 +.. _`#42409`: https://github.com/saltstack/salt/pull/42409 +.. _`#42411`: https://github.com/saltstack/salt/pull/42411 +.. _`#42413`: https://github.com/saltstack/salt/issues/42413 +.. _`#42414`: https://github.com/saltstack/salt/pull/42414 +.. _`#42417`: https://github.com/saltstack/salt/issues/42417 +.. _`#42421`: https://github.com/saltstack/salt/issues/42421 +.. _`#42424`: https://github.com/saltstack/salt/pull/42424 +.. _`#42425`: https://github.com/saltstack/salt/pull/42425 +.. _`#42427`: https://github.com/saltstack/salt/issues/42427 +.. _`#42433`: https://github.com/saltstack/salt/pull/42433 +.. _`#42435`: https://github.com/saltstack/salt/pull/42435 +.. _`#42436`: https://github.com/saltstack/salt/pull/42436 +.. _`#42443`: https://github.com/saltstack/salt/pull/42443 +.. _`#42444`: https://github.com/saltstack/salt/pull/42444 +.. _`#42452`: https://github.com/saltstack/salt/pull/42452 +.. _`#42453`: https://github.com/saltstack/salt/pull/42453 +.. _`#42454`: https://github.com/saltstack/salt/pull/42454 +.. _`#42456`: https://github.com/saltstack/salt/issues/42456 +.. _`#42459`: https://github.com/saltstack/salt/issues/42459 +.. _`#42461`: https://github.com/saltstack/salt/pull/42461 +.. _`#42464`: https://github.com/saltstack/salt/pull/42464 +.. _`#42465`: https://github.com/saltstack/salt/pull/42465 +.. _`#42474`: https://github.com/saltstack/salt/pull/42474 +.. _`#42477`: https://github.com/saltstack/salt/issues/42477 +.. _`#42479`: https://github.com/saltstack/salt/pull/42479 +.. _`#42481`: https://github.com/saltstack/salt/pull/42481 +.. _`#42484`: https://github.com/saltstack/salt/pull/42484 +.. _`#42502`: https://github.com/saltstack/salt/pull/42502 +.. _`#42505`: https://github.com/saltstack/salt/issues/42505 +.. _`#42506`: https://github.com/saltstack/salt/pull/42506 +.. _`#42509`: https://github.com/saltstack/salt/pull/42509 +.. _`#42514`: https://github.com/saltstack/salt/issues/42514 +.. _`#42515`: https://github.com/saltstack/salt/pull/42515 +.. _`#42516`: https://github.com/saltstack/salt/pull/42516 +.. _`#42521`: https://github.com/saltstack/salt/issues/42521 +.. _`#42523`: https://github.com/saltstack/salt/pull/42523 +.. _`#42524`: https://github.com/saltstack/salt/pull/42524 +.. _`#42527`: https://github.com/saltstack/salt/pull/42527 +.. _`#42528`: https://github.com/saltstack/salt/pull/42528 +.. _`#42529`: https://github.com/saltstack/salt/pull/42529 +.. _`#42534`: https://github.com/saltstack/salt/pull/42534 +.. _`#42538`: https://github.com/saltstack/salt/issues/42538 +.. _`#42541`: https://github.com/saltstack/salt/pull/42541 +.. _`#42545`: https://github.com/saltstack/salt/issues/42545 +.. _`#42547`: https://github.com/saltstack/salt/pull/42547 +.. _`#42551`: https://github.com/saltstack/salt/pull/42551 +.. _`#42552`: https://github.com/saltstack/salt/pull/42552 +.. _`#42555`: https://github.com/saltstack/salt/pull/42555 +.. _`#42557`: https://github.com/saltstack/salt/pull/42557 +.. _`#42567`: https://github.com/saltstack/salt/pull/42567 +.. _`#42571`: https://github.com/saltstack/salt/pull/42571 +.. _`#42573`: https://github.com/saltstack/salt/pull/42573 +.. _`#42574`: https://github.com/saltstack/salt/pull/42574 +.. _`#42575`: https://github.com/saltstack/salt/pull/42575 +.. _`#42577`: https://github.com/saltstack/salt/pull/42577 +.. _`#42586`: https://github.com/saltstack/salt/pull/42586 +.. _`#42588`: https://github.com/saltstack/salt/issues/42588 +.. _`#42589`: https://github.com/saltstack/salt/pull/42589 +.. _`#42600`: https://github.com/saltstack/salt/issues/42600 +.. _`#42601`: https://github.com/saltstack/salt/pull/42601 +.. _`#42602`: https://github.com/saltstack/salt/pull/42602 +.. _`#42603`: https://github.com/saltstack/salt/pull/42603 +.. _`#42611`: https://github.com/saltstack/salt/issues/42611 +.. _`#42612`: https://github.com/saltstack/salt/pull/42612 +.. _`#42616`: https://github.com/saltstack/salt/pull/42616 +.. _`#42618`: https://github.com/saltstack/salt/pull/42618 +.. _`#42619`: https://github.com/saltstack/salt/pull/42619 +.. _`#42621`: https://github.com/saltstack/salt/pull/42621 +.. _`#42623`: https://github.com/saltstack/salt/pull/42623 +.. _`#42625`: https://github.com/saltstack/salt/pull/42625 +.. _`#42627`: https://github.com/saltstack/salt/issues/42627 +.. _`#42629`: https://github.com/saltstack/salt/pull/42629 +.. _`#42639`: https://github.com/saltstack/salt/issues/42639 +.. _`#42642`: https://github.com/saltstack/salt/issues/42642 +.. _`#42644`: https://github.com/saltstack/salt/issues/42644 +.. _`#42646`: https://github.com/saltstack/salt/issues/42646 +.. _`#42649`: https://github.com/saltstack/salt/issues/42649 +.. _`#42651`: https://github.com/saltstack/salt/pull/42651 +.. _`#42654`: https://github.com/saltstack/salt/pull/42654 +.. _`#42655`: https://github.com/saltstack/salt/pull/42655 +.. _`#42657`: https://github.com/saltstack/salt/pull/42657 +.. _`#42663`: https://github.com/saltstack/salt/pull/42663 +.. _`#42668`: https://github.com/saltstack/salt/issues/42668 +.. _`#42669`: https://github.com/saltstack/salt/pull/42669 +.. _`#42670`: https://github.com/saltstack/salt/pull/42670 +.. _`#42678`: https://github.com/saltstack/salt/pull/42678 +.. _`#42679`: https://github.com/saltstack/salt/pull/42679 +.. _`#42683`: https://github.com/saltstack/salt/issues/42683 +.. _`#42686`: https://github.com/saltstack/salt/issues/42686 +.. _`#42688`: https://github.com/saltstack/salt/issues/42688 +.. _`#42689`: https://github.com/saltstack/salt/pull/42689 +.. _`#42690`: https://github.com/saltstack/salt/issues/42690 +.. _`#42693`: https://github.com/saltstack/salt/pull/42693 +.. _`#42694`: https://github.com/saltstack/salt/pull/42694 +.. _`#42697`: https://github.com/saltstack/salt/issues/42697 +.. _`#42704`: https://github.com/saltstack/salt/pull/42704 +.. _`#42705`: https://github.com/saltstack/salt/issues/42705 +.. _`#42707`: https://github.com/saltstack/salt/issues/42707 +.. _`#42708`: https://github.com/saltstack/salt/pull/42708 +.. _`#42709`: https://github.com/saltstack/salt/pull/42709 +.. _`#42710`: https://github.com/saltstack/salt/pull/42710 +.. _`#42712`: https://github.com/saltstack/salt/pull/42712 +.. _`#42714`: https://github.com/saltstack/salt/pull/42714 +.. _`#42721`: https://github.com/saltstack/salt/pull/42721 +.. _`#42731`: https://github.com/saltstack/salt/issues/42731 +.. _`#42741`: https://github.com/saltstack/salt/issues/42741 +.. _`#42743`: https://github.com/saltstack/salt/pull/42743 +.. _`#42744`: https://github.com/saltstack/salt/pull/42744 +.. _`#42745`: https://github.com/saltstack/salt/pull/42745 +.. _`#42747`: https://github.com/saltstack/salt/issues/42747 +.. _`#42748`: https://github.com/saltstack/salt/pull/42748 +.. _`#42753`: https://github.com/saltstack/salt/issues/42753 +.. _`#42760`: https://github.com/saltstack/salt/pull/42760 +.. _`#42764`: https://github.com/saltstack/salt/pull/42764 +.. _`#42768`: https://github.com/saltstack/salt/pull/42768 +.. _`#42769`: https://github.com/saltstack/salt/pull/42769 +.. _`#42770`: https://github.com/saltstack/salt/pull/42770 +.. _`#42774`: https://github.com/saltstack/salt/issues/42774 +.. _`#42778`: https://github.com/saltstack/salt/pull/42778 +.. _`#42782`: https://github.com/saltstack/salt/pull/42782 +.. _`#42783`: https://github.com/saltstack/salt/pull/42783 +.. _`#42784`: https://github.com/saltstack/salt/pull/42784 +.. _`#42786`: https://github.com/saltstack/salt/pull/42786 +.. _`#42788`: https://github.com/saltstack/salt/pull/42788 +.. _`#42794`: https://github.com/saltstack/salt/pull/42794 +.. _`#42795`: https://github.com/saltstack/salt/pull/42795 +.. _`#42798`: https://github.com/saltstack/salt/pull/42798 +.. _`#42800`: https://github.com/saltstack/salt/pull/42800 +.. _`#42803`: https://github.com/saltstack/salt/issues/42803 +.. _`#42804`: https://github.com/saltstack/salt/pull/42804 +.. _`#42805`: https://github.com/saltstack/salt/pull/42805 +.. _`#42806`: https://github.com/saltstack/salt/pull/42806 +.. _`#42807`: https://github.com/saltstack/salt/pull/42807 +.. _`#42808`: https://github.com/saltstack/salt/pull/42808 +.. _`#42810`: https://github.com/saltstack/salt/pull/42810 +.. _`#42812`: https://github.com/saltstack/salt/pull/42812 +.. _`#42818`: https://github.com/saltstack/salt/issues/42818 +.. _`#42826`: https://github.com/saltstack/salt/pull/42826 +.. _`#42829`: https://github.com/saltstack/salt/pull/42829 +.. _`#42835`: https://github.com/saltstack/salt/pull/42835 +.. _`#42836`: https://github.com/saltstack/salt/pull/42836 +.. _`#42838`: https://github.com/saltstack/salt/pull/42838 +.. _`#42841`: https://github.com/saltstack/salt/pull/42841 +.. _`#42842`: https://github.com/saltstack/salt/issues/42842 +.. _`#42843`: https://github.com/saltstack/salt/issues/42843 +.. _`#42845`: https://github.com/saltstack/salt/pull/42845 +.. _`#42848`: https://github.com/saltstack/salt/pull/42848 +.. _`#42851`: https://github.com/saltstack/salt/pull/42851 +.. _`#42855`: https://github.com/saltstack/salt/pull/42855 +.. _`#42856`: https://github.com/saltstack/salt/pull/42856 +.. _`#42857`: https://github.com/saltstack/salt/pull/42857 +.. _`#42859`: https://github.com/saltstack/salt/pull/42859 +.. _`#42860`: https://github.com/saltstack/salt/pull/42860 +.. _`#42861`: https://github.com/saltstack/salt/pull/42861 +.. _`#42864`: https://github.com/saltstack/salt/pull/42864 +.. _`#42866`: https://github.com/saltstack/salt/pull/42866 +.. _`#42868`: https://github.com/saltstack/salt/pull/42868 +.. _`#42869`: https://github.com/saltstack/salt/issues/42869 +.. _`#42870`: https://github.com/saltstack/salt/issues/42870 +.. _`#42871`: https://github.com/saltstack/salt/pull/42871 +.. _`#42873`: https://github.com/saltstack/salt/issues/42873 +.. _`#42877`: https://github.com/saltstack/salt/pull/42877 +.. _`#42881`: https://github.com/saltstack/salt/pull/42881 +.. _`#42882`: https://github.com/saltstack/salt/pull/42882 +.. _`#42883`: https://github.com/saltstack/salt/pull/42883 +.. _`#42884`: https://github.com/saltstack/salt/pull/42884 +.. _`#42885`: https://github.com/saltstack/salt/pull/42885 +.. _`#42886`: https://github.com/saltstack/salt/pull/42886 +.. _`#42887`: https://github.com/saltstack/salt/pull/42887 +.. _`#42889`: https://github.com/saltstack/salt/pull/42889 +.. _`#42890`: https://github.com/saltstack/salt/pull/42890 +.. _`#42898`: https://github.com/saltstack/salt/pull/42898 +.. _`#42911`: https://github.com/saltstack/salt/pull/42911 +.. _`#42913`: https://github.com/saltstack/salt/pull/42913 +.. _`#42918`: https://github.com/saltstack/salt/pull/42918 +.. _`#42919`: https://github.com/saltstack/salt/pull/42919 +.. _`#42920`: https://github.com/saltstack/salt/pull/42920 +.. _`#42925`: https://github.com/saltstack/salt/pull/42925 +.. _`#42933`: https://github.com/saltstack/salt/pull/42933 +.. _`#42936`: https://github.com/saltstack/salt/issues/42936 +.. _`#42940`: https://github.com/saltstack/salt/pull/42940 +.. _`#42941`: https://github.com/saltstack/salt/issues/42941 +.. _`#42942`: https://github.com/saltstack/salt/pull/42942 +.. _`#42943`: https://github.com/saltstack/salt/issues/42943 +.. _`#42944`: https://github.com/saltstack/salt/pull/42944 +.. _`#42945`: https://github.com/saltstack/salt/pull/42945 +.. _`#42946`: https://github.com/saltstack/salt/pull/42946 +.. _`#42948`: https://github.com/saltstack/salt/pull/42948 +.. _`#42949`: https://github.com/saltstack/salt/pull/42949 +.. _`#42950`: https://github.com/saltstack/salt/pull/42950 +.. _`#42951`: https://github.com/saltstack/salt/pull/42951 +.. _`#42952`: https://github.com/saltstack/salt/pull/42952 +.. _`#42953`: https://github.com/saltstack/salt/pull/42953 +.. _`#42954`: https://github.com/saltstack/salt/pull/42954 +.. _`#42958`: https://github.com/saltstack/salt/pull/42958 +.. _`#42959`: https://github.com/saltstack/salt/pull/42959 +.. _`#42962`: https://github.com/saltstack/salt/pull/42962 +.. _`#42963`: https://github.com/saltstack/salt/pull/42963 +.. _`#42964`: https://github.com/saltstack/salt/pull/42964 +.. _`#42967`: https://github.com/saltstack/salt/pull/42967 +.. _`#42968`: https://github.com/saltstack/salt/pull/42968 +.. _`#42969`: https://github.com/saltstack/salt/pull/42969 +.. _`#42985`: https://github.com/saltstack/salt/pull/42985 +.. _`#42986`: https://github.com/saltstack/salt/pull/42986 +.. _`#42988`: https://github.com/saltstack/salt/pull/42988 +.. _`#42989`: https://github.com/saltstack/salt/issues/42989 +.. _`#42992`: https://github.com/saltstack/salt/issues/42992 +.. _`#42993`: https://github.com/saltstack/salt/pull/42993 +.. _`#42995`: https://github.com/saltstack/salt/pull/42995 +.. _`#42996`: https://github.com/saltstack/salt/pull/42996 +.. _`#42997`: https://github.com/saltstack/salt/pull/42997 +.. _`#42999`: https://github.com/saltstack/salt/pull/42999 +.. _`#43002`: https://github.com/saltstack/salt/pull/43002 +.. _`#43006`: https://github.com/saltstack/salt/pull/43006 +.. _`#43008`: https://github.com/saltstack/salt/issues/43008 +.. _`#43009`: https://github.com/saltstack/salt/pull/43009 +.. _`#43010`: https://github.com/saltstack/salt/pull/43010 +.. _`#43014`: https://github.com/saltstack/salt/pull/43014 +.. _`#43016`: https://github.com/saltstack/salt/pull/43016 +.. _`#43019`: https://github.com/saltstack/salt/pull/43019 +.. _`#43020`: https://github.com/saltstack/salt/pull/43020 +.. _`#43021`: https://github.com/saltstack/salt/pull/43021 +.. _`#43023`: https://github.com/saltstack/salt/pull/43023 +.. _`#43024`: https://github.com/saltstack/salt/pull/43024 +.. _`#43026`: https://github.com/saltstack/salt/pull/43026 +.. _`#43027`: https://github.com/saltstack/salt/pull/43027 +.. _`#43029`: https://github.com/saltstack/salt/pull/43029 +.. _`#43030`: https://github.com/saltstack/salt/pull/43030 +.. _`#43031`: https://github.com/saltstack/salt/pull/43031 +.. _`#43032`: https://github.com/saltstack/salt/pull/43032 +.. _`#43033`: https://github.com/saltstack/salt/pull/43033 +.. _`#43034`: https://github.com/saltstack/salt/pull/43034 +.. _`#43035`: https://github.com/saltstack/salt/pull/43035 +.. _`#43036`: https://github.com/saltstack/salt/issues/43036 +.. _`#43037`: https://github.com/saltstack/salt/pull/43037 +.. _`#43038`: https://github.com/saltstack/salt/pull/43038 +.. _`#43039`: https://github.com/saltstack/salt/pull/43039 +.. _`#43040`: https://github.com/saltstack/salt/issues/43040 +.. _`#43041`: https://github.com/saltstack/salt/pull/43041 +.. _`#43043`: https://github.com/saltstack/salt/issues/43043 +.. _`#43048`: https://github.com/saltstack/salt/pull/43048 +.. _`#43051`: https://github.com/saltstack/salt/pull/43051 +.. _`#43054`: https://github.com/saltstack/salt/pull/43054 +.. _`#43056`: https://github.com/saltstack/salt/pull/43056 +.. _`#43058`: https://github.com/saltstack/salt/pull/43058 +.. _`#43060`: https://github.com/saltstack/salt/pull/43060 +.. _`#43061`: https://github.com/saltstack/salt/pull/43061 +.. _`#43064`: https://github.com/saltstack/salt/pull/43064 +.. _`#43068`: https://github.com/saltstack/salt/pull/43068 +.. _`#43073`: https://github.com/saltstack/salt/pull/43073 +.. _`#43077`: https://github.com/saltstack/salt/issues/43077 +.. _`#43085`: https://github.com/saltstack/salt/issues/43085 +.. _`#43087`: https://github.com/saltstack/salt/pull/43087 +.. _`#43088`: https://github.com/saltstack/salt/pull/43088 +.. _`#43091`: https://github.com/saltstack/salt/pull/43091 +.. _`#43092`: https://github.com/saltstack/salt/pull/43092 +.. _`#43093`: https://github.com/saltstack/salt/pull/43093 +.. _`#43097`: https://github.com/saltstack/salt/pull/43097 +.. _`#43100`: https://github.com/saltstack/salt/pull/43100 +.. _`#43101`: https://github.com/saltstack/salt/issues/43101 +.. _`#43103`: https://github.com/saltstack/salt/pull/43103 +.. _`#43107`: https://github.com/saltstack/salt/pull/43107 +.. _`#43108`: https://github.com/saltstack/salt/pull/43108 +.. _`#43110`: https://github.com/saltstack/salt/issues/43110 +.. _`#43115`: https://github.com/saltstack/salt/pull/43115 +.. _`#43116`: https://github.com/saltstack/salt/pull/43116 +.. _`#43123`: https://github.com/saltstack/salt/pull/43123 +.. _`#43142`: https://github.com/saltstack/salt/pull/43142 +.. _`#43143`: https://github.com/saltstack/salt/issues/43143 +.. _`#43146`: https://github.com/saltstack/salt/pull/43146 +.. _`#43151`: https://github.com/saltstack/salt/pull/43151 +.. _`#43155`: https://github.com/saltstack/salt/pull/43155 +.. _`#43156`: https://github.com/saltstack/salt/pull/43156 +.. _`#43162`: https://github.com/saltstack/salt/issues/43162 +.. _`#43165`: https://github.com/saltstack/salt/pull/43165 +.. _`#43166`: https://github.com/saltstack/salt/pull/43166 +.. _`#43168`: https://github.com/saltstack/salt/pull/43168 +.. _`#43170`: https://github.com/saltstack/salt/pull/43170 +.. _`#43171`: https://github.com/saltstack/salt/pull/43171 +.. _`#43172`: https://github.com/saltstack/salt/pull/43172 +.. _`#43173`: https://github.com/saltstack/salt/pull/43173 +.. _`#43178`: https://github.com/saltstack/salt/pull/43178 +.. _`#43179`: https://github.com/saltstack/salt/pull/43179 +.. _`#43184`: https://github.com/saltstack/salt/pull/43184 +.. _`#43193`: https://github.com/saltstack/salt/pull/43193 +.. _`#43196`: https://github.com/saltstack/salt/pull/43196 +.. _`#43198`: https://github.com/saltstack/salt/issues/43198 +.. _`#43199`: https://github.com/saltstack/salt/pull/43199 +.. _`#43201`: https://github.com/saltstack/salt/pull/43201 +.. _`#43202`: https://github.com/saltstack/salt/pull/43202 +.. _`#43217`: https://github.com/saltstack/salt/pull/43217 +.. _`#43226`: https://github.com/saltstack/salt/pull/43226 +.. _`#43227`: https://github.com/saltstack/salt/pull/43227 +.. _`#43228`: https://github.com/saltstack/salt/pull/43228 +.. _`#43229`: https://github.com/saltstack/salt/pull/43229 +.. _`#43241`: https://github.com/saltstack/salt/issues/43241 +.. _`#43251`: https://github.com/saltstack/salt/pull/43251 +.. _`#43254`: https://github.com/saltstack/salt/pull/43254 +.. _`#43255`: https://github.com/saltstack/salt/pull/43255 +.. _`#43256`: https://github.com/saltstack/salt/pull/43256 +.. _`#43259`: https://github.com/saltstack/salt/issues/43259 +.. _`#43266`: https://github.com/saltstack/salt/pull/43266 +.. _`#43283`: https://github.com/saltstack/salt/pull/43283 +.. _`#43315`: https://github.com/saltstack/salt/pull/43315 +.. _`#43330`: https://github.com/saltstack/salt/pull/43330 +.. _`#43333`: https://github.com/saltstack/salt/pull/43333 +.. _`#43377`: https://github.com/saltstack/salt/pull/43377 +.. _`#43421`: https://github.com/saltstack/salt/pull/43421 +.. _`#43440`: https://github.com/saltstack/salt/pull/43440 +.. _`#43447`: https://github.com/saltstack/salt/issues/43447 +.. _`#43509`: https://github.com/saltstack/salt/pull/43509 +.. _`#43526`: https://github.com/saltstack/salt/pull/43526 +.. _`#43551`: https://github.com/saltstack/salt/pull/43551 +.. _`#43585`: https://github.com/saltstack/salt/pull/43585 +.. _`#43586`: https://github.com/saltstack/salt/pull/43586 +.. _`#43756`: https://github.com/saltstack/salt/pull/43756 +.. _`#43847`: https://github.com/saltstack/salt/pull/43847 +.. _`#43868`: https://github.com/saltstack/salt/pull/43868 +.. _`#475`: https://github.com/saltstack/salt/issues/475 +.. _`#480`: https://github.com/saltstack/salt/issues/480 +.. _`#495`: https://github.com/saltstack/salt/issues/495 +.. _`bp-37424`: https://github.com/saltstack/salt/pull/37424 +.. _`bp-39366`: https://github.com/saltstack/salt/pull/39366 +.. _`bp-41543`: https://github.com/saltstack/salt/pull/41543 +.. _`bp-41690`: https://github.com/saltstack/salt/pull/41690 +.. _`bp-42067`: https://github.com/saltstack/salt/pull/42067 +.. _`bp-42097`: https://github.com/saltstack/salt/pull/42097 +.. _`bp-42098`: https://github.com/saltstack/salt/pull/42098 +.. _`bp-42109`: https://github.com/saltstack/salt/pull/42109 +.. _`bp-42174`: https://github.com/saltstack/salt/pull/42174 +.. _`bp-42224`: https://github.com/saltstack/salt/pull/42224 +.. _`bp-42433`: https://github.com/saltstack/salt/pull/42433 +.. _`bp-42547`: https://github.com/saltstack/salt/pull/42547 +.. _`bp-42552`: https://github.com/saltstack/salt/pull/42552 +.. _`bp-42589`: https://github.com/saltstack/salt/pull/42589 +.. _`bp-42651`: https://github.com/saltstack/salt/pull/42651 +.. _`bp-42744`: https://github.com/saltstack/salt/pull/42744 +.. _`bp-42760`: https://github.com/saltstack/salt/pull/42760 +.. _`bp-42784`: https://github.com/saltstack/salt/pull/42784 +.. _`bp-42848`: https://github.com/saltstack/salt/pull/42848 +.. _`bp-42871`: https://github.com/saltstack/salt/pull/42871 +.. _`bp-42883`: https://github.com/saltstack/salt/pull/42883 +.. _`bp-42988`: https://github.com/saltstack/salt/pull/42988 +.. _`bp-43002`: https://github.com/saltstack/salt/pull/43002 +.. _`bp-43020`: https://github.com/saltstack/salt/pull/43020 +.. _`bp-43031`: https://github.com/saltstack/salt/pull/43031 +.. _`bp-43041`: https://github.com/saltstack/salt/pull/43041 +.. _`bp-43068`: https://github.com/saltstack/salt/pull/43068 +.. _`bp-43116`: https://github.com/saltstack/salt/pull/43116 +.. _`bp-43193`: https://github.com/saltstack/salt/pull/43193 +.. _`bp-43283`: https://github.com/saltstack/salt/pull/43283 +.. _`bp-43330`: https://github.com/saltstack/salt/pull/43330 +.. _`bp-43333`: https://github.com/saltstack/salt/pull/43333 +.. _`bp-43421`: https://github.com/saltstack/salt/pull/43421 +.. _`bp-43526`: https://github.com/saltstack/salt/pull/43526 +.. _`fix-38839`: https://github.com/saltstack/salt/issues/38839 +.. _`fix-41116`: https://github.com/saltstack/salt/issues/41116 +.. _`fix-41721`: https://github.com/saltstack/salt/issues/41721 +.. _`fix-41885`: https://github.com/saltstack/salt/issues/41885 +.. _`fix-42115`: https://github.com/saltstack/salt/issues/42115 +.. _`fix-42151`: https://github.com/saltstack/salt/issues/42151 +.. _`fix-42152`: https://github.com/saltstack/salt/issues/42152 +.. _`fix-42166`: https://github.com/saltstack/salt/issues/42166 +.. _`fix-42267`: https://github.com/saltstack/salt/issues/42267 +.. _`fix-42375`: https://github.com/saltstack/salt/issues/42375 +.. _`fix-42381`: https://github.com/saltstack/salt/issues/42381 +.. _`fix-42405`: https://github.com/saltstack/salt/issues/42405 +.. _`fix-42417`: https://github.com/saltstack/salt/issues/42417 +.. _`fix-42639`: https://github.com/saltstack/salt/issues/42639 +.. _`fix-42683`: https://github.com/saltstack/salt/issues/42683 +.. _`fix-42697`: https://github.com/saltstack/salt/issues/42697 +.. _`fix-42870`: https://github.com/saltstack/salt/issues/42870 From 1a9f12fd67583fde75d0cdb4be804335b19a82df Mon Sep 17 00:00:00 2001 From: Eric Radman Date: Fri, 29 Sep 2017 12:12:54 -0400 Subject: [PATCH 469/633] Detect OpenBSD guest running under VMM(4) grains.manufacturer is 'OpenBSD' when running under VMM --- doc/topics/releases/oxygen.rst | 6 ++++++ salt/grains/core.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 0e6d63304c..7b27673fda 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -56,6 +56,12 @@ The new grains added are: * ``fc_wwn``: Show all fibre channel world wide port names for a host * ``iscsi_iqn``: Show the iSCSI IQN name for a host +Grains Changes +-------------- + +* The ``virtual`` grain identifies reports KVM and VMM hypervisors when running + an OpenBSD guest + New Modules ----------- diff --git a/salt/grains/core.py b/salt/grains/core.py index 9adf2fd776..5cdcdccdf5 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -848,6 +848,8 @@ def _virtual(osdata): elif osdata['kernel'] == 'OpenBSD': if osdata['manufacturer'] in ['QEMU', 'Red Hat']: grains['virtual'] = 'kvm' + if osdata['manufacturer'] == 'OpenBSD': + grains['virtual'] = 'vmm' elif osdata['kernel'] == 'SunOS': if grains['virtual'] == 'LDOM': roles = [] From f96740d278f3707c06410795851b1faf6b4df225 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Mon, 2 Oct 2017 21:07:27 -0600 Subject: [PATCH 470/633] use recommended source for TMP directory --- tests/unit/utils/test_sdb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/utils/test_sdb.py b/tests/unit/utils/test_sdb.py index 91216d4c44..0550758107 100644 --- a/tests/unit/utils/test_sdb.py +++ b/tests/unit/utils/test_sdb.py @@ -8,7 +8,7 @@ from __future__ import absolute_import import os # Import Salt Testing Libs -from tests.support.paths import TMP +from tests.support.runtests import RUNTIME_VARS from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import TestCase, skipIf from tests.support.mock import ( @@ -19,7 +19,7 @@ from tests.support.mock import ( # Import Salt Libs import salt.utils.sdb as sdb -TEMP_DATABASE_FILE = os.path.join(TMP, 'test_sdb.sqlite') +TEMP_DATABASE_FILE = os.path.join(RUNTIME_VARS.TMP, 'test_sdb.sqlite') SDB_OPTS = { 'extension_modules': '', From 05392bc664d0efcfe79695507d068913c4d5a841 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Mon, 2 Oct 2017 21:15:16 -0600 Subject: [PATCH 471/633] cloud friendly vagrant destroy (cherry picked from commit e82b1dc) --- salt/modules/vagrant.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index dd28f1e653..1a10fcdb80 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -511,15 +511,15 @@ def destroy(name): machine = vm_['machine'] cmd = 'vagrant destroy -f {}'.format(machine) - try: - ret = __salt__['cmd.retcode'](cmd, - runas=vm_.get('runas'), - cwd=vm_.get('cwd')) - except (OSError, CommandExecutionError): - ret = 1 - finally: + + ret = __salt__['cmd.run_all'](cmd, + runas=vm_.get('runas'), + cwd=vm_.get('cwd'), + output_loglevel='info') + if ret['retcode'] == 0: _erase_vm_info(name) - return ret == 0 + return True + return ret def get_ssh_config(name, network_mask='', get_private_key=False): From c6b2e1e35b062bb24a128e0d56bc8fc328d5d35d Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Mon, 2 Oct 2017 21:36:41 -0600 Subject: [PATCH 472/633] update tests to match improved module (cherry picked from commit 7fcdc51) --- tests/unit/modules/test_vagrant.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 8862b56793..aaa69bb078 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -47,6 +47,7 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): '__salt__': { 'cmd.shell': cmd.shell, 'cmd.retcode': cmd.retcode, + 'cmd.run_all': cmd.run_all }, '__utils__': { 'sdb.sdb_set': sdb.sdb_set, @@ -120,7 +121,7 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): # VM has a stored value self.assertEqual(vagrant.get_vm_info('test3')['name'], 'test3') # clean up (an error is expected -- machine never started) - self.assertFalse(vagrant.destroy('test3')) + self.assertFalse(vagrant.destroy('test3')['retcode'] == 0) # VM no longer exists - with self.assertRaises(salt.exceptions.SaltInvocationError): + with self.assertRaises(salt.exceptions.CommandExecutionError): vagrant.get_ssh_config('test3') From d28aa20c74c00625c7bccf04479174bd48c96dff Mon Sep 17 00:00:00 2001 From: Bike Dude Date: Tue, 3 Oct 2017 12:08:34 +0200 Subject: [PATCH 473/633] survey runner bugfix --- salt/runners/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/runners/survey.py b/salt/runners/survey.py index 3fd2b1014f..591840060c 100644 --- a/salt/runners/survey.py +++ b/salt/runners/survey.py @@ -171,7 +171,7 @@ def _get_pool_results(*args, **kwargs): # hash minion return values as a string for minion in sorted(minions): - digest = hashlib.sha256(str(minions[minion])).hexdigest() + digest = hashlib.sha256(str(minions[minion]).encode('utf-8')).hexdigest() if digest not in ret: ret[digest] = {} ret[digest]['pool'] = [] From 73fd7d91cc611d95a7b61677306b36975914a0b9 Mon Sep 17 00:00:00 2001 From: Kunal Ajay Bajpai Date: Tue, 3 Oct 2017 18:48:03 +0530 Subject: [PATCH 474/633] Handle possible KeyError in couchbase.get_load --- salt/returners/couchbase_return.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/salt/returners/couchbase_return.py b/salt/returners/couchbase_return.py index 24c3a9105a..1fdcd4800f 100644 --- a/salt/returners/couchbase_return.py +++ b/salt/returners/couchbase_return.py @@ -264,9 +264,12 @@ def get_load(jid): except couchbase.exceptions.NotFoundError: return {} - ret = jid_doc.value['load'] - if 'minions' in jid_doc.value: + ret = {} + try: + ret = jid_doc.value['load'] ret['Minions'] = jid_doc.value['minions'] + except KeyError as e: + log.error(e) return ret From 8c671fd0c16e3feb8698842c1d107e8f4ac75a37 Mon Sep 17 00:00:00 2001 From: David Boucha Date: Tue, 3 Oct 2017 10:07:28 -0600 Subject: [PATCH 475/633] Update SaltConf banner per Rhett's request --- .../saltstack2/static/images/DOCBANNER.jpg | Bin 809407 -> 448969 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/_themes/saltstack2/static/images/DOCBANNER.jpg b/doc/_themes/saltstack2/static/images/DOCBANNER.jpg index f626a181623061b4659343d4a88b3b84ec4ea1fb..886e3a9b7c62fa0701c16d910a29d2b4ded1f121 100644 GIT binary patch literal 448969 zcmeFa2|SeD`!Iep_BDmd9-?I5ml(SwOLj>bV=%U1#!$qxQd*QkB`Jk0kz`LQMTP7; zA=&qAS?4`7Qa#W2dA@z$|L^@ipZERz-Se6IzRq>7bDis4XT8sTopXpS#2&%>ddJW% z5M*o&i9rxV2hmb&hF}1q06&O=3!)~&5adL`{R4KV*!7);641y%;0_6cKbAt446_3` zIt&d_eGi)so-Gi=@<|91-L(GHa>h8J1kBtpei)n^#z#O$M?h9uMoD0oF#?J9CLwo2 zB!sH8imI%v000!^RORIWD+qRvVl93m5rXbfuE#^&aSyf*)9fG-f{TU%_FYz*U(-V& z@-saYlE0>hl46|)g(Bq--aC+M?4W(%2{g0zB(@O8ASQB39CeH#3gS4#LUuvUCrNa~ zcIYg`L`O$QPsc=0&&0;az{tkI%EZLVv2Dv1jxAfZu`!Xa?~gUdpYIgRjEu}I%$rzP zHnFp?u&|SEEbMDR*!~j+#Fr2o9dr-61)~szDA_1rY!t*!kVppN8wxm(lA3~i0f9*u z45FZHW<~8%`((l2N7EY zeNV~KuwT2Mr7a}PVgAZVP9A%@R)=<6cVSvneg{xYGOyer;Afand9A$t)l{EgFFg+Dpf?`pB za8^=rJ(N2ne_qjEwHm=79Ws898+k^lGJlgnceoK(a7O!x@^;>nTZoY5*~>QP9oPc8 z54~Y~C-nIS2Wnu(HNcf}XM05X!mdLjBHBTAxT@ya3e8qxZmD#Z8kk?k(iaT?~C!f~U3m*t7QP@;#OCnk~@XBV({mxug|m*9*prXbjIJjKj)tKHb-T`3-Kwe;HhDI zK@kx$%5&fEo|~5+deZp z&f+9TYMt@mz*Q}QuUkY3b-S-V_IRt5_zh01>9h(=KA4&0w>13;R?NA2Icz#T;AuD! z%5kkDLT9XFw#<&CEVf3?6jl#T5+OrETs2=45o+2Muu^~zZ|E!C98!L#&nKw$0c-6- z^2)o3RizJK{6b=`HBDzACpZ{Vvz63#kKxbdu9qR$t@@7pvKRuqQt znT0^`?2K2V(gRXZmYzgt|EyGM%e*r__~yr|%$+&vGY<8aoj!Ch zx^!H>?xGHNYV)@*5sV-t6?!L(E?>$>zF?}W5^V6OIehs{)R1gUk6Eef7|*5gxR?mN z<8}q={>G~9UY$IDfL;hxc2Zi zFYYWp-fJ;uhWi-4`buJ2TOAWa^bH=55YF1>Ae`}zX-q>7Tey~U9hxLUB<605buHc< zICylH2pI(J@fcr(yUvu926iPF9vE7@80BrPsMPM^@zj^)s7CCS8eSuH(U9vM6HOuU zU)i%Y5?U|UKP^0?8nvC{bJG$}TZ_rqnd6xaoW+l^S+ntv@Zs1%V`Zj1Mwym!w#4X~ z`9p_?w-05t#Gpo9Q_F(V@uoybMyV;Uwq{XeYvW;A#*2Eb)tCW~qD~?dinSjmyg6vk zp2KkMabcH@T10H`+bvi1?O2}lT)Ek^JLr1MHy$2V!5SCM9+gfu$n>8ByYbu0i_&xpb5`$foM+&C_^|SQJl~Kg zGwzbS;iU2Y+n+nNuDoBd-)nVy(1WE%QbY9H?!pId;zCEtl#S;V?t2s$i`VN$9=N-I zzkl80d6ot~Iy3#Ux!giHv9@v8Zg!@Fae9pV9CP%ORa|xM?UIq`=uGja6%twVhsWmH zKtpsKcPBu-Yd|t~q}gH?n>u6_U;FuO0iXZ#kjwG2;SQ_8BQfTvsM});&osEtoY|Tm z;#o+98i

Pp9AQ3&@U!FI4!MWNfiF|B;tLO=zYrTkgPvggG^&5H_OvWU<}g3{>f z>#T^2^&>-eCDx{vuhK*Xhvm4VD|qMB)TQU(NzSf;UYRq-?J*W}b`c}17jWoZ?Gw2A z!X{dSH?AfRQmkBD_YVahXpAvRIumsbwe>UNXuF5qTugRN*K${~>q>s6A8R_lb=YWn zQm3hL?#JX1$AfQ!O6`mzUX^fGxXo&Wvgm!fK0F_Cr6VGn2<6>|;rat7H7N!~!WPi2=7o zg$DR_V`G$i;l5jLLhaVOKEuSHUdLW#<8L@i(1(9A4sY7JdBj*an3k0zIHrbcJE|(H zs@b>xNQd~7eMY|eZ)M#FRPe0&^RW6_Ts>pl0a{x7_Z{ z9I4tB639&4!;zL-ZAz*}`fV#E23X(lY2=8AqCpW_EZbf3-5-ygR5vgP zEriWHTTL~qg72HiA9}dswVLSpd84RdnkTm1&fXBE3aolzf8q7d5UXUw_WJIGD27z4 zxGrK{cSd2w_|qka!uGo(;sF=$1Smfej@(cAj3u-2$~9BF5rw0lrX9HXy%Ljn`k%t} z%RTb^CziZy7t4u|V%F4GnXEt;*8{_Tg}Y`Cf3DcZIwV2JM7`*Y*Qp1M_=;F!jf`Z) zaE?vHosYMIxJGur&uS0u_ChHPcUBh>YC&-wCPHeaslT}{*ipbKQjFOtwE7b#**Pxd}t6K~-3=M9TDPKC(!`Fb?Wz^qv0$$Y@ zHjy=4yl2?)z_6{l;%b~P!Hk)yuxM0`v16BK(^B!L`4IO|)<(W-mm30`G=ec7A5h-L z$`p^IV)@!fgE2F_7LJ&~V1NZ}MaojUUi0Q$$6Y-$_cH{m>eW-{NBC`zMhtkPSRSu-ekulCTDMGzfm7Fz zt>obGl0$Y^^t?Fa_Ar-vLj9<=s>}0AGg~oftGU@1_z$mUGTtwl4ohMUU{aD^x@VB* zZO~6hsLEXCwN89ZrJ3nqv>F*9qk_7Tf4=Y1)b{6s)rQh=Q0K>j=FU@blrWlVxOj#1Rv3? z#9O=C*vpFh%r}hiA2G414bRK^NEq5<(!+DG#>$_+uuS&d=ZeMmJg2RWjGIXEhp;UW z-V>ofNmH{Sbw;J(+L0B`BA?(r_Qp4ke4SIn-7>@-nYW!lW*}Ge?N*~pj2(wN`G?~* z6%e!HOdX+x1=*=h12kcg{bO!km+nV&z++oSSNNw>@Ikz@4~Fk5hRC%WO*(ANnT8wAVLgpvue>J>dO3W4mMjK1v_$-N*$+)zBcAP;Xk+PIwbXO zg-esM#ho^_82&lQ4ok-(9!aAj!m>rBT*`zADQ_^cbLoyFoV&lKi*B=@$UfdP=OeJX ziuASi_#1_zu_KhXx#s0~&y{Fp;EF{&SNbgzt)f)MvJJFQ_7^of26#XESAWUU^>B#O za>-*>e>`c-JZb&X%gX*<7vE=s{PVGt`2N*vL#yXid{L_Au2W~AoTFwxN_0uzvhC9V467)MiG54FgWE7>X7!S4Zvb4(>Mc53@ zS_GK6`TB@w2R!Cj;Z!K{%8Gw?-*sjGkn5wBqxTB3^|y}L@YR(r7Madwwh*CVE2qIi zQ-{Tz*x863jcPLU_)~Kp+mEF?6{LH&l`5`A6^%TuOkPznqC2gSY4gzDAhs$}1uB1Q ziyZ&b({_jmDZv-x^LvBs7L&u87Wq(BB6k)i6na~QZN@Uv4ppS`-=4Q!X$n>p>)blL zs_4M$gWrFoBO~gHqeD>c=rhKOys=f>!tJKV$BqYY874TCd&D_>Q{P{r-!pSRt*=@gk}jSh)Ry4yUME5`{L2hJXM+X81r-VD zr5Z>D)D34;xE+#iKU`6&zmEu=`*P_HXYl^=%Egq6je)`50cHw|c@}Et9>>t;ruMet z{KwN=r{k-{*!}LPg(S5%ww0GC6rABJ&QLR=e_W)_f4|^ULQ7Ffzh8VR5wez>VzRS) zqLQR@VpkVrFOQe zM!^lMLBZ-1c{4BKr>*Ye`BOXP;fby@BaSh(h0(^AYJ};i#fX-EOz7%Z&Qk2Kg$G>S zJ^1Kgz>52eyLmO9X@Y6)UN+|Qqru*2Tyd9?M~UInwm2em8ly9O>t>gYDt!N-NlwA> z`~s_fm)tlVt_l{-#xyaDgR0h$DSEN zWOocLaeiHR(a-h=HJ_ceCo_^AbxM$$p$G7>ogj-JH}Do~vDE{bo4t(U;XB0aN_VRatr7-Kd_*poq)2i|F~;VPV^)Bavk+38wUak*%c zOz&z5KX(NYs&af9?D=MhS1+mZ@I}@KLBUBMa*L}Zf(Ws>d3j#ob+*=DhM#dbgQL?Z zwLQioOI1|=McD*8mL5}rc2|0dwP!xE&jr=xq85M8S53cZl?Xjt+!K_ximMBx zl#w?ucO!I2y^p_n;g0ix13M^G-UJ?FF}0Do0w4Vpl;$w2JsyXTQ)q2jNcT~wKT#YV ziI{`q9(yRtt#YQpyGQML17O3ZniXlRl6U4c${1S*J09+T4^tgZec zKIw+jzNoS*ad4+>)cNQg5%P(!ao_Ez?k0^crr=LoqlQ-GhCAbJvI9TikNYF7VvzO@ z{?`!cFAB3(^f6wTf){y1j>9`#X0F7=)r=2!*?h(Fn($nzWJ+LcdkMREbK;B5A|@H{ zZ?mwP7;?LANPR0@bXdu3Xxl6-x}&*ik0fMlT|3NbGq@Bi`pDvJNLujWVR=`LJiJM2 zy@PE|kuQ|fkTU@c1_d!810lqI&RD0Lj(gE|meFv0$`-E+P; z9ffb0_1HT6Da2#(qKtUid)NKmOJ1p+jqS|ARhh}f=_@YHHhN78MINhY12BZMC>sja zMSZcqb*1KYlS6%{y%&qHn#ag{zAMJ8t>gDcu+^F0@)soDzZ$|@_TCx4f6L+P>vl%{ zokHxr_9pH8=Y~s!#_)EI?Xr(&hGyz4s;@-2l@GNBIQRDU;;lP|B@5H`G=vT^ZXMWh8gV!E0=0xYcp6Auzs)=R{Y=P|B4I!9iX216?Q{e?(h-X#^(nc zyoa_OSFuyKON?Pjup4}u+%kG8H+#;jp=;4zohBGwxf-y{^^^!@UdgCT@8F8p4+*{* z<6?QJ`f2gZ(+{fW)B@}RT5(+H)}ooE6~{W^WZ{)VL1IO_S1gk2_3R{HsnArOY?pX^ zsj${)_zf;S@p5+ktixy!5sF>mPees6VsuKBSSow{l}ulymeq~kDZky~eT>yxAtWlG z74_-Xtk+yfcxIi%s1%Ui##hxFv#*@jhLurDurhq4Y)LI{sMGjZb?K8YFS^Wck7YE~ z%>}JUnJ&v#4z{jHtnBC%eQb5rXXxD$Dxf=>(Iczzl1FMm0pb3V#N|b)xkDA3%~Ka2 z+(MLM+I`$zt)32N@pwObI9hm*O0KlU=WXcRLRAsH`GPB9#k2O;k2qM> z^)E^UzqqdGofRe9PKdP%bZlZ&#aIk)ubfR=vfA$H`Vb2CE`Iy1sdL6Zn-9Fd_QMyR zNgO*RDe1qXw4#V@&c?U=Nz({gQbDCs&2g|5DTZsSP>-D>9MnEt)}l1r@?|9TNd-@t zLXno+CIL2??%N->BaKqIkEmcaUyV+UmOG$>ZtLd`Q0cncD-93mun245W%d=!-9#^H z(YNZJ(NsW$bPojd#)rpl6EF%;-YcK$SM%*&jrphI0Aw?Fg3scA>~ zu5)QZlwVncHLhRxl*Hub3*VIMK6cOx@Lt%&=rawJ`>?h%^M&V6YQFFr@#A4rKZ9&D4QM4LIgX!8!712AIbXqY4yQ19lekKI=d=fJn`@tepOw8 zN>3BamCb7%@0vSQ@>)E=#=`!YlFlP8gO0e_C(orXphxjW=d9=L3`8}EP{y_u<t5Rh7)Wm9w3<$#!J; zkz()F>8Pj%kF3g&Ba-?e)!Cl2B?e0u&xDTLYmSG9`k*H zXQZSSUxvnoHb*1xAUGD%9x}|+N_eB-& znCBZ7(FH?y3*QxXd_HrXf3;KR%Vo=RQLwSvgqPiWPwF(ja2z$Vz1tyr`nA*hk7y6g z(Sa?~vJdB*R$dmlbsT0o@NQD46tORBq&cG5bE@wuhZ?A1w&+0x1_=K8b(S&xi=rgchn zIZ|ZDI5JFN%e`weB~^N5Pjl;$hx>Ehj`^@kQqnFv7I0Q%b*UX`*6>t_rG2LIw&?H_ ze^X0Tl!d8J80)7V#hz@;@hFTxQ<3+}s*kf2H2d1emf+*7QBpSSywontW=1D;9pR1p zKcqZ5q`R4pWpnHJUTqY9&MDeU5_z*p<^Db1>HOO(N(SQ(2YSYZ8Qw96RMVXr${J(F z3=sA-Xc-Gf)iv=L9I|aXV|nH&>m!G8iR?ilbTB+}zj^c+P2}-~wEUI^&Vo5<&UYSK zcdbs`wzs%bBjlh#&Dctyr_nAuKYzt}F8*YnMAek~>equ}DObv$pPQdk06Em44i{w&mM&$w7?*p&}q7kW@>gckN(8# zcw#filvA0(1i=ybR54-IPKgL1s#jx-h!A%(5$d~6nBxI6cz#64JGf*Mn6%u7Eg69K ze<4E8kMDDRYj_qC5t`0#cIAms7HLq)z%A7qI(}+;nED>68?pg_hFFHsqV;qT4 zv@sEC!4aXvnz@1YuQrQ1pm?c)H#yOj-U{QOJX7Isle)ViOceTQyE^fNKSrAPN0E+n zEQRb}O95dm1&&91i}p2_>rsQc(i=#GmY-r)OXF80V^)v3CDH-&CmKR;kSkE%Y70qpcc3hCVsYfe#)p{fN#UT~>(#*f{Mu4i zo6d)KsdLg7I9Zd0OiLCkz?Ye_xoQyk%9AzCwXUT*mlb40TRi5CB>1R;?HUML4K*q0 z-rH#>?wU|Nc~q@w?sV{%@fJdAY;zhiPIA`4!lRw972&BC@O3UD2IOEn9AwAbD^e2v zMZCOU(=(uN#he!uz=nE}_KII0R(!^hPz>VKe;%IZ;em{MqR`z6DXV-@=FDE;-f9Tc zIu;MIY3V4bXp~G9XRqb7CqGbKjz-t{wQK^aBW2Tvwy*P%M?7nozi8Djh%r9?U1I;B zXz=Zc{6)~*P&o1qCSBcB2qu)A6VrYF1 z1r9p2n=2$AeK~sYn(xarzJ7(dc9bJJxAN-Bvp~MscrX$G*;D6U7RkBo#R zczS7MK5S~J4wElv>7KT<^F*dw-&F zQ^h4#ONs@>vv7lj>Z#i|W_jj=?LH3c*DUSvHky3OxUIRc@JgHb`}=2VT=eH!+TB5A z*nWt{u}Qq+Akr)4fg0@=!OG|Qmm6P{sd3!A6$Nj{v^>t-vA}(3j&Kgt_$rC(XF@&R zr`2MT?+n0qpS&uLKPlT>$UBOEj2TjwGB*C!GV$D5BT2QtX&BZaylA@Ur#9_qQ3Adg z&(9|s6w(|xtb50w=~9{(>mULD;bXwz*SVKevtP6am^~1xIi8f-QIR#gWEFtS%~dV( zl_fk_447!0S+UZteteuJZ=i`w8uM1ZCp7Q)%0sc_Js!nHcRE|8%4SwWm#vD=)D^m# zWX*LuHD5;ERvC?S3K&IaRnLrcjnAa5a9t0?n4&n4dAz4D1?lT4TnIhPcvsT9#1z-L zd{)e1Wa_YN&q&0@@=)dXD-#ET;rX|7?H#O|gI4TsmgSBJDB)gGwkzhfb@P5EaO~RSs{4AU>7*8^Vp|>9j z<&F0ud5~bzQ^ylaa9n!VmR8<=`ag{1x6D=jlhr zn;rAiH2*=);EzK3VX(RgKLjZSWFgI5&2a0bf&`O@!ILC5bO7bD#^z7aS^@*7zlzoZ z>A8kC!y+~9NF2!5CL|W)W8;PbnF~rN+S_$qeJmsob0Cv8#?KGq<%#ij{T_sA-G?N? zFL>s4ygAy{?N<-Rbq^rh4|-BvfsL)n@ZHpjV*N?3M}#~L+1GnxFJ%_BL3wC8zg!Er5Evk;z!m6i8ZOA+C+Ad8Y~FX zCg1-@H!?SAGK(E+HeCPazSb&`v_jGgsU*{p!_$#5-`faEO&})tf6r4s5=1tKTMNxa za>DGvZB4TzCqy!}qwWB}7DyK|g>*p5v>+4k1GChS1o)#MBxv9K!4nJq&VcfJWhaIH zas7fLarC=@*vCOKI`2Q!x}LOL-H~7auooN|91Le zc&z6iIybWN|A;Zhx&E;Oq(ON4St4BjK*fSYd3su+j`shtux96fhRZl^!_NU-(QehucQA+&gkT-gYm>*e^D;m8h>qr zA08kGWP6+dT;{%2cIMlCgDuX9zeE$W@FyB}Bo065x^0HeiCfVWPTWqPk(Cx?!Ta zVWPTWqPk(Cx?!TaVWPTWqPk(Cx?!TaVWPTWqPk(Cx?!TaVWPTWqPk(Cx?!TaVWPTW zqPk(Cx?!TaVWPTWqPk(Cx?!TaVWPTWqPk(Cx?!TaVWRpw6V)}_3}6KXE(h?ZAQ@w@ z0wXAAVD{t$Or8XQk*6Dg{lFhbhI|0U*56PF08=U*5LOnFhGZZm663X)GyzDqWc$Uk zbKU%j6g3&fIJpY|kW$av3k#lPJg`$l__+bs`t!SaqcH;K@khD?$FTw=I#Vpz(*s8g3Tti^*G1X&U zK3*t4gupQ`Pj8&+F?B&Qb5#J7&{Bc|WD-9QbwTomYXY{$W&%1GEJ{E@Qc(gaBd08& zq#`M!prR}*FHYK6PEkr)MM_3qLRyBjp`5gg!1qfK#D;ZtQ8m}q{~i}`sSAEjR6sz0 zWPqF`2J0#%qoSfBB`qr@D=PsgByh*Q{Se0_ym3P79CT4QBo_t)ezfV!ff(bAb~*lA(jVEQO12s^#yA5x!H`xT2)oeNe7sDUFR)4ZU%G2yZ0HP*+_L zBvBHLc2<>BMmf1CC?h41a`Mg+@(33t2^B?9zERE!N=T%#5(0@@OC<=ei$UT^#efuk ztr|IFkbuWenUF5BE-un4iW1IFGBOe}GD-*u6~JD?MNvgo79p)DqbRMiF3p-{BEs`m zt|Y0PNm3z|peDlRUv5{jTgIwPf> z7Wcg(Y9m~~L!Rhgv__^OS=Flx8i3Zs&kZGjLSQ|Q3;e2DG7*X6zYt0NhNrfnxvqe& zp@ojAm5Jrwu>2#NfH3Yee*Kr8b%+X{3enA-e}Tz??> z9X|^#Pd@=qga=9hiS`4P?PuxxMaDl7{g$T>3NXa}eO_4nB={XKEpKmNu#7}G|6N)6 zh2VF**0NJZ@o!Q|&QB>)_q0~eq<-q+4>b`~XwY!%L*qaLeEfUeu|WC$tVQH*K^1`{ zEh14DM3S0nl(V3~8XEmq4c33sG<**q0G2-d+tzz68XU&OF93l>X}N;N?LX@xeo@~a zQ-DMG|9kmmWo6`C5V8mfB_$Vm33+KnSqX%kEP&)xq!CJr;E()~{y)jDWGN#{+Pzj* zalJ$N|1AG^1tHxK-mWMxijw-Zg8px&NM1of5p)$u327(LM9a&|J4qnq5lRv$c_%s0 zej{a_l;qcQ^w$*qWf%ERqzH-eCK+}A%qJfN7W4>ypc<+Rx?nM00tg=;U_6f?6-p_8 zZ|CnL(jR49zz-wvOZWP>g~1txMgLM9evRgb34LUCVl6c)~0^zB}ly1J^sTd zn=_cw1oOORfZoX&hx7vQGqC<}8`1|0R@GBi0=!UwpAQKpEmLI20s+DDQ_>n+cGop{ z8wqw=gT=`-mgZooJdGw;f6RnHV8Qxz7Lxn|{zzBAhlM1gw6`-FOgppO0k8&m)d8zs zSx5_v#l5^R0A?jES>EOJ3m?~CUw5Xkv zw4~V?p=$(S9sqOVJ*>38!?Fm{)IOP>(gf{iX!#v>!eY$7^QZLp_B0{sm&E718_IAU zzKg=?SpYncfzsavt#1fmP|_%$1-e<<02s(g`Nkh@Z31Ah+@G=r=V3vTfyC!ipqmZ} zPo}}*%}Fu~1N@8&R-cqcKm*(6MOqt8<`0uZBFJ%pvH;WcbF?f67Qe1OBer6>70NJQ0p#$K{iY_Dow%QN?UuZCpJB0o?nl}nuYd-oA0xVW{ z1>bHYzGK!y*pPj^!QBE1gvKCmK!f`cPMhqD0*l?fp_L!lwXhOkiw_c4(n5F~HxTFg1;=4GL_1DI04pmIcfB0bZPd{=+o?8$B>uQdjiR^5c)50Mvy0n z1rK~tK$7oi_{opDpSqU%Gj$_%H+3sy4SgYV`A+4CK8;Euz484((mI3XC(%IAG$*;urr`YW6L)3@1gRef+fzLMi8HE!pzLC> zdq&$&oM<1Yt{))iT?`VB^icm^XDpP7v8c|wOI#9Y& z`cei`o~67(d6P1MGMzG)@)czTWg}%bUGNkk9q7ZrCVnfr^G|3zYzs1Sp&PsH~`vRK8RpRF|l3Q$3=}rFuhEPt`*; zNwrGNOwB{Rhgy}|klL2ol{$brlscL^i8_b+EvPqr)N?d6G@LY|G)kae+R?bv1kqfg ziKEG+c}Y`C(@Qf;OG~?*b`PyOttl;n){i!n_7-hAZ3%51Z6ED7Iz~EvI$1h>xJ{AN>*o8-pl=I)f#HJHsi4 zn+zEYZy7omrWqL-1sPQs4>F<|Pcq(Qe8TvSv4?SiX)}`;lMd5iCOp#>revm{Z+ZHw%Hd8h)wo7cOZ133yH`8tw*{rwOdGp!L z4>p%>?%e|0BD6(k3v$buEf2T6-O|TS!!F8h$nMU5k^M3INA^jM%^dO^)*Qz;VmXRA zx;bH-BAkYto}3Y!PdS@77q{|m)!K^MdSPqE*1E0p+qk!BZbNMg-mXME*Ill+T%+9V+-lrN?r`p>+-*FRJYqcNJV87Ucq(~jdHHw^ zczt+p@s{$A@^SKM^11Wf;CsP0#J`1KgCEU*gTI7-Sb#%7OTbg$robD4$sIg9_U*v$ zxWD7$jwL}+K`X)2f|-JyLJUI6LM}o#gkB3x3iAsa3!e~96K>l{zf*aq+s>Gs)Fz1o37GCJ8MGe~C1S9!XBg1CpmD^CZWmgryEiU6U%8T9sCi z_LP1o-7d3PX1~m7nL?SbvSPAG*;v^IIc7P1If7ii+?4ztd6fKJ`4)vO3dRay3a=Gb z6qOXQiW!PSO2SGK1T>Cn+|%fW?|>uV3GmOFLYmH+Nty#%yS3c4(zV95rM0oz zIok6&sye|sFLhzM2D;(8AN4ltS?S%@`=l?Z@2a1sKWU&~5M=OjAN4+?eb@Fi8}b?= z4O0v!_bcrW-v7pk$;iU!w$bMUVh6AX3XLg^_Zwd~?l9SD;%$;=3Yi+3UN`MD6E*WS zD>_JX(ClFB!G3cY^AqOf7F#SFEmAG!EVV5oEZePiS>df-TeDgpwob90x6!qUwCT2$ zvJJMau;a2r+vOjkJ!E|->Cmjbo_&;k-(iKrp@*9sL>-Pfyg$Nq#Pdk8Bda6AG5aX> zQJbTwN0$+%hy=u}lcCccr%9v^G6p%~tm%Bic?hMBibM^#sJleE47#ejUUwaGgS$n$ zjiPnXx6xDX``qK*=RHh4l0Ar?HlCSYbY4fj3cNRadw9RW@L`T&YJK+jg!***s`^Ix zPGXI)NjM6e11{f>9Sn0S@w@Qn@IC$-{;~ew0;~hFkFg!|K2~{L?D&P_1A+R1i9u9B z$e`CJcAPkMq9<4@_>q3@)@$w#LcPI;WFIxT%V^7PCZn=^%HdC#6a z+k4L7Tv{kgC^ob?Of4+_Jk@#h`HvUmF5J4X8txqa;iB}#=!?sjke5DOmbrZMGVzMb zm5&jM5qGcBT=l-%bPay(Q6yVrU}W!gFaFe?2KnSpWVsXk`tawk$XIMGS4lqJKrw9wqSq3>q51{ zoFd7h`D{?CpDs!q7s&YOme$1^_s?M)bsVS`8Tl=C8UiYeA zx4x{wu%V*Sq_MWis;Q;XX{j(f7?>~$qwp{@Xk%0(VuueC3NlX%IsF` zF7DCosr+p5xxE+JJMsnhWx4NcKXZTd0RKSBpzPrDA^oA6Vf*2}5ucHz(adu0o{^t}mY0hhIX+Heh_HU0Clo!gu#)$(<0n4<@ zH&=GA6s{Vrb`ZUYMA8Qg@>l={?v!9VE$C}9*jWf{IJO0hAsATJ-o3~a^1U&(Nk&}Qoc#?+RmYNWyoZOE;z>qGSi<~SOpkTs+ zp~*KQahexw-ctxcD_A0NDUL{7c?ibrtq}CmckP{zL`|~^>~BNb!wIV6&n7+oaQ$-j zq9*WIJ9}|$KP*ZL3NWUiq=c7oHWHn_l9jlW(XAlN;Iik6CsmhNAD zf2I5zm`FN%k!A;V2aEz7R`~Pm#pmGc#RMQG4JB+{ufRo1M?+0T4`ZODfWX;{5Dg5- z5728D@Pg4&(oxVu42+u~aPA>BIE#@6NKXNy!GHu(u~F0P*bLFiXl5LXou?VLV=Y9Gk-7>AmQT5Gim&TL^_28Ke?4>kLAJBi zuG0G5%DvMD`>au7DyoM2ZNT8t#See>oU4C;TWHvgsE3Kqa^AdcX#CPQGaDU~l$@Ja zR^HUyKQK3sc0YDJ@cf1Fo40O1N=eNxD17(+LrZJh;Lz~5g+(?992N6HKy+=a9rdoI@wzG9-%314s&`=Ij1_T@afl)*LAky&LliV z?v%IqoUI_D3r>x+5@k61Y}-s$8ycIMft{P<%XvJSClfJdi#4_c%CEpRLE2G}is@Jsk&oy^kx^>GW z?1XXWG3u%e;n4D13)U!qWTe~v%qGPTY+p3`0^{kv+8BGBd1-|z4!WCZQ!qTc@z_wo z-;@FZvF4vD+Kj55jg^mUjzqe!WVsr&hM|Ucr+zWhL~qwiI7MGyd0RUxd9hMCt{Jvl zZ11iKi6=)~7A4x3qVLh#7^-U;s%yVE!D~D0f*w&apZb_OfPTJg!~ghh?)`Ifp-~l7 znn4EJp5B`@gKDsRzNkhH-lW0@NS8~+H>6w6-rpfqe_=$;)Pc6c<+4Y|Y+!MXzk;MRCs4GJ9XyD>yd^r(!Tz;TH+Yk?+pDvSek^`Izr; z@L-!qs!DLUY}2Mvg8OD@=5yftH?y~ymEZDgjCnSOeZL?1fO)-D6VXJ5ep!=(bt!xY91WaD+gL*y}ewFJf#CZw$8nUm0i;8~*FH zK@)G2VrTy#PO)4yCC=rmbUI~tU|V#(@4l&^MH)wI*lAIaI16KVpj5DixkKzAL(58+94HKr~l`3V;`gR25d$ zp5uB3m+?jtPxP%{gEt7Xt9us>M??%q%jn6L}d!d$9q{R)+st+Pu zv67RGWqT^rn$J*`AwWZ#C9?NNMS7TLMoS;K&*wUBb8j+2!O!~1bIK;v|A8v8`^ob{ z87?i$dd~vEcB%H>e4qERPLo&NirY`to=;{SK27*w2zoTfX(Gh2s^uVfJ;cY)rX-b~ z2<;YKy+Ej5p8vv8x2Nk4FME4<-&APb@`tMapqKWjqONk^&q|Lk3K}L#9Hu>>C-Tif zA5@or+ZSqwI3D#m1~t6`$46alX+PaA)IEEzs=3RQM=7CgUg1bmx^I+E;N><4OV;GT z$&AU@yIwc!AC&0UKKDD$|7Z{Vn^gW^FN;Zw35UpT6;1yH+pNVPimAQU6?(nwQ3E>K zXbhkIYc}m?K@rcg9lr|rXi99_rQsdz5kDJ1ON7D+U%I9z6KuUUJ*fv~)6) zVB=6r~ko9K-eSI`Ie|x?^rhamHPgi3H|LSfcbUl96eV|0_O2qNZ5Ogq{^>y)z11L0~ zuMwdg@a4wp5G!83=_AW^iy{2yf)Abr2WzMP<3K{I+J7VX_q1k$ zwD$gGL|q`F35AI&spOt-HYI8K@MQ}kB>H>d*27qeb!6!X>()l>F84B|9V!qV|LiJW zXAlP4Z(n4lA&#aHH`m&uvnzYIK*afOpYyNJYJOCRxc&IwHVMS0|7z>@(A2!}T(qib z{jzJytvX-hb4TUz9F68^U!GkKr_apAJw_cFeM6DbYh>%m0ZyDsAG0|zZ0ET@THD~3 z%;?Z?X8cyi#p+utI`~)_e7v&KqW{tK$cN?mPeLU&Gx&+l3!Jl7__piI-s!|X4nBoO zrc5b)EANZ`a{?6o$Kqw3<5g+D-HsUfpO4zkrsohLVNfnFlu%p4T_4kouTJa_?y2Hv zE-0K8&4w>WRxcY9F!WL{UJd9+69N)x_cEGA{$^@2#UN(N@7~q=hDbpiIAHheQplO! zQf}9!>sLzmNp7Fe=XTT9x6vHbqBYa1S)o1MATGY|-s?#7qs^YL%!)Qa$3+B6F{wka)oGk9_s` zFHe8`-;##=w7YjL&|>(diO`3%SG-pz<5gKc#c%qWXv6a8_HQ!(Ungu&kG_W9_UA3e zf4$eOF)$%7nYTo{7aemx#p~aF@*xnL|Em@Fv407llokIwY$)@Pnpi_y8rRpU!x*Id zvupc8Om2Z=6t|irnad04hGkD~k3Fo(d__4v3KiyJCmQ$}3{Ol?@%OTQ);L+dP@a(E z+s%@#|G%x4zYK8pPA;KKoA4(7bga~qg&OaP@tNdt-h&Q7v6<#UazyC8)G9N%I@bQ@ z)sd7V4|Qy;4$vh>^)h#Puo03vo(OxUUS(;UnT^7WaVFVZl{#8!{Nm#Do9nsHA#O4F z9_#;?O8SWkTWV{!vhJ5|ahngFeq}fD%*(uNj%tqYV_>lk?b6=mi4!h^e+HYKeCN`wy%qRZz*mustS9_^|sIFcvYD|On5C31iGr321U z*^F1Z`2vd#GyeT}^fx25hJ+6V?s-Z`-)`HWhu9B1XLjreZWPBHHARd9xy1QRkLL%Z ze%EoziP^w>B9z@v5;b*QR5NZ1n1!X`fiGLQWh2!NbAvC`r9fcq4~}0=2@A)%zNStZ zqUPdNnLh=WR!k+DvbZG_l=DQs%hFAZJI??W2+u({-M60f_dOVf6ufjPJr6y!$Yxw{ zD>}bu8|ye<?jD+a_}5po>X82P5B5$j(rUoFm(%suCEVdbQ2Xoup@8KGqw6aA9F@ zk3L7KFTZv2Q@F?PJJ3gxmGG~&v8%9EOCGwzD+71gC34rAFEsw&H(&q7PnP=zmZzf& zQP-wI5?sYj8J#=o2gV-Q_|9;;$}5^v9*h(nv0&)oK^}UA=e)A*fmYrseld=}r4m%hQB!2}x4tyjODCnl zcuNu1-jc$UzcHP0bx+J{c*Meka3Uml_qriLIpdUxU&FHovv7A!ZN~WH8Qwu7h*HHv zOy}88H8Um|yM8Wo0z#y!Tk%TxY3SLy7@a|RCH3|q-(Y0ozn@H+7<@!(A~N4yxqf8%b0M|JG$)5nSULL*t!7_=(4$>X ze4h55tTf<~$vajgXKJYuKofmsH_FZ~j^hH$R&xqfFZCC9_qf+6Uv-Hr@H0ygu*@88 zf~~Vve(+T&=_ZM7&|=Q1V+K+y9Q@M;N()pox90~qYIZYTs98=Do-w0r%c$*GdbvrS zEAANIA-_lAd$n_fxeoTUI#6El-GarpBx}vY^QTSp6e;T2rZI^7k4qcx-pX8hOLl%#stl}{Tf zxP0a~eKLJ(zQ~uB$vaO77F zc73s^^KiUlqnX6yNcZ}Y*zT6?oZI=TCn&Wq9!`1E?8wL7k=5Z@c4_R>gn!L>EX|?f z64TlJCDR5Ft+Z|Tt`o9fuE~024$~Z(GbKWLHmh#^B{$RVS(QX5KWwHMT6GXlJkZV3 zTWLX9kTbz7gah3zI0YJ0-wLB&H46V<+`V;Jl-=4sJcx*ZbV>~p(jn3{fYQ?4B?u@b zBFzxe0#bq?4blw)N=Ygq4N8ZAgmerr4BtZC@AK?;?|1Lt@7;SJ$M=1IaLh6H&Asjw z*SgO0sx_aaYgsrxsy@zB-=C~NL}BT>G|FpZ%s9SNSC!#)R=)Wg!}5?dfn362;lfyY ztyT4xP1>X$1}?}EHf=pl)v5@7zhs-KbmITAG432Ps1251h#F;4>M!lMRItjw^{1Y# zT=Y!mqG!XjmfgZ$SglMYLAm;59|Yxdh?Gn0vsV|^y=uftcvvy|lJ!&y_@L%V0!$H~ zmIm65Ogi4PoEVnR4@c!@vfa8HYOg{epG;^Y;&Ly)EKH?$wjst3f^ZOI-lV2;xfNaQ zWg|Ibpu%=*NiqChZejtyftAaT7cY@f>4`X`o(pwd36yU>R3D7YeiMSi<3dnjW&6&s zWg}EfE;QaTLl=v86N8OnCYBv)fdTXU5@lpuCp4;@A{WFncApy09SpO!W#+QL{6X-@ z*JH;2y8!kJM&0F;j5O8UVOm)A<^mVju?Lagik%CSn?dwUJ3_mMnwK8d&H_F?@_wJ z3roM0V|B0se5fe?nW8rdD21}`;d9%X@+q{8wI3ibPd^W{-5`M?vV_(wrJzB1@fJ&o z0SAf2vXu@jF#Imw>h$Wa)Z1F~H6(NIPQQ!kwyn~1Z6kKpX|yv{@(r z#FEHl&VlV!l=mazoA<*28G-u#Vh5(vz;I6u42Sn@9mj)FOM<^Yn#zpGtWy?XPK!0j zpwPIxmk(RS&{iFN2Y&M%Z~(VoG)A3por_rM*~(gO4w{CmLU`1N*Iu(;mwcN%8>Jk@ z|K|vG!X4CM+c|_-bwPOTK)?3ae=?l8TyzJ9d>4q)2JgV%`*U~xuPJKggDfy=8{j)3 z2EW;U6Ex@n#%{^FvAphTkkpJ!C~RKZPv5OrF4CI*C)DIw=}WaX?WD9f<}Rv)R=|8F zB0GnuHDi~NVnxNtz6?w7P>+~1d3FH&V=|`VYEmKAh54)Pnbx+2ACYCu;w|~l2l7j|BT1H^rU0COXn;1;6&YDGynHL{*!4U z^=t&k5({5bf^KS@+A)v?{_TO8+*-5t39JZ=b7QMhq_U9t4443TdAqDfA$ z!#RWj6A3)f0o#J`YR|!?M5rPGuL<2K;+n7d`<8ohljeo#btt3ot(1X!YCEl->9I)W zkk*9POxfMrK$s~H$nl6Y+rnir79|YRsc3Z9uAD>Q7}s4=)Yl}Q5ce8^BpR+-OhMC%34DZS87hGbFYW2t}Sv)n~jW-pZq(3rHr!5P2pu zfI)`7M$Kl>-TT^R{wi*$O7|yUK~?)djGlenY7jCW38N0Rd~rpGj4mQTJR}j|Ubww2 z4uSJp27>_DUf*0l6nr@LaFuqg*wOHXwgtX_aJt`5%MS(Yf=!4fK7KxuTr&uRu+XPJ z=i$_;FZ%Nm&&z|6H5K(*xUZGjNZ7bW$ka2a88vFI^D|Tr8Bl8Qls%4r^P9202OVgY zAJS%JnZggDk?zJL2|FZ01nG*$bdm{$S*c|ZaXf)MtzRB1k$pJ!!;J;&dud%CNAZ61 zmX_%bKT6D5REB3|!aI^qh%P_*jrw5Y{r6}S**~BU_R|D%FJtkQ1Z$mCzeynYKq}(l z6oC)5MZ~pQ)#++5HCEbpQn@F8>SXC`?AQ522iU#89Ox>*kV2s?pKMdwreoN5PoR9H ztoAzsDdwBD9NAceF4@zAOUw6Y0fw!zcg}dmN#OEul;v+X6-9|BSpV0$7FE^X^ zGeD(#q;(EiFElE6`c;c`@zC_zXV$jc$Xytj zsQJ2tE8KUJg0HjnUkh0K?ts4!wE+@18cp8GGld;+PnGPIS@5mLy>L1)g>7_AK*^hL zzqN`Jw6-gDj?YqTk3N zX)=v-NG$Mb_H=@ax&cbu)xb@&w@GUn$>#1NLsInVl|S#YP(s}7rxz4UwE?MaK6lag zor!-O6-4n>gox%UQ!09fvdtSLKOmA_SnGDUiXdzW!w{k*3m^^6|MHUg&NKDKr#I-L z_-Oj0Tkwf|URM(6m}%>j``--r6%GFsxv;koi`(R4gl`c^;v?x~Jt11{FS$Zq9H%Vi z`RboRB2E=P^5xBQ2#DS$U|u4Jc>rQ&-}IrM(rSUy_#Y`?)BjXvR?cVF&%$nb^)QnD zSMPFdwi_s8v)-0I{Xxwmdq1X0K<&xShR{Sj0hTBRM{TX~WSDIQn>rSHPTZv?AKyHY zoutNluCu(ZWxSgT&hKhtuj-fW?&n{AQoY!=uU1&VP>=CG?xW9<(mI>i!-B5@de75F z$v&b8!&Y@hp_R{gE%*lR%Pc>rN6RedkfodWC*0)7(T;P-D>-HQP4Yo~G+DH4D<^ODgpiq>C^qS~02!W~ zcnbbaVXZP3*R?Lj3hDRDW)V(Y?Pz7~J3)0+M7Hf2#VutX`|#a3%ro)`l-&Y|#=l7e zZJF$S|Kk;XC@UKN&3PY^7k%1%LtP|lA7e?anJur-goMWm$+@o?Mc$13VW@G_Ryi2> z6h-ilpQi8T&ySXUVaYv*G>ptpXefdho)}3HP5YAZU(%GnRZ5kzukBB>mV9Y%}huCEIa7iRY(=SLz zP?8KmALl&U`ue`$EZr2%E%GQ-TyLPWZ)Vl944WDsl0bKnzmvJZXB%q1r!FsuMfkF7 z@-%NvKuo7Cb?8HmAns8OA1h()>?l??PRvwza z=_>GUue_IT&po*yf$8@?#p1(SW?#vGw``l{tw$Dt7PJmNd4#*-|Voj=Bls7 ztM|sS%q%xDh6&bg-e6B?v((d^nAw9%o;EdMO_odhZrE@vo7MQ#VOtw*y-M)shfKC~ z(KbJxjexOyotHm{2+W}v6p{F_lDh#Z*VV6EKw>2N&LLx%wZq3wnd@jo(N25B&RxN@i{?GrgadIfvYGZc`Ezt%+)2Njq+|@0TIpmyKz+ z;nMSyi2E=KX-U?III_6)5n(rS^!(%5CRaN|;En`Xbx z8}_~$+?*$OHXf+?E;x3zDe{>Vy&bNRevebHlzA@Zbv)VmQt4_R=P^3=zf>w2~B_P zVymET@yIJ%2a3DSq7S)=2UESkvh&Hg)SxlX_xbZ{voI3}IiFDo@XF$MYC-n|p_>VV zGMzh_Na=bE4xAf!n2(=R;O7*GWj+d53mI6MkSe9NOBnY)aA@osD}Y64JTuoyP5RoD zc65SxJaiBq&Hdwjko*_hQr3HSU!YCpy3!8Lu2O5?)s6tK{d*)`Ep7;U{N>97q@wA` zUF3mhbn`E}r);y}9P;@?2W;sG*)6{p*&%H?2bG(OW1Y8BPe0y6@LZ)S>I? z$c^hjrHnv#%fwAG;QycvYmx&Dx2qE~(|N$}O~je`_W;?ru!^RTOBHlQ$1SHY(oKOl z|1g08yc|%He=7uE`ObxEku^>e}4=|E; zOAWEuFqoA&b+MIK>19_`*O>_GVoLyz{|O04JAQyVjB#4A1J4VZ_{UADr{;hNw9jrs zL0C(AWlL;3)4ukL@Z!COJ4*i$LH<(-orRZ>PsIVH)9^G8Ix*_55q0Jmf7?v_cN&#d#5c^^m`_38wSN}}>X!w4a!bU%cL}y>@ z?a5Z9t!j_KlJx0Sss2kN41cj#9^!%-cZDI}s~&4PJ$!i9tERZP#V;7K$^_o6X~G%y z=DO1Iq)SC5esnbw6Nk%mGnh)5%RFuWK<%NnzNR5_iq=wp&?&wjjo+M%A{(!bO8m1> zQmXi(aUk>Dyoslu1VONSN%O;namE0Qv)x?N%hI9iQ#)lEcLd=vfhfjv$Om`o<_VU= zp6I;KF)Tl*!U(>`2VC7|@IMjFh6Y-oleA`((tI=ajmh>k+ZC^Ke5X&!U?@CGM=Ode zJQ-Tx>Y68R@>SB>tdfTbua@KxM94dP+R^eZH|1xY?WI@fg)sv~O#&hEMgh)FHl z$?FBGFv(nzdED;I)~i!c#BS)4f;R@iYr%iO_v}hL_QaSGuN}eV6kPWo{FvYLMOOM?eHdU&%vzY zeww^PA+cn{mG@nDw>#4i3K}vOqGm?Cz3Jcu;b%lbmICf~@YDnk=MQ<-l6qi;zlc|9 zKeRsa37kKg4Y7GdpWTzvVMOach!}=Jv%)`-RDPg)NygSzaiuG*B#??#St=BJ@LM+| z{-JY*nds`A+>~$H(&w6Xwbt(a{O_ij1xSv}AXlIT#FzRrygpTp$r;1N zTQ$i*&6S@RoI}cKsl-a?{a(2kFUGoCs!rYHzcF54IQ{gVT_newj{!gS;mZ5eu2^Fh zF-_a%BGFqpHP#H`#zUtJ+mcUC&hi$I^laPObC|7U%Ovu8^M~Qw3hc@9J3_HHFS^?9 z<9*<0#ae!{XXtKSn~d?{(xl&3#tsdNVR5Q{tS+4V%S`MO?tu{$nOf_GE%~e**{+OH zYNY2Q0Zk|2E4dlIMY(0j5H+|eUrMWG^(PJYhe`t%M;P}*P)`Mb-X}^##lXZ(vbTPf zp%tAL!uQIZRgZnnA-qwZjq-SZR+A%TABB8q*AtS?3J_S&f|Kx8Q3X~|hf2|0r{Epl}gEC)sDf!ZjW^hctrVZ~O2?3p)0{W;FC1Vsv4E1GUEm+|mgi zYX|7a1J9t%geMb^Rzl&o+T%;9RA!EUEc1-_Xl{Uf+iglWu*)Oyj|*3~&UhD2XF+0E z(dgEm_VDb9RmV{?NCwl*Z#b~uCBwYY<6#m&r~~Wi#es5nSxikj?;PTa9)e)^BW~<= z=fZDoPWc8N-vqSOUG|ZX9cC%;Wd9PEpPCfq=<@NsAu1^S&56Ld{=<~D+Hr8txpnuA z!3VXQ2()@8TqeSDji^Uk$7M75>cNZSll9ju=5 z-nbOXguH#%@ODtWovz?Rc$fXu8K$ZEg~Q5#hF0*_N9@YjtMu)JQ(#4lfRSA~s{e)l zl7ax_!s&EdA2U-ri=@Nl&C)e=PPP;(cvmq$bce94`eL?k)tQ*Il*>O8QfAazgSGfU zi;tLBLA#sX#RBDZ2=KhD<(c@WC#_8VnG$QJ)Y@JSXH}2=ZhMQPPf5loKrh{QNwA`lNYvXs@_>Xc zM)8Zhh9939EdP}pO3m_g^2D7A*?MB5aQp>+ zUwDQ)q#f;p!L=OPLL3e$zM9o0{9sGH+`AMx?7WOp@%;Oueoe2Xp3FbGBw}k{cxe%A3hJzs^%5jPsi4QTw^*K>RC$E_qtn;Fehij^?mHrE)Tis zDQd`*#CMAe9Dmm&?xlJuSRpU_Ec(()9p)GQg|5y0xpaF5>v0{n%%}+T58K}e2ZlNb zXxqt8`dwQ2g>vorp1vGP4ail#Bct}7If&ZpIvR>09-G0uhy=Y_gNkOg2|Zl4Uos#H zeno1N7*+YScF=)B@JWt+y9OqwMq`q?SNrxvFa5iFB8^X#u8)6_^T%wb#d=X*K_B=~ zN-9vzeM#^_PmxQ_ETX;5LmKOT!Kg_BxQuzdPXn&Bc6$9@AZURYcsjw&kJc?=&Rp z!JlbJZnuHU0JyQ+#}Bt-Pk+!pv3+plspi^JgT?-<3;70Z@NemuziFENS+7fHfa=-u zn;jpc?4BSXLS8pVEXY);LqdpTV=HozD9v*tDNmD1U_ z6x7Y;-GaH`w&+~KneBsPjk16ncU?E##|v(xG=Ka6a0))xfM@!H#LKE{ctze9^ZVvA zRO*61tr5NI%o)88oqou)F)A zxaHZ8?@T-~N8*pRAshSHhKF}DmMuo|)aE5a0B6cf&qKK9wNloM^{I7sooNsFy3NV) zJk}x}ymPI-PQg@#q@g()eV?fe&#qBY_DOrxji`@)({yK)r+lY!Tg{k7{e;_bQg;4Wkoe-3J`|E3Ed22_C0KXi>=u86V_>7v-^O$q)wd%X*>*bnt zuQ9l79=}ABe5*&M>Aa=1mENz~^NhXG+3T8znm-wCwgVqXED{%InY(kkp4UAy$IsFYuc zouB(pWNHeKsW;vXjvrIpc==HgoXw8pMl|u1-Ejet!cNr#Z{L9(Kl%KlMkKxIksRY0 zHb!&Ud=WZUhK@~xw2$rWj(y_aHbU{p9>3VCh?^|trr6j-xyZe6L&d^aUT!maC`cz> zBe2K~9xb6e(2>wcFWr>QZWg$vt-|-BG}59ZnSZ0S#fN-2;D$O|XRMsD%<^g>82sOQ zREa$A0QWQ1H4yYu0-?`z02N2Sq2g>5cKk&s57~6#;S)E>!ThV|jrY4ymPmG#Y1uY_ z@nZ6{Uv79eHQ2IH$2nw+3w~?_FbjGn4F4oZ0>A}OhVBvozMlZrMQF~=$82zrXiyA);`5*t zRi@4xOxd7VWA==1`18EvW^XT?R~1mp#o^cfcVf2vo?(_Ip^G)so{}KgP(J-_qickK z3^D9+d3V<QF8boF90KN5ncH}m^zOoSN_Q!NneEf&U0cgzFeI}4 zTiDbZG3@25I_~$$b!9I@i^0`bU+l1%CYVMmQC|*g4{u+nEsX$ClfESSg zQYNdJDU8SV3D2d2`0?Ei+xSQv{2FE?zWGl5HZ`DJ5B_Q){}7A+RfwQ+bqm=^0^h%l zim4?qItxkTdpEg7VuK*ewD$tSB+&)?R%-~N4$FWP_FwsNmid1y8qn(R zO{%6e>P^GkX3?1q&G8QqKFogFJTTk7z1HXSWPgxMDp(7a{cX=_+vuK{v$<2nT}mbqjJYht9VqaDePJs9WF7XdTE_i za>1IPHZy@^qmZlxQLMo_SVz=X%E$bkN@bRNtkSK@C~i*|yFqWeCctw{?u6uY1Rr~3 zJm$v(raxxnFWL(X`CquG7WcCG+l%xa=DEVIjf0Ho+38i)DdDVlM}j6fhYx_)LLX?0 zvLEwIM-yV-hx#}7GgkcVK-l-^OVw)rH{i!%frx>@KY1n?RMD8+(NqOA7cm=Z1{YLrX>Rq=6abK;XxlnZ)uQdKO zdF!oR9*|Uw;6_h8m7)FWynj3d1s;n6gt2#>$qiC(@r8$z6QYC%kwQZQ)R$@b7Rc13 zs98SAu&EOD5x%RthF?wSH0VKwbKq#trtB$L(-mXs>DO#HeJqy&*pFI`tFt_q8Oi;_DB3k+0wx*$U4pQ z$|3&FQXi{OzxC--_PgtkO$-dKl9LsYr)j?nkZ4VxkXy>D^=F?;vYEI9;|d;02@W2X3V(lm z-b1it{)Hjap}JgYKeW0Q3u-s!rcLd{L6;x?LFf{x6zOHX3h7f$rTye)Tjf;4b0+VuKJw7@R+ddj zjyqe604%91irFUTvrpc~VHJ|ed`L}kFZYiPxFea7JqfhC-!JN2di#|i(pi4PH%9j2 zAMYP-nZD~f+@6k%ztne`gSOCYVFLGJsOFgm4~lbny2ru!dHp+M%N^e zKdEYA^SeBNq?i{_H@n9IFvjl}j{a^$sVV6k(hCoQ_Z*-w0cr(+M}ej(++4#%c2P8+ zEdk4!9pDdu<2>%;n;?*V2tC#YL%7c6*{HXi%;s*A^%Of4=a#tNrElpO016oN$BTp$ zc^8PdXw;c#2rL+)Zn7vw^=`b2^+_y89o?}Z$| zs=*kSdmddJ!;9w9;BZK-r@F?Jv*=Ua_~ItHzz)vvDvgiO96o+Szl@dn!pFV}Aqe1A z`|ss6vG~Y$e@Sy(8=gZLoc57mOr6C`W^K%(9j6L|o)?$Bbn49w>%H%v#t)bz$n z+oY=ZRI#eSp3ZE`(0?qZ%Bviu#q}*isuFmB?dp1mhT`Go$49;i8@?4_BB%GW3proMlzZQ2vmhv)>*lD;A9x`~;T~P=J~_HZ z3AzZjfwqk$bXTVkNaql|8w3nB-N2}ESmB?5?`mlg4k}}q5+}UB3wj*kb9W|)31@}t z?;oKov0*2`eI`E{%H`XpRMNB~8{tn$cCoXFH(XFkuIald$77>yhuI0Spi8e$qf@W? zFaUrU)kW4pckAY@wg5tO_HRm<2H{WP4qTL4|!`kIohsASgmLI|=deN3j#=xov3k&d0O<4};(5nwJlxP0#FGR%`T!+PdC!Nf0XR9vy@ta#K)J zM@y+$qzV$*kfwaJFB? zW7g0J#)NH&UxQ`k1h+}*(BVuUr?0-aJ#Vb|@p1gOm!Y*PBhiGb>K$^E zRfQO2P4t;Q{{nOPyZl2f;cw|g!!J7VhNJ6x9M)r)Q<1Nk?+mfYLbSo{Y6+Mkt~*I4 z=kI>t=s6LigleRx1P0LI(@Bet zr01a&s<>vI90f1meBVT8CpRYaWUZPp&Ht`)o&m`Ew}fC{@;4KGEOgVkfDdw6H^fJH zZQ883I5;3yaUzl-Oo{*f8Tx`sU^L%XS1J0qhVND)k!Xm|&K)~CHrXzhFRxNk*`lg& z4H4c#B6iap8x;Ek7?VL_&Jyu)H&4>Ia`?e6xig~}8OJgF5^D2N4jC-BF~^2P4G*Zz zauZA)3}rtX@i7@sE>VNB`$t*aa(|WDw8C(>{WOy^`o?xfN%;P|%wZU1PwNR45RvI8 zJ6(LLs@z}AEqv1GqQTnAkq9(iiEv2T8qABvmKzyXQeq7^x154r61$3U`hGosP=ST# z=@_0t$^}PsZ_U3~%Jv4DeJ2CfAQv(q=DS79?Cgm_Wnf4Z{jIcF?To4S#&e zC7ik&ajP4;>(C@>qwf7A;6my0Oub`+&=3tCLHkWt^Nen$d@djsI-|aO3z0VOTXk$2 z$!TzRFtP~az`rdzffb8cvq0UHzC6p$|c z1-_&M2+V!(@Qrt`H1=QNkiNnV^N$5+%S!-I+Otu6=T{q%D|xo5mD8@mB24lOC@c_> zZ39N%Gw|?r*XQWNuVqmm;j3(*m-HilYq966&5!UM#90t@l6gi3$nBT~bmO<+0TUeb z<+e4oiZ}V?!j6IryTrW6*LNEyX3O1Rcz{M0HPd!Eb9@T#d2TV}5vS8C>ge-q~$d*iJI+_#y|u^HEKr&5A*xBDPJ=9oHuT{ARwF zaiTmMRgHc!zx>d-FHh{pNHS^$Z!r(g#lD1I%)#b=c~PjPjg(*(kGmb0dM!--qNm7C zL1%s^L1?tm0VLEPzg8SNc}CACa1OyqeJ-&8CO{7${)LE@T(d+ej+*YCs2=NqfjEGv zySIb4^x7*U z*GiB71q=P(T~}MkhJ4(p1y7L(8#r<99u>XyeA9d4EYdmx$G>yVKL5C&MQTo+Npsd; zY-~~b32vTa;<`TP*S8ACJ#kMf<{w@@D30D|fbzx}yx0{hUjMBm{;eOG%$$VCOxS|s zsSs<$DSqnDDtF)c(VtcBmdPafL31yRjR<_3co)$9a)v|orDEjSUYD3sr6;h{BaDJz z#`{PFiE-AD6R|Yk^bJF&V%C?=pkhC~nV6nHV!?fUDREH6*PMnwkmy5h8(ozC>)XQg z-MB#^)su+FQI!T^i@Nr68(r80P9U{h6ZqSzfon$&L#Np+WAVQ%{e!<)`tXHY@FD&= zpnV#S2kO(yJv7OQhW_GyAN)gZ$0|DHuI46j!I zkqA{yy@7yczXMt?S4Fks%Js-jqY2?=+41cundlMw180mLuO8Mm`0!^fML!<0>q&KQW$wrS#h z?x@Irmwa?Niu8JkRjb0HioVM17#r|oJwJz_w;u_;Eo1YYYVAs;VEA~Lll&c|gIV`# z@h<3*%8tJd7UlK$?1Bmn4yi)|cd>j_ z#h&Y$rrC8!Jb(D1)Hsbx(x-jXGxEp^!FYps#MFGSWJ4&>5 zI5`AegYcSY2lr_g4;m3CMl4@t`X_JBxi>c0 z)|VRyoD#kB#tueqHn&B{v}7z)W`EF*<=_e?o4TXqxMKtRMp(AeO@6|9$3yRmmUUhQ zhq#{&X1N!HrY`7ikv-N6zb_yuWt>%I9Kikqka_Hxb>Z0ASqz(Cy`65m%lAB^;Mm~B z%0{|tQ{Wj5!ZT-$P(2Rsh#mWTt}PjCu;rLgBUX!y(Yee;Ysqyor9{iFkj<`hmUFb< z&G6xlnOAybS0~aaWYPXgDet%Xa3_g1`$xF%yF?7n%vw;A(f#4?_XltE7}3y9^wVOg z1RoG3BIs(7uO~p%ry&-6n%3IOTOZE2HjO-61hePyIGa<>8PLIJh8 z7>AT+MUZmPgAFOB%N)BN-+i)FJr}U3 z2u=XhGZmH{JHeKmTYF|frdeeAly@^Ww2ms1NnjDSD_;a`C*BJU8!dQ*f9sy;VLHRz zRo{;%LV_8_kHKKMotxo|L-U%f7o@aftk~+=2bK_Ur~`-0?W?s!Hci-!Hm2L4Cxr! zm*Y053vSRWOHA5%NMR8vK+xJ>gP&JwwDyeZXw>nH>`k>Q<*H7oN^<%FZ6G1=jf!P| zdQHvI%jW(A39(QI6G0rdLl0f`S}ztA1)B)xYAp4iCbQq#rSL5RwISfwD4P1Y$NqgE zFY7gGE`5?dymC6en1PGLTxshezoekBc=sss#j!TPNWCEWUdd$|N`O8# zYyn9Aj?o4RD1mP#$RA`&xJSsKZns}==MO1 zWD_Rdx)U+Cqg^r0^!5{cha2A9Jl=vkY&`UF&Y6^MLUbnM$#Y!s$)FKiu=}*<#9tpc z#C%;LBB5Z-lVxX;`og);++0>U0nH0}i@T;%8jf$xwW^YqjPA~xVQl>-P0=89Eo(5_ z%6vc2xjQCbstBE8Fg~*+f*?EUE&676M)ZV4_Q$2M9z|9$HbvW(HR*T0{LF37HNuYs z4f9>=tZBkHi{=zczLXz@APJ8AP7np}gKEo%$T?qZGHNM$H~I|=d}mp>>|VJEn9f#L z!qbJiVXR7fF_6o?edvtFq?ddP3+t=NeJe;z=7txA}XRmr03-LavqQN-5ns3J&*a#?Nv0nfZLfwNWpe zM9;*{7vs^KC+ngqEvXD1Fh*+uXcNY- z^@}oNmyOzVqe>ZXka9OacwsPs_Hc{*$HfsG>F1CKSRg`F+KlLhuRXZ1$*P_KYIo%#Gzq++Ag?bG83(cgz3`ERU^bp70)4+; zWV%f z5*Hx^_=88;#<4rAPT}t>ee&b4>rJWFSUW4{l zn%j<&IObWp4cL8zd!9q)w>Fv`K>2Xv;DQUaUlA)^0EZOK2hd)nW|T67h=YTW*DW!!dF@ZB^?vDVqo6j}lj}ECQ!>7g&+;{4?RK{&*JccG7b6B}JJJ znD4}ECg2PQfolgO&vkQ;x30d*O2K=nCMw3h%!&v|gt>#+(!Iw0Mt_U!l|>+KG7sF- z>Q+7?_7Dm75*1$Zlh29t8jZ?IVe2ap1v=WbGM_LXd*78VFrI?nzLGl>fH~FZBAen+ zI${T&Dd54Y`bsE;Ex@IyP}(L~98JS3Qt$igJbd{I=KDxA`tzFiC_u8>K>vyCl$6UM zrLp}{IG@qgfwzrdmeVcMcMXey^z6aAR|2h3gWp(4R;Ika-&utsUxLEz79f&cIF7#H zU?c&!^DdIbch2Ei{3NhGKG7GUF0?jVn%L}=SO?D*DAgp+J!SYlH{E;EUm@g(o|oLym9g0GlnWdFf79%4rC2L7jzsM6#rpeWj)v z;009y|4K62zx5RV?5!?z5GT+7&dHs(QKnf%)gKgO(pl9JB-zsEWhY^6T!DW@if4OH zm%navFvDKA?V$L@#w;W9VDKFXl2?^HcyOW~tv+lJyem??^KZ4l|I}E|wV~nes@A8K zDS>@~6l0;u1lz@)$@O=^bL>th{4c~-FPZ`Z%d7_v`I zdje4?0w1yZwP!0KD8|_qCsBCYSky+{iAEsaW%F%xaCT?ch1>f&Fj!njOwJ)Qc8=sj zifappAmfS3TJ@L%hyq~*Dy9t5f41^DsP`2oblf8LNy$C*hQ5#u&+G_fq|`fXB#Hxy7^1ZG3@UVyB9IGCQmY9 zj8E2kO|C-Qp6UL0s8Sjo1l1GG^mL~rZ{TdP$2}as*cR-w#6Y;OeMY=0>rBQFj7r%z zgO{AHYz?o??GBc%qL)!dJ4|osc$Vk5UHe`MMqgl>;Bsfqg@Vmte~3nMGY_c<3!S&> zEBAIC%&qxUD=ue~`6h%Qt3891M1v!2(Q>l0-3@bfjTa@*%0^uckMlD)f#br^9*%|8Ks4;W3{j^X;z5QXu zJqoP_qUy3HNi6Z8E%OaXDrN|~_lqDu5e!3aCV9sF^aR#ocV)Snbz0PAgS<8S~V@xMx+3 zHj>`_GjKi^_stgV1IFD0u#T!)NmZP&#SYMgofFWt)dj)pIA7%s$JZWUSV)i{qT;v^ zNj!(q_&IJ+Q=IsK)M{S4CD65i(^rOr1INs%_wPp zY-l1?9Tz*o%u=gq;Jk`Fvdq&8-Qfj==}+=%&F3VdC?Uj^{tiNU_wxJXmI}gwS@frk z^Vi=9-znOUi1D36N|=9@b$`4t#qUSUdnIx^G~6 z(0S9{GdRG#QL~0$*7f^$Ag}0FCpX}_F2M8wMw=R-6Fn$}x;$iK2ud43k*pH9=sH?< zJno(cvO7=Y!K0zfzxpW9lnVP6269w-_JJZ6UOS*@0%5{Lz2+681=wBK{>3FiyZ?ha zvlVX3(c^~{fs}0qLoMUAfq1=nb8UA332CjIfl0mE*1OdpG$Z`-a7Txdx3--IG%EqP zgyaD^!_dlejQoAYe24vA=#?D}cYJeT?RzdU1Sak=WoBHG` zT*LNu8g(^)FGG8#p?qpX3v8Nrb%z#n9*k8Q(3|`NzWAE*WY3R7Hk0q(1#9jH<^gV0 zxqrmK(d5)>>xK^BWp{ix1^}Qq;3{jD9&3!H%mAAoIY@Ev5WV2U%-VQ z<4}asuDms0P~AkF8G)_A4M;xjAZ=z_;OECLZ__DyOg_Quy8Gft+MBdNJmeyc-J$*` z>-w#jv_v+fBd!H5Km0~`c2DREn=>T|L{Gr~dDVG#x-P zA!B&+u?sgyMB0M`r*&I1a9OD&ZbWAXdSZ-ofr(v_Z~m4C@}amccmY%67qRclJ)06* zV%x&fY_>pke+vRmLoSfblz>m`xJhdf_x=#du$|@GeBa4YvU>}Npm7rXTZJI_1;KnF z0&u>E7>l9(A)fm%H^I<_BjJDCG5@|2_I!#`)(|syV-^1AD)A%n&I)3C@*gyW7Bx0E zyCe%UV!bi>?pfH5(A8zchcULGhlyg?Twiw^dBd>QHuA=D4cWVhm*$wzn&}nIjGxEE znrM5zmn-@Em=e@CZ!TM z%UGB7wh80%T18$9>siho{Iv@xT+n)=bN#jeC;g*qMR0-$ctc=k!s8m}qKGHVL3~;| z+S)HhHTYissEc#V{@QLOV`!@|v835%$?WU~(vc!{G$?_a^$4A4-1Df2_b*dOsoJ)}}%|%#@zh}c$IrI0&s-wb>{`{`7pYLK5 z8`b6)I1Zy!dOFoUxwRaY`~~u;n7YK^ClLXLSr=o>MTr(T$U%S(iO>JGwleP=BHRgE z;&MD;n;q`lXU@<*NQ*N;xjct=8_n)(H|M1dr1_hSc`ADykKF=kW-jnST~wNvv?qp_ z{wm*8uii_`$F7V!6AmEjd35uaF?;jn+tOfFfhgUu#{71uV+fn1&O|wrP`GASS{1bo z18JQGKMD}##rA;8z1D1H@{wl?j{PJkvgufR5NGj)}+NLZ*H-_ zVq|(1#u)clVN+qTdfFL`XyNKbNm;CD=Z8fm`3`Yxlj7CRWh+I?z>^m9{ddpjm;zQn>Mgylb_)-dBm_m(_= zI^hOI(0IsDkYvWYMPN}Yv!T*33w41ucz3uYdEcS?defXL^pSfI`82zdow)1^q{+t{at}*~vDYpgQDP z63rRvU&h-%4RRLhvno!JLjsTM(!;1nd@-&0j;T7t+xeP0+A8mvV!}_k^QI@G{7DRI zNt)~8)a&i+e!uk}eZ2G$@5tv7Ta*c3a9sL;BmA^Npy5%rB-SSAAQjU(xvCs88XP0` z3cef#KwnN@#0^L`{>g1_a0;l-I^DXUinsG$z7{1OFR%8W@xi|L&hE*9eF0B1c?}=F zgL)2@kPkq|(ecu1*B|#YwhDdZKfQ{+sb#Lm{~%SgK}NSH#)0&MNTvTIz)kqax>PdU zlx?IYA)Mw}oLx^u*?_g)+UpxmKe2)T?&#fqA>E;c zS4D*F`G(%NN0jLKL*G>q;YMYv09*j#RMn~9t;N5+cp!1--cCFfO(}%#?1c|t8NsuE z-H?1!=D{6C%X;2stk!k0Ps=h_o3sMHiKK00Jf%MH3}*&|1mog4Bk$KYWu4cq{&JxD zVo$L+yXR>g&co9T#2!fUg7pikz*m6_Ll!8PWZ{dzecZ@YLK3gB196a!M-5TvA$I~@ zV~!3O_ZQF_8%b%!s2sz0(vkEGz_jG@kh}YfsATBAwd1d&So^tEU_aZl{?#6-Z_(lx zo%^487wY($p5Rg|Qa$7f+`R?_)x=);imd9j-({#=0tM zTYgrz&8{ToLH7sFA=$Too>SY1^6&)K=*2P%{ygjzeQlHJg^TDLvmRIfqM%qkWC3}v z$%oSJD@#9ipkSdhR9=?83C?9_`DT;*JXlv+>au$P7$?P4?%JiSWWMaa?dvV&d-D*% zq3@6IhpwK0dw4>on!H?G8AA(dnXvcl9uK}(P3{6Nl0$*Itm{+~Ff%I*%L7)D3+bAU z?1zCG6DAzO(+a_)*~g|}RK%}E&a`!@29~ZWi=;$LsNtMLT5e|g#=$R=FS!y(iAO&l z@T&|d+~DYKj|Cb*1n`$4{9lVjy(?(w?ERj|JZYD5?QXeF^QxK`ng|bV+Ywl-TkbGz zURqxN%yx#L=uH*xO4v6}{1H_U(NsN%=Dxg#>mxDinD+7%XqE!3B+sr`9*U$p8tm3I z*?C7bh~}?Q_(f@CrKPb_hl}-t%=TMPcUcMw>0`{55+qBpEEoD5p>=qG;CdnVo~-;_ zQ`4VoDpx!$vRwvN07L`&fMkUpGI6+4Jlkt`C}SvxU1|rLPp$r+`886QEl84d?!PH3 zgLAPQZ9XJ^n29dsGPT37%C2xJfhQyAk`1tegkHf=nwV$AaVXn4m))Z82({=vmQ}=f z^XHqY0H;2H$jvt(R~}p4W!dv4$Zio|KEn$A?1Bz9J9E)kh5J?#19~rfH?h^|&_<=K zUNH{!r;!+p z%RsiZrNS?0S;X*bH|&;&Yt|iByD#NO!3!T#u51LhzJJr>XPYgmi+;Zk2fjkg46F-a z7G^z$_lYqfD^DayT#w_QoJI;-93Qc1oAYZ8Xghe^h|%(JEMSwQEUS&m`f4uOMO$dq z^Nf`dy=Bmm630pN*Bk%0MDXAClUjuuNPCd_@M=qyXJ3#*>)3r0&U;6q^Jt-Uo^JG3 ztz(i!dBpdRR&R@1nrxk?vn40Wg6``+5N6)Psd`*7_keJ$>hK(r+hHuHs%*RWaiMDF zA7R4(AMU+&T2oC*@ruZ@^Og8;a1O!at@%zYEAV!CUcD`nh0WHy(6}-Z-Yy!wfpDt8 zi$53`JtYz2Y1tpXhS0uevVW(~)b8kHymy?JlI@wS^H-~Nzh@Cf?@t$-0zMo&b?>}p?7Hh5j%o8*7%zS4?&o}2&fJds+4$@k)C6V#3&<{m`$d*6+ zUs^pUu(+Bz9^oFVIo)zOOz~8!nph*-&NjZ4^(SR^<4+En-&iSz|3hK9&rM8Z0{cD& z&uL_d^71&czWMN89q)pryLlQE5~*tX@diFf+9V}QJAz>dN@Zk&DvyEwFOQ4~EmyYV8B|R}gA`yylsC=Nj`tw0p z8e^(cRnXrg><_8^A9PcXI(YG=3{gE->0%W$5)(uRN@Hp^mv(&iM*8Ng&eA*|u~Dk5 zM%K}UlXSN3SsJhRLT#(YPd)(r(wq(HQ9wt_6X#6DDtm8D+BJU`aoIE-kC3!f7 zsIw05^DH?UJEnUdZ!Bm=yq4q*n zoN}$?1aFI%BW5S8`lvJRcAW0_UVZ#Z;0$27erqoIk8X6Fc>6MRLzPs& z`BI|W6&6lL##S2?tV%JBm1t$}g6Xjn(cFG+A`xFG#>LmpiLk#Vlp6m-!GPJ0?FGQC z5xqAlBp7Ss^A?^Yo=fmHcEC0j( zm(}r1r}eQwPK1{Gg)!VZq$``km@f=Q%=9n9e2B+SecDwm$D6^axY|GoL} zA1LotNC4Fu;(4Pz@}aZ^v%Hh06be;$w=hqG*eI-jyg}fancLKw*cZ1A*%#GB^bVoK zhsM41N_8oZ-7!O*Zs2BH=;|^JE3N5UP+Yx5{^+ZMCn(`Y#ynfI(8U!GV<1o(ydZsU|7Tjz?c-xxJ zhmIZszRkYFyv~?Em0e$NJc=nfMr&ONRDa{b_vtf1>ztOv7znkSQQdpk z(3K~*`xDH!W2?9^K^-2BQWo#;^d31nmq@ebTRQH6hQ%af4p!Vz_?K1dS! zsmpPG$moBOB=|2R6?uH5(C`0MQUWtGQ`e%dji_>CC*Thvg%@83_Kgl_DN55*M6Sycw<2QZbL+mz)T@rw{+}kOB<&bh08Pa(bZ(fWJ zL|u5o6K{z{-Ej$o(*EJjNG)KXIC`J5m|U{CPIEwnq=3_c)xj+%JuTx|C(i)?BlWQh z1zc}(FULQ^0EYZ}$27{LyW)yFVYo7>jn}ZXl{!Pa@|d~>MDMb36G1q^R#z*yHrX=LKgB-2$~q8(!@Mmw8oI@oJzN zteQ`&!%ojY+?aOVyK3=S=Hy!+YwRtY_81Z@yNT+8A~>;O7I`g;LJ{}`2fX-S4@`Iz zMGJ+2EqUN@h(8-u4bwn`!etvI7RKL}$Coai(eY~N3SE~{zLc-zB6U!wK+0XZ2FSQb z_Gg_oFePBmgAev(RRy~2aOc^_=ZifW&#|)9gcUToTZtP&9&MP9K@>n|?VEp~e46uG zEakq)4XXD8NqNt*F(efvm=RmdO!>@W#strLMM{V*V>RlNgKKG1U=50!2bcDME6~A% z9*z){1UX#6Ue*S@6-_^#x3Q11ZO(!i^n1wVa&NA ztvl`t3rdCcv5W1afZFQiZ;*Sc(v$~F4=qs{(Ukivz_mkxRPrS_5$=QsrRuvlsI3@l zHE$~kGn(HX7#^76QCGc`cY%pkgVr=+2yulUeB_<+cRq8^@krTH@x*+DvQ?#oao?81 z^z$oBVmaa@^|@$gs05v2B30y-Y;7Y1(Ia~&i~}$}(@|&sqS)|> zzC}8XV>kLb&T1zUzCm&q(9aaly-k16i{Agv$8MX0y<%6m(A#g2aA8z#V|xRt+b;pW zKJ_3R>16s1vd@r$tcUkgyf;RvV+Q?__vZ5(*fUlZFFc*$(wY@0?}>NA#xI~@Q@W+sdgb*q z^t)ds!^<=2FjZdug#N%%^*+DDROzvsjULt=8+EOF?h3qjzxZ3{75F;rB}P`!CKBJa zxKu(GLQJe|CsDw1@rqbW({Ngx=Aszy%U1*GNmVm1a9x*9S4y%mR;E=uYnw_pk!ArX ztFIGju9)-Fi^-_ShU^BE!wLKwM2S`S8-xgiK_?2XqowII@@($Ktn+T}dJKMpoS2$U zAGM(a51AGD#fIY?cxH)p3H;SNfRG-LdENP zpt!)&<8c{BmjFeRr&n0UZKoGEfj57zxutPC1Q&RO`h+ff@2Y%*C>RfdImz!U4xF7g z$pN=j10K2q|2~%KM2{22gT%w*dH`}hj6lHGZicF**b^hh&k|!U2-o8+)w_$UhrRR zn`r!ZOEKkcwu=wTor<5Bq~;ty%<1X@%lcCb(?Mkl^R85e~Bt0N(< zx>(5>Crt7E>keP}r6dMMX)O*_a$k+2J97tb@jL6hAVN2_m~kJ1XLV4dM<~?wvFzK( zNcMsjEEB6lABO8;XxqE`saBK#7@Pm`L~t}L1opJ6Ao+wKajS6ZNvVd7Q~p>!lpiZF zM*Vr~G@?fP+*^KAf>o+Ga~XWpXI6Yxfue3J9GX%oC;Nu9MNEGMIBkW7Wph75gGKzy z7;apsX~Pe>B~xpVQFH3E53BqPgvWP_d?g2x&Sl{GzTB|3`Tm21yeIJ{%2qObWS=f* zx0UgF#ss!!V@k46Ui%aTG>>l(@HSXjxo=>f_C7y?HSLw8H1JsvsB-Ym^72&d2-G|+ zlY1pCeukN+<#-MnFuC=1D#m&3dE&N6RHF5G)Qx85e8&74XNxdTTB|Hs^fn`Tpc1A0 z+MKd)5F}j*>Jz99b;LA3y$`ShI{o0M)h{BU-ymz_zHT5RIR}XH>v93E^slw&=;(b| z6$FT%&-*S}z63~V#BQ9f3q5H}X+X4dqO)Geh|Jv!F$k*!>gzpS^`COtNIg3k7ADnQTpfX)4_xXY6zc&X}-azeU&ZqHJEZ$9+ zWf9w<8s6P!dNf5Yqg0Pyf9%wD%Y8g8$~27-kn#X1GSaQiB-k{#xoz8e1BtPbO!O(% zhP2KdByo{)?xj$fOOdXS^Sp3E`fA3_=CdTXTN?jq7{&8_(pI z#SxBRzQS9APtd9SM_$C`(xA+Qa4nGyprt!rtATDenMMkygJ;X>WO!xyL%Q+HO7FYE zYKh5-tf2mv@FoG3wV)qW_|K9^fK&gn2oUegRyC}hjrB0f7;|)6_)Zt)dhQW>b=}t@ z!`|;Jqe!4}R<&`rx=tO$;`*3?S6+X0PC4t`;Z2S_%xO1rJ8GWEQ|(hD)(NQ2RD5Z3P0gE;YNWnZCQKNW)ZlfSz3Kr6rhF9^VCJH_DV z5_%_ZG1N#{#wXq@e0Z|!$9Lo8uHR+SyWCdNf@RSX640+ep)L`jVnU~kqU_n)-TAP5 zn8>y{JZOB(?6`%pOhM)gi7r-7vt&2yXid)|dqn|S&-#WFD7*dkh9h{w z`)gif%W;bLPA>T9pb@`@7@o4T$@JmfwxPG7qTu1*V zF(n>dKMvjBC+2)A{_TPJJuyCiFF#PuVKyDB>gwu1%3w_IByXrw83Nk|@o`5Z^%sl{IXH_=<(b&X>{guAH^7JEPT< z@lGjd7Zy%Nm8zxbP;&QUhi=(ymmtksko{;qH`<@+VA`nsq1uvhd$~Sy$k$`vZIJ!N zoJ68wZ^D)mpEm?9qppYlaCscB^wl)580d^)V(Kdr(WJG^>srr1!x;M@xz8@9Gm=2^ zA!HyXaih$Q;|>wB%-!+d;wG4YV}Y!6Iv*T|N^(&} zus5sPqN6F5bLD1Z2jZHvJ0iu3QcqHu6O%D2)TCnN`|SuMz-J$UjlMLl^qHhv^`JH^ z8(|{pB1;p))un+6H67-_;v^q{YQE9!H47t_?>mR-*>M5(-<*5qJePc-U?uvqBP2)w zsv%1B@g+ujqUcKLFdalpY8c{Bd_`i!^kx!JW5Nmw@>`h~N{S=0Q#`C`BgV>w%O-nz z+yML*K;?3gzgRMwOhC*}0mAo)9wtQ*Dvt9uJYw6%F|J22mU5MkkJxUmR0j_nK>nk4%&Yf>wQLl&C>Rhx*s4%INu6WEEnzh&j)y{RhQ*Y6bsw8vTyc&= z6vsb!990XQiTkoW35YQC022GvCX&fy0J67o^k~@4n?ut}4_?62K^_`o_xxGL6=(U9 z375N7E+8xr?3{}{z5eXRw*xyp3|ml1{#7Fi)AR6568!X@DbOk;8VTU1e5pq8t-BrK zJ4rx!IA;GBCP7FBQq-rbIv~y~1-u=@V|era^i%_GS6M)1|)dUUNf=5{~=rGU$k9Uz*Cq~zO2F_ixVc=h>>&GkL zfTMsM-xDZfw7ChxY<%SHyrV#nrAmXj9QxNfO?@26yBmY93csfEd`U!G>v3#=;KB-h zr0jSBadGf{GnL0(fZLQ5D}`vtN>5$~On~|%IKl}hckh&ZCB;_9j)Ti|xaNUwXjNl` z4%&Mr*|Sm1*1Rn~k$u=Zy~bywvoeV1TgwPhcf!C9=r?-6fo}enfL9Vd|4v5Gm^*&OEzOW_lTPJcSlezQZ ztNV>odC7whB7oWW>f2@9QOoDSy!?m)f(rrd)h3*$L=?hAkwPcILu*2rn)C6=cukz< z4~a(R;<9mKsWzUXv5sziUatoH>X!Jd;JZT7L613^M+>YD1)WWG$(Ir=%DseT?yS+Z zWQvgn>QkFE$&Zf%{`)SD_U9M_wGt#_%K#cI6)EiOvl*8sX3n)&FHZbz-|R;=SWk%>SL^s@V@al)09__5%3;be&IF zs~>l;s`1+UtYT9uC}&8mXD`x7wv#!tVW|7dj%edrsCZ~EYYbmnk&i-iz}JP(euU$e zocaiYwo?F*z3MfTSLzC`J$7T+aSpOu81-_r@h=|{Jzj)oidJr}`B{T1NS{DTLEgImQdlxezK2}iB1S6ls+s; z$Sslf)jbzHw?mJ>d-59>D88`Se|<|wOUaCBvJgkC@t9pLd+5%i*Ox-0&;zk1)$c%m zu}}IOr(n|48TrktRwSvuyKR{b1-!R-Cz=HdX`kdR9qy{<`7v>!KpYV5+-*2(Wu z)t?-&Fk4^51nPPvqB?76UoUl_>OmSph5QP8jhh6K14{+g=j%E)dg1-d*S$FaKew#Q z7%Aai}N0WWYAp546|}4kQcBuwDq9vlmz5NV5We)_^Q%%Dq0jv|5zw`rRj}+|Ls_VO z4ME;9r0u2m<$PFrvE>9p(AlvVk)aqhdG^TnLPXi2K_PWq=pgiXc!y{Ic*T);&vX?_ z?8LEc7+F{!q7uzr7qQf*d;-JhfbT?aJVDqw2#3KM?i_-juLb*+e@|NnhB;Nrv*hxu zZxEGV)pZJ>u21Loo>cp#!Dq_i;X4!RKw!-W{UG#9T2MDaLH?Vc3eJTh5io`(vSV>O zZhQ$^(0xBXjeHYyY9oeKv{B2Vb9;62mtj-kIMUoiu5U!czoZg^H*&qmEE^yCgjyb& zi%+tPcgUTtUCoY|G^FmY6qx~&smhKD zcD!9Iv`O#UFw^jG#Q@)j<~7ZwaEmXsJocWZ5`W_VufsbgMsoMoS1gh3_Yu7eyl#wl zWqqnDUYaVH8u2|{T{4t-*Y`=bpeftlZL<12fk^nQ9`9$r4d5niKz2ViFe4b!LDCFv zh)HKxh1oBSP@nL67(S(V z<)Ik*b-T~?b0Z8#&KG)g`j|YsH3w`t_HUzYF5uR{<#>^8zEH=7+!dbBnu9&m+8j6P zZp>x{QOKHkhM0O;p>8OUN#~3ub6PX$Jy0KW7ZGsS$1yGc6rA+Nz;iuQRGChhECs58 z?`TTK_mfrbvs~oVKN9%*`#JD0gUAaU+d6>1jPFFd_&xWTYE*(t)t!+0Ah$>%fsGKoPqvbCqa4z*t?XU`VY?j`2e zG;2C(FgQG+{ssxW1Vh`0OUw9M6gtDqljyI!F%=Afh9oSAwox!;i4^#Kv5a0qo=Y)f z0)ht=4AM6lo+^_ovm#2!`szAK3$O}kUNbj^wUPo(sR5<}m|^PF`~64WCl*Ml69azb zGl_e7Y=-L9J3S8*@VG?y49MSTY8kl5VjRvsGA@NePfsa;UD#&|gFQra zVKB6esY;rVMErD1k_m*KV;$(>U+Z#{!w!OqfMT7!AntPX?ulC>ta0cCT9zJla{l&< z&S&bg6>4dQJdj)`C$I<0e7buLE35P{&(5w4+|DBz3X;Od&3e=?eAelAm7?>7U+a?<~9bP1J{9j$C4k8#NXaL2>@SN zZUOr#e?R7LkOOZ$tUwrM9{BR! zy1DsIso0!&2U}R)hf%@0k+i-sE-`qPE8!cXDP%cw4+y^!VcVyo!~@~5&vsOeY)P9$ zEudgZYj>M$C3DD_EU^^orWRsvDm0>Z%H)=sQlGw=E0;S_h@IRFc%bFC<{$I7`~LLKnp>??v8n!=SNFXQpRB*R=CTc z=kb9P8g=Z4jyCBfV9&*ZUVmD|#dbSU%5SApH#PuH8%mYffi#mi0x(t|v6JwX=8XvGpJ*pzFpkK-dF!<%o|*CpL|X{ev>56ij6TJl%qx2OJf%{4!o5#8{*ArLO=TJKuR*(HL}jjV z$Yo+@n`~Siaj1ei)nMjWr6f%+%U zJ3T|`FRb2RnmwMl!dk$`@PvAHERp?D=hx@`pJa0!#!AM{D_t8T!<6>D`zQ^P$M(Pvy;k~XaEvMmr*)fdZMgm_9*fa#1z#cZ0dt@0# zAz)$2vb<4cWdXh1xsykJ45W1h)epyCf6gr6*nz!Ui789z*lBb z(S#puMHwSMFo8Yvxej{HYlCDCd_UKgR&KCOEqVP70(in(rg-ASG{oSX>8tz(6PvRp$D>RPYvC@F)$o#JX4WxrjB02gMgO;A^i=K2HM&H zzzNlUj3%>P;wLY^L6*ea4Eh9&_PW{@{?+Z)c8Qz=u2>}dS8|g(vwqm3}nNPiu^0R)CvS2*RoQMa{-r8p{-ebr0 zyJ(X-9lNJ%@9WtBQa3rJVHWkQ{^|6!#>HHp4LU)ow&Y~jL zaU1qLNSqqsR)TqnGeOgNwj8Nk+M=BZ{2eh*&Fum#L_q5sD9{HiNNe!}O!2 z+#6|X-0naJ6~y5J-kBg+ZA~*8F4McWw3Tnu>ov{e#vx=jCH-kX{Fyn?l#z^^z0%Y3 zE$aP<-ZyJ10g_|ZP2)q_3c||w$&22wPTW;~rU+4#lel=9PGE{XD^6tx#~P9C`@YUL zVnx52sH1=>mvKh&;jSuG7*~#|OWxY$&OYZ>sv0TTL77nx`7&!&AyY;Lbx{Zg@7~Ow z*GdlaiS*uVOx=a(W)V*UYBs1;Q7H6&YGy&6{e`#XH>uBJyT@=;ZuVfgUc#ZI;O)Q$ z`h&5r`@&#qyAwGE5uMm$F&+zny2k|HxnK$CJ^qo`%R1Q~MEUP`m2hyoQ7MqC%a*?n zXqCt_x0m}U`kG>B*Ei{kC86j5&(-`Rzsl&Tn2pU{;THFqEv`$hgt!x^XwSEMBQJcO zvt;r9j=O4Z0nE-4iuy-WtLHDK)*K(g3vIhsbzi?vv;MkW{eWV`JdIM7=*)>BrsWd(1KUSK~j!$)xs?HiltFhdfh3}MR74ZqM|zks@GRg%5DC~ zZ^hU;4kO$t8`#>Ie67c(&H+gQ{Z;E(4B0pL|l31HeKD&E215PZ9}oYrmz|k z+cRM7n~NBp_pHDWk$7gNhtJtK?hZNSp@d^NOffxT?0kY2{CfH%IYxXXSrw!^(K!)N z{!)}RMCjdiA zXE`G~gcU~_Yz+3_In5Qg(w*?8_%YoCR`siE_F_gNE_Ff_d<1J8J3 z1X@JmVZB@e*L(vC;p8tve9m37T@ZdEKtKpON3c$9R`$4Hh+Vp;%9G5 z+mYOx2#{>&?Z@txA~B@bT~+hjJmx!j6PWk>*-}8Y^;l!O)D}Y7uWm-J)@e)KX&@;tDTGw8HCmOtT%V@7H@vkE+}?5 z&;Ol|Kz#wq*tuc$Q1oXN+4;VXMln0CbN6NZ1Og~6qhy+`-~#kD!;LhP{?v+$mihN> zS-n0~>uKauB~JJ4(&mG-{?m=XE5*E5&kxu3fc8 z`%qOU`kMy*o#Frfq=6>9Sp^oDw3VvwHJ`0@y=x4IKZc>OD9GDODm|naHzq#k^DZRz z&ebnHa^b-Ajsi>1_6%$yEn2Ot;Fvxw%G&Sm7k0c^>=ZWEU3Q84Uwn%}$eR_a*RT}j&C@Zzql5_-K}U5GLK?Q=gl6wt&?gN&sfT|3(XdeytORUdHbSyn+zGV)}JI8Qp0YwXzHm z9rM%WHuZ!=h<@h6$oK}in|C+VPyeSYIaMpl4PX1*A}4I<23syHooExeSHCrhvleu* zBxp_w!$5CN-F=oyB|tKn5h>$>Y5-#}xerTlp)h;T9cEno(JMBKv2=|)#U?*L_p_#33ik@QUG=NGJ@^ho-er-( zD*%K0SeWM}?b@si;Uy(C(w2~)YjUr#aA7`PQ+{+1HIY)(zuh@@eW9mP;mL)~_D79v z6JJ0s9yD?j&vdIhPgd|09T-5R;y-(^9*Nb>82gdljZdf87xvPi+^2+!Yr-kW^@=f+ z2@UD*^qbE-4Pp7^`CRv94rCk)P=1@?T}b+$Q@~EFXUH_Ila7J~{9T0p?xe}bAGhVx zCT~^t?B;#ZV$lV$VTYMBGB+%?1Ogda7JK|j5T9eSmVB!?i$2}|W5C20DR0(TWO#Vf zmQq`j2M&+YyuL^-_iP}|k3NOpTmkR!OZvrM0YbBdKaFb->Lc?)6ex7q7r$Jiw|Jw1xe`8So`Tuo7lOI#niUxsd35Sd`OaXluw3JydjT$o^6|9DBt&m>6 zTFAC{Rn9ug?X7^M6v<{aFqaeoKt@5schtx7_zBpm>>nfc&r41e><-s$&I4(L-PfUh zLz-%`kX3U2gPz@o?AS*$0WzDnXF?y(DPLc`aoXjVidzs#iW3w`px&~6Q%l2|zb!iQ z`%?P{PL2OkfNK#&BS9*Za$Oiva>MXq&ZllcIX~P$d)p3%=%qyj*!_z+xtDH|bgX^V zi#PPxAR6N&7`XbOQ#?vHM=h{+UPnH)QY;`yfmdIIChzIrX=QFVOnk3$C{4iJ4k=c{b3Lzz=O!RhFx8*36ov+xMl>dWRi?iFG&^0bggWmQ z0iI(`4MmA|J6Nt29l#7>eUjdC18t=`!|r<4{X*--kDe8Zl3ZzW=`GMTSB|>Zg+8p_ zcyOHpJ<1lefd@K8;f7~o%V>87Ox=z`p!3*`8=eoY9p^7Hlulj8*F@2)=bcx$+E2^Q z%6Od}5`Y_gFUSA7K)MWO9tkA(JOnZgB7zR`WVge6^=z{~-ylU;xWsGD61zVxxlt62 zS{J2d6UkW_%bq?bjRPmgk1sXY&lMpnp2WSW>%xu^G#4NE0Lo4-XWTzHAkEsdtts_L z&ph}Fl;Zt5zeH+ zv0a<`I5EDb!+A)JJq{SsC3thf*)0GHEm=5K{B) zWFK8JpHl^DMG{!+e*>pAJ!czt8^;kT0vn8Z7;{;LCB#fnjwmNHBg20wH^rKYIYJs+ zGI=WO5hE>F_s8DfAf^1pj;Ahq)88QKhB_d*Fkl5Bto6(VK9LA8NGBn4jmN#9snct? ztFKU#BOc!%LplT?Hp~x{L1#X@ax$5g8T+n24K{edA5-Ew`Eb=*?qtK3v~_a#%P#qIb4>t!Zt3T~y@3+6M= zeS3WHue*|YRdwfMkSgO(f2odrQ$58QO`DtYdRG|T$X7^|^#s;M5#uYq;NX8hkdiLW zD#L$a98F7$(l0w}{dBOWXX&%IJ6co#_H$rRAZVXl<#iFRd$=-Hpw89D2N8PYFN^k^ zk?Xs}&|iQz2c6kaymZA08YosQz?+|v_(@6C^ljEPR|c5xPKh!+Lq7^5 z)jRu=8c0=h>T9j@#MI663)4?Gq&V25pP1ZheCP+U@w}z+_);R1q8m}hMBElLS@jd? zfZ4hJR$ejIj&-i1sHTS_hTK+C(JOeWh89V`k)FRA?Z4m5{N&gZ{aN$EE5ekvX!}s{ z)+HH}l_O_q<-`0CYnwj$XGKOvGQL_d!*<4c@%3ffw3AzAm#*Mb(A$W!LC?Q|2h(}< z4I=jGnpc6RwYUnZ*JBO0sOZr7a@QVBFQ!(P8*^CdMOWV+C9hKL^DECOnh>R4-deI< zxTO*jwXpJLf?}Kn5uIdR;fPI|*ZxMR_zi=G|Ws|#f1ZEi2F^YAiHB^V_v6nJG6yfp6 zm4AZ9F z(PkSzfDf?4plvo=B&jsFEt1E?@G6UF9l0qJz~))Z_-dO$vow+DkpkTL*uBSI05i^m zg%;)J^YS&LWBa=R(ib4T{~R|xwFP0Gm$^Xx=S zp$VT7Q*m2%17agtxp^P%3~06DS2#i6HSZR6#SaOs(ZvlTPB`wxf5A#9)mQ3_EuaF5 z#!)29rcN)?iaxmgYb2979XXK=W+X6}=ZFDx-BC8)6Z29`!-tWW%Yh#hpJ_lx&-5+; z@mOO@M2`_S%k$YUB;_&RP!FbNmoQl1 z(m@^4qg~225F2y#V~HcT6kqGRUE6!U#w9V;1-P0GD*F)#+}p5l#J)r-Y0t2|aQe-i zyjcjHD4l?rVvFiE-9GQjpE6T`=io8CSyl8FJNiqP>7uv2ikDURIlBc&ONTTyqjOuc z?}iO1e{H|=4H9x5!#Sx6d?jyzGa#!G8UzCVXQwp>lCI};wx1N3P+woGRGt}M4MdG)cxW+ywImP@F2ya$uO zDCiF(e056B>j3^{m17HikzX!$rjgbew}&x5ZWE``q$os7y0({z)Sml>=!3DwHaV_Z_ri{D8BmbDAR zG5bFV_~A-NByeo>o9w=53x!Se;>YE*h)hzldp*z45*mM3#sNw^wcla(Euq?YOYya8 z;BL+bpO2=Lwy)Y=Gj$sXq7~j5?a{y4jIi7b_Is% zaBc)>XlajV$=j7lUb4_L3NO>T(ZwVQi7M5ocPDh6)3;NMmXaV-;Job9E$VoZ*^}}{ zRAn)xVe#S$4wF{@?9c}Mi=7x{fB&HW7{RlPBO42^I$j12E-o~ecUZ<@t{Ja?&7C@> zU)QXp)&liA)DPAW5OtMVUmV-X-}e7^R+aAu#5<@%sk%+ZSAN4QVJt$iyvL}-6(olQ|Bi2 zj_e@-oL>=VXq?0EdjqQ0VY+$v%L!6j*eok#GUJCdM+?@@N|e2pLKwq{8I z#vb;;DU-L8TfFWKieVvwrkwOoQ}73VA5Wd`GK|DJ7ITcur>lT=FL5C6Rs?L0uFm8S z!v+`bIOZtkvdxD6Y)l}p!vZxuyFhe;Q|eA5M#;}L+dp`TQ+O9en-$S&p|!)?>pOnKj}f^RN3~ zGIVLgm>c7eyqDLK~kg1zaXu*dDw6xt=?l!TjD)ceZDlHm5VrY-*>)(`T z2{YPST3zF1v1es#I=gbdEb33Z^7q~Bz3TRK;1o6uMlNo3haD+JI_LcBqfS9d2B>u9 zGYoqxSO<)_+NUZtn)JO8;MG~h! zxEyT6Lv>J1TT!5v_yV6NK3n@K^i)xfEMFUl$RG95KfE&e=fh(M#J6n$EMnkYe!*5@ zqvq0s!kERGBlnRy1!nORH;p4oKGRBFtNsq+uLkjx9vh~m_r%=#i`sx?WGs+TYAUcG z6YU_U&wNNB|LrV+(qC}QANl96SK128@a_BiwaXWa7*9xF(Y|6QoaQrXlPqlhT6?)_ zu(m{I-$Y%+NHHhaDp}~6!9r#KRhH*h$QUquema0c;IaGp*uHRXt)rateYI4B>Ph)N)+$S0`M%4LpJhQ$P0#B{zT`(w6RF$U zMCpVs5Qs3VJPSb&t{nwXNH;a&N9Olbay|obIQ(ujL%d^fD`hWW#f19AuA6%?D2QoR zYp-0f=GsMA)lW9#;XBhC!|a=T*F*#n`@}-2M_MMcL6=3MWX|joA{6klfAWv_PtSt| zs;Rd6uLBAz4v-)Z5WYy~QR>O#%wC?c`w4LRg+$A=?jyrD!SuRQ$W8X@A}G*(@X# zN68X1JFFO67Sk0^l6AWxn3b2BGyedmSkgOBSgv5m7rv0|2wZ$9){iDf^OXDIKtsx- z42vrO)lE|BePw6-GR~M4f;*mRo2t(qKP8jHIo6~d3atvuBN7f`yMV_z(9FB(2Wya5 zeUCLVdZ*zctly#DZNDJyq5uw45bnZWBNu9pGo4!>DzwMPOc<;%Sp9NNR8CaXn8$`n}i7oN=`_9{v=7QM2wJ%>6d(g;E;*LV&9 zI)JT4xjZrM&6Qm73ha=1Pdl`mO#~jn+O*gZHGxzvgS$Zal5BiBddAOHf^>#Dc_C-R zqSgvozCp}-=1+hstflOY(!Yx2Yh0-fh0li+q@bKS6b?XIv~mH|WAWh|@*A&#T|)Ho ztQRhEYn_+DbF+9PK9nO!y*pq?nrvU{_E$ODk_UIJ*=oa0{gzaC-p;0BD{VE$jUQ+x zvJC}EU4JZ_eiH*DNuEr}jvzzU`_t#tJiXDIh?}nYur(9Xg52W5+?P|CDPGu-f+xDk zBEk>Z^g}ustc#<@C#XMGRE#}IyX4;o@mEmczx$iX9%vlo>LCcwy9Y+g)v9T`+b_C} zE?*Tm8|LOs)H#s5ZZvts9MyJ}&sa@<+$#Kb4{2A?=);vKuj%9lAFVQ67_nWMSebZT zD?F%khr3tm4ZbPo+e+!HWcF1bS#qa%rNg&BGFg+JS%@U*S#iGfFV!KvZP?lKWx<2} ztXrL-APpyt3g@-79DbkQ_r+HUfK-8mCxhey%P7L-Q`+IR_S8myTqbw_7wKoM27 z(P)i|;DSsWF1b_O*8$jRK{#1A!!Bv5Z>gI$X@q6aKe;CnlF$YH%^-j8TYpr=J@KBM z($+bHPKjB7gC?DVkNFjo=yND$WJic4+~}8~d<>&K-{brPD%h(};=34gCWoQ?$Xw6Y zB{g1ycTbhCy`BgV!Ws41x2dBd2V2|>rhdC6sBUiz7X!DK^}FQw$#oKLllG~O(Ppfw zN87mie5RdE{eiRO#NYSd8s2>U>@=}lnDm6ZObci$H)}Z=Fo9(d=2Qi~O`&Xv10JTpoJfq|NQjXriupSozn}YXOSCkJG<- zGQN*3Ce)*sPe-@|G2KfR(1b0yzYv&}tx`UGPOjTGC+vi#L4Mx)Vpz4&naHrsTR$>T zL>H(}=kjO%<7mOODz9ziI+d`Phb;v&de`Y_$(PKI7lWNsx)TqwThx>#krEu zC|yH_YlL5Glj+uH4s3oOOcNB}yt?M;Artc)=zXAh-?s-pp8m-Q`urWF%9&=k;+L4R z7ao^#cU+X|@)@xRil8uF>QmBd@L=w;(KiUF{y?4N0@o3x5*L4M)-uUS%R z;Up(icB;qi=+P+z>HV~?k`=yc8nG1rwZ{F})L*YO;#j}-%qQF8kViQs_86yO#pmWd z$aNCUGSddGmLdBs)fSy?^qy#Gyrw#qWac!}8XhnAx0CvZl44ML**_cOE;UJsF z@Q*PCpjn#lFygk?K0c$zi(Ss=!mL$}B(XBqBRVCZxg0(uh}b7VrXG30HhTB)=4x+e zd~U(Zjy9qV;us|`T}ST%T9;;u=#d?YD#19gjP-rL-HO6R_C+u5bx(-5{LXn{3jCz* zsB(9uA|iLE<^s;d$L!RF*|F%=)pq&WU{*#Y{aG^td?c^?%KNexezlx$F(Ylp6F90! zJQEd2nioGjDk~kW27xF+$C-%gvjC!MlyE*AVFDaaDz}2nh?LD|j|#7AYTdEhR(uhb zGuG8~%`XL1>pK4J#L=1*zS`;>vb@zpouZhZ5H>a!NIcbBrY=c)1~w`hqsab!1JwH$ zz}!MUEWXc?Qlf()q(01OW}V%wn@@eQrvo<+0%2F6;>`LR~t+mcG}st@n^DChxkF{z8;`O^p3X46L-0Q{Bb187BAod8qIy>>RBh#*o= z;?7+;|GB5~Zig+r=#E)ZwEGyOH9MeK-_M*DdGkR-HDiP%HHrp|7NzKK%}f&}s2kqP z1y=11PD?N&xa#ADGU_8ME|Q_f;fs{VK7v$Vt0s)Zq!;~+$KM-|@`t!C6r}{#qWr~| z!;dSgQfW!v=YQvEGW(>tf~uLv=){QsL*8G! zYukAg=}=LkWfzo{q<*+?!|x7Vs%Ofz3k#1hP$;Z&H5V+d>Kgt1Li0>k?W^cHu@_rR zk>x@%7G;_S+54J?`_^WO)WxoO&y&jYvZD^1;(B*NiPi6J+06+0ZyEAkV-xftp4{+f zk`S}_0)sv?@lboBe0XOqkR{qFwwgE*noZbflWy z>=Lr*>AuO%D4y~>vBh!CiG(RE9AFvH6^Bscw4sU6(C!j5m+JjkWH4THdftPW!O@7J zzVL9N%)lu2>#@99j3F4DLH+(F@nrInpYx+%A|5$GwI=-4inNk(3aoF-Bc2wD)rt6i zZ2%r?4nm~nJn#0D;A-dWV+syTtC^qLKi0hqX)Gv<+yy!y9+!ys7+1jB*E);nzJsL* zfTot9M&pV%i!~`!$=yfT3M!c}E#`Pcp*Aqh4Bugp{N1vqsK&^CrDzg?z-7NXrqENm zn^A|!LxGAJfPq!>2n~47@kjQDZ{ypa*&jLoH`yPfoCgd1;ylB+wenFz^KZw^9Ecct zAB=hxC{WkTi3X|9+JS@)9EmrFcEGyCN9(afKm?8qn9(HHDwbee3nhqi^P zC-)r1g=u{b;InfLV=R5|#K?Me{xMr=(-Amqb?Y1GJGwg(@@-X@7>+_#&^Djs&u-2i zmaLh*uOQMoEO*4O4Rhi2AD^pvEUi;Z2NK>tzk`#I&@1Unvv66LMXIv_LH2Wy|Mc;{ zu@;mtx01F3oCrBQt%_|4o__z9nIYfRtf>iURgpW4Ih{`6%%p-k|5L)6-mzgac2^L%y0p#JS&JVCp!T!kj-$9)>Cy%; zE_W_NC4IrS`vOu;4-a{6=jymXvq}OOr+R&9O}!Mg$^cjFXc7aZwx_D0 ztRcEHaA_z-@M#Avb~vtsBv7Ig0I$?GGLW}b?@lfVKP zU6s3!%IpI@8*hV>HL(%QP>GE|djY7i;RYsNOEoJ6jOgu{R-a97s-aov3Yb!cbj+`46P?at&UgRA5)mO9AH zi%*gPlE7~g?Ncdq&EwSp*Q~2mPluh9`N@Ge&^()`Z1D$y?vw zr}Melj9u7AEzo9F{;_1~Lt6Ibb`|D%e!r}k6ZJ^hvN~=;z@({LnFobGNVrWWS!tEfI#dfRlmYbMh_M`tYMRg?TZJ_ zzlujaZS~RYXChl*@0^3dH;hP$B#DtxR%G)*U>cvjJy!hE@u6Oa|J<7Ul}y2n&NJdr z)vZJ{m`vbKq;f^ev-fac4)UxWNWBV!ZW__`MKTQDXYyZ7d4^TfFSdPw?vl`~okiiO zls5;xEM&NEogF#i{Igv{cTx1fE&|5Sd}+H6hNNnfGu-#QISU{vlMW0H&9=flP-_8I zQCz&?K3?!3^apZ#=stmO`dc;DxAT&KR>36O9-(t0ye zm@fgq-2=Dz@q5nzEA68pf0@x_!(C#QpW)Q3q6uuwuNA9LCKI}~Qh?8|j3zz#^VVEU z|I?(^EYE;<$(P*h#SiW~ks@W|K_bsQbjgR%;9K^`@W)3(S#vHC>%UFILcZ6&%Y^N% z+Z6VvO1ol)4*PkDN69zD>M)$Be_AL?$3xf^%6AzUtqWAj+e@e(BKP^G=;pE&b9NGT zKTufpd?D*6pZOzF0-Z(4~4R%k%J zeE;9Xb%Op6V6LOvfpcz=9OK#H@t7JrROWUJZ=^$=aT2*MT0Lw#b0(l3+uJ(1j?c*@ zb^dT@-~DOiG|S@v^D`=LXfKA(+q)`+R==bd#TF4sV|)zYQEnydyw1juQ7#7!F(c|Fz* zP#GR5Tg%R)PHK8Ffwqs|9qZx%StH-Z{AT=+xsF1ccp|&2pTv;3hnsoctBj<_=67lL zL^4uMme>Dkbr}8+92T_cFBD;B2ia?;6#ez$4}xzm($4cUW6tWp0*uD4Eg~cE+(l~Y zSXs`+0*)sjiI1YI=kCD^C7rkxR}u>!iz=m4d`Ih`f_~%@d?Cp8FwrGCtbx7HPI@%P zOYvstnorLn2$lUddw|tfI zxw7*>H0#)%T*(HM*pee0?oDz)Iq3VaK!9?PorSreaEzw0VNBkFL5*ZUQoov?T zThhdr2WVjJqlil`rCP49JZu&)CmBd}>rc{9eoyK^%%A&64Zd-?=OI;4{N{N98EV+o z7~O!Mmez;x^bTI^^_-7M34*33d2*Z=iev8ko7TWd@Y)tQaDlQN&0kAIFq`gV!tEHY z;|YXu7$iHIBV7Z=RU~lF_Cvk~Vs^X+x=j-^y6Q+A?xUIa@f&Yvfg=Wv7N3ZIu)4Uq zBCz&Uth8T7FK|AYTn7#aMZq1FcMR%kVMNMof-(0mtBGHJ^(rzHcDY;NLK)C=Nl}a4 z>rvS)4c-nK%aK9zJjzDe21!PRk?+uC!k^4Jwlunua6imHaO!B@W)WeU!2;1U*E7L` z__1x>nG6SrLMl=AX}BRp;z8_(EBV>EiXOlOeS81LbD?FS?#cr89$bL7#X|@~uMa5B z1~Z9UH);;S7z#RA=^`rL__)Psn$*yG4$CYo^AndXJ;;A#i)3QogdakHT_SZo?@%|7 znxsc{Wu|PI{XW}V_3RQ`h0KRSV~hzb8V#ks$5|zlC+NT^$vBGui8l-grZZPv4r)>* z8%ON&6u!MjB(<4W0WYAnX($tf*aVEk?XOWMD!B7PuZv=Wsst9?uDW1B2;Z`)+>bSq z9Vvb%**=NFENjdT0<>!LrNdhZ&{Bi+{Yb*sTw~XkWdp}M?uDeicN;x@S#cx)fGVRr zK<9JN6nQf`LB`7@Drf>dg5H^Sa>?xI@R8`iSi|tC&9RSeU4G~~6co-;N`6%pfO%Sd z331zsV8F8ND(Gp6uv~aTMN%zfVbB>PRAHia`KUO>8Giu(m@yK$c*HoAYnD$9l0JQJRN06hI+02RytEbyNqs+#kiDe`0b5EMbC3a~lL;BAM7C~QuK z?Wo~5-DLpcedh@lW)FD-s6L8T@sT+IdjZbdstjO2`K1|yJu=D{cVGv)gvQyi@YFEN zo2}2=^dr)B?@>w6*h0-LCnSY0S{6USkd<50a}(qJ!U`&AnE4!x+Y1E8l@`PtbeAUV zJKB9PN&usC>!A-RWY@w0pn*L&5vZ)E?Nb}No%-)+VRwR&*+af7mZ0gry~>clE`;`L z=;80r+6A#Wg4eer$Xn5AW%J$Q>fr(eul54?Tq zxPSD+^fe@*?`pYGgC)gbUFzB`)zH&PVZD7)^}L7@Lybi6YO{;Pbnob?Q(`z|ZP6Ot z=7cTE>1z5Hwl!WiRQVa(`G)(9p5fx}Qxn%nV%7v4{$?$VQXRU&+@V?2oLy(l>2Prl zoJ3|DHuzT|M7>gjI%ZSD1vOXp2NY@Ci_df7sr@N<%PSpkyJPb($SO%un;`@|ik3Oy zVWTt-hsqNhLlbZIzD`&Od}1ZJ)n1H;`!x9#=lM%fnEz_)Q+G_^-Fj{9Mg8dRr+Is_ z8k@S40qqB=Fy(bdc{DF+(cf*6pTqv^M>>F5B|>)z5t2B?$Zf^tndp zqWU%BLR zcT~S9&Im_R@w!N09SwO*rpDLCzWy3bt(0kM!qjhYDp}rqq1EkCRD^%0~f$ze1kJ zY3I#e%FT=I=V+vYlY}~q%+E~SUMG#;Ru7Y%$tDDAe9K~ydHnXXSOhPx5V3&D@XanFBUV@#8s;2r+s9V`s;_aGhU|TOOCv`qmYB>60g+9;ig{?cg7>w(+n5KIWu#-Gl9k zzP(}8m;Merw&umfV;T>(p&u6p#68$PPyBYlV5Sw)loooRS?9@l?D#}2T7T;VbzwYH z-|ojBN13QA_4`8xqih{5>EK^Snc9o_1xe2dNSNk(0|?c&3HXPjuzR%fXzjW7)Lg$T z9ykmYG2E-eV)a15o&z7TUCnuK-A>eINVwp}Wgb^atHiq!w;QXFda%$aEhXoAF{PhZ z&9DAw1G}swTeVZ=P zVT%Umnd!hMMnF2AC*0>soLlIE5)M<#_H&A-qP;WQ%9ue!gpKy1Z&umnH%Wf=PNTm0 z{tp~1!?U|#hpNayp8^;+hhE`+<&4OFdE?^;3>gyN4Ch* z50r$#WfZG+b7tv1#r{Qjb(xYmOedJ-@O?E0SlpaPzRa(}?caSnGXn%0zEIo^z9H8v zIZoka7;%{Jp)iG)JxToV@W_x<(@vi-h|12}-xVJ@h{5xDTURvZfjIPl+3r3?s8-_6 zhYcnE9uqF)K}`oqAbUy=8m^dYQ?uA^TAV^7uKVYL{)Ivv5CC1Rz-JC z7G&CNz1oKjiiJqizc|y8BRfBFrig`|uG_p4YRbmc>H;vD5TLC!a2HBWQVu3| zJ{%wg0wYKAcvE(llVfig1NahnDNQqpc+~ z*#o27%BOi{G{wva-WdZ)9bwE{TvGF*j^=B*l0eWR?6Ts)IO!Jp9%*^L#Ru zoi#H)CAYGf9$-_jHev-ugzQGD(|oO{vyj9t=0Q=fobmf$f#Yi=^bxWGuIkYO(Zs?y zW8q~Al?zS|o-a+*o&}Jl5)e9F_@KQo(Q8tiZ~6=vwkyCQfo*rv?Er(v7q3-WCcw|y^Oo31JhWT@ zPa=Q@Osy7Y01(fxe^1>^awz>x=>Ye-@o?Xg#zQqqa}I>(Mj+;${*{DYeJ`%^w_ z>NPr=5Z)ekVGz!{>@6UmJVKQ@gf08EuiWxM|_$1JdSexcXu+(5beq-Y?WEeMOiRfh}M)CF3I7@Co@;+Qr)B(bgR>m#I`|=b&-4DL9eix#7rHr&fxZ2V#jmOwc zQo{MCQA_D;N`X|2fs0Z~t6PyGaW^5SmuNU^C2CGhr42OVhPdV`L#!2Jc4MDeB(uw< z+>{~)a5dbmy031jqqfpS`!>TCQ6aQpk?73gKWCFZcM*#QDW)iJ=4Da7cDR#xhu4D8 zmflVW8p`)3J-QC#G3Og89_l9oa!T31q)O?dQl&KQYa>X}tL^_XHBk%g*NYF=i|$q2 zVKhD!xcfy`tbr)9Z)LOm#0@w5cMx~P5}b!%R3UEdA7tHC+d%<1HF>D)C+`2Zv!8(S z%16`%yf!70aiN(zPC{S?6AH{(Gf2eod~z1K87_y{p-9k%S6NU_75Q4*p}UZwwoUZ1 z`Q1gHA34`kj_Rn5UTH()p&QB>!R+uz7CR$iyq1%zrlgHgPX;qYp7>4AG37}_13fryLLMSj-zdCv8b-A8yBzW(5|}T482tPq z(QSx~F-^X*b42>?{iKFr#I;yPtFKpxC)hM{pi0cbo}As)`Z0a{H+wa6pNxo;zKH85 zM&ep={yIZZ-~LP5lq}AZu(O=7Vf)-SuEO&pA^R9NTYGlI>%Z*Ru)ecE|D;If-W-sj zdK8RhZwDYif-blMF~_nDNOzB*kR7J>)?G>5{cxsnH;rI|UiQ7}{R-Eo+OJ;F$Jj|T zsYrj7*XO;+MH~fun4pI|e=|t_-3RqRo8w5I53JeQsVS4Uf~T5n1}ctt%NmOeL`U?S zZ%*wX&aTGYuAup?!SmRYFG^FQXov}>x3tI`^z+<_naM9^>!HciH z`~mB$XOX&sclXG<;8C|1g zYlkqGfI^$AG;b7@$kH+mZ$p&6IDGWUIs5d=VuJjoQ7)EHDJ74Pr}uA`1CgHwkq#^-;QX{ zwL`$bmVR}@LUPLL9Ut0erstJ@a!(qiNqI9>!t>0a?GlC7M6A&%kc2f@UK01M)4Jyv z3@ccLe|%)kPGDDfB+IplCdRlB4Wr$!_>M;J6I*az?`yFJbt(a`aD74V|f4F!4Y3_rj{&hk8|8OxVElnwe zYob_aNdS`lT(6`5MW%+%41?ND2R6|u(CAVWbixxLdG)?{fYj@;?dt$Y_Hv4GO&D5%d154&4j)M8lvnuYW zi|$NH92LLJR@Bh_m*g}h;Iwmcz(=#PGWs?3POr1Do&MT!YOrKQY0Lt4+iAd*>m#oXWqU?#@+B-j&o@o=&^5e{_NH7V)j*A5Ml}9*bsiolzRGn#$ z2-y~EqGj&qL@V`tSl`vCfZzjRy4cM#JSulcNd56^72$$(0~pMT{XZ{bRL4x}A_+;O zjr!9=H3*YfdC6@#O;19jE^eb`s`xH2W-TrNeOi{I2|^Q!qqQzYYx(&!8P0^bvE2o4 zD`)QM6Q$3^V3C^vgKpq0gZ93zp#OZmC@qLBc4qfs@H)`rR-oF|N+6o|)Xb_t;M*`q zb_YRF``!_q2z`cUpVhioyNZ|Si*ATCxK}s*UxaL6LZAS$2eC+kR8dakigx0(HyvSs z&kWdg-$xP_owRZm2&RayDvnw(UU%z_%Mz>zQG>RI8IP{|4(7NBADUCyZn^aWhz^k% z@I}?6gAEhpB>W6x&2oWrEuUzWPnp>I_T?{{&lcvFaHf$Jb#J!AY{)7^r@ZUEBaL36jZFbW-8fs4hjU8GB0M;vGraf;P^&J`%Teb7 zzJPXb$5;`OpOOYpI9|t~Bc7Kj`ySfA+6D}?8@9kjU;!Gv+XjABBqA6FkRSaaNG$hY zq*w2zuj3uzA$f0vf9P|{LCatp6BrXo1~?pe2_gmdF_#k-J`2@ z#$c4BhJZhc^)T_VM|5lpLKbkx0HFY4L+o*9J>SvlP!}b^MPRLXBDGl#0oK8uj>-EZ z3h4;OmVPz_ca9RywzGGzsSJk}y7zJ{x|s`k6JN+}^Kl&DRO?U%s>@}kfya;F zALMhp6K6(Zr_XsJfqo{#Pw$cLlze9IbbQ0yF5#;jB=6JBc&lX$n0p(mHY-z3EMtP;)0ZgEAE=Nij6a)d+9)sroY_%HehCnR7)NqAsNRG7T+y}dSxM01q zLQf%~s=G|hANMJ-q?`%Sn08on7TV6Rb*N2+_Xv)u)qdrVy zv@%8axx{JcpFxG~7;7qDl5MM1x?qq~s`hwb=w!LD(s1sBn8(L5(R!T&9k4Q*yTo0=fC`4 zL}Gq6RynZy8Frn5Nm(Z2e^AZX09^Zuh2^x%7e11>y~JS5eY%A?j;JaZ%gV_PUp&JSiTfXe~7MK*81 zSpgnhT_D*&;U4$9zyUhnfSsOxf!9QlbMJA8$dh=A+uGBltFeo{epx7>F8>vA3j-vC z6Ncq0otC(Z%o*+}j2L6und!sb5)oA2XeQG_0|K zWx!sk`yBIy??OcO=zDKl(d6Xh(4H4;tY59o<6nLG5_abfWz`*Ou#H!|sb4daaq%He zd~kBnpC;Ph&b~h`YV`T;#Pxh&X(f>BP-HoVJ-yCL;oBa>642xObyV=sqb2wa*g|zJ z(D`XtD2h;9zAfJYbRl=RvMz5ae)#Z6_{Ub6@K-ha+qaSg=scTS1TSPkJWN^cfk$6T zxFi`{GC1##F zaGl0w_3<*;mnH10bmq;Hs2+nl7TTXEaDGcI%WpjnivnDcr6{21h%A5$+yKF%(AJIN zREh6sZ(gH=&CpA5|7n(4_-0Dua!cj_D&W;SjP0H>z=%z&YK{0Y1VvF1{2JulAZ!D!R7=oi< z(%+@|HDMt%J?;c9I5Z2%-!jzMSZUJ$B_QB?A}t2 z)vvV7GYGb@YqDIsX#C19NGD(SQ{GFQa@8*6o4xo5A9g|hS1V#y*#w!z;+9G+NttV{ z+!8Vbg9$vI#drjIwKskm+gO~KDOu*II=twp9B!cl4?Rn0d7f)qY`qc5TS>d4j^6gt zwv;qZ%_VswPuFNh3bcRbNEw;9R4uPvSh!ML z5E=7IwwvveI(0YWTgQi;yyc9!3rP}8e(K3oI+%XS=f(FCA{&zJPhTi$MQgCIe3qYn z`cLN?O2+0pk0nLQw87Vts!OQDXuh%$U;0MtKyz|c`(gkNxm0dGflBBxSY3a&CjZ^V z3{TcM!w(){l~Qbm=3PpInx}r76Ao&{(Ko2=QU>QuUQ(5ix;Tkm zX>so3n2t-u;euCGM9xq4P1G)|<&7IrF!>j{Z_Vzh#BLd`&gJi|u4XA5g466zdk1_E zhEW$+S~(&^0rh!u*j?X`RS}gYj+4M%uKkXNcF(8>^Md*|@rZQe8!^1H`$aJN6eJTJ z->#q%0bykdD`Vy}4F3ce3S|1{#F{^jRrxZ!S8|9#&oaryHHO*DWr8M!-nk-_r^BMe7WpPo4~lb9o3pU2do^2<74aDc|}Jaq&kG4bQJ7V|%D zD?$Et8_3xF``ZMM_f`a&YqtBdL4!Pd4R?K)27Ea{gLDxK0&8@y_giq)1B8e##EX+%1X~7mnpt}9qw~Wk%E2;v+-i@7vYP^| zDf|@0&;MGBeNN^_!(G+KkxD$;YV1AV0c z_At^LU*a9YkK-yHg(Z=CWAKS<29K82QWWT1l|(!P7My06ZJ9oOU-PAPyGN^e4gn(z z?wTZJ@8NiULTA=fx89#^8lki7UIK=K2{CBI>Y+?RCb|S6j#e3C9S_HK;MSlS$fSRuoSMyu(-CYWJ(mTM$Cp}wlT3{ zfau%?_w9aK|Bfbohg3vhG3AUU#dz;FRZ-XinckY|=d4H4Ccw#SgD6{E{6wond9JWzqJZ zr3`4kL?BQJU)A9JY~PJdZ{WLlqvs0{n$A?<`D9Q?Auf_g9h?~&2j~0tbq;ds1fnIH zefD5GU3{Ruj$4Eb6cU>=$U5UxyV+j;>aLo~2i>Lhg0Hk{gJZOGPO6M)V@9R5=9j<_ zszW5w$~?r@(?`ZJ;e1&&26m)jH)KLJjVv=t!f{_^z|gD9E4Duk7L_%&f#P#6`9A?F zM|W0xHc*~J3ZZZ&c9Q7f_I{vuAjM&If1F$vG8t3SqWxsj)NtA*X3WUNR^GHx6hs7G ze%^%S1Bb*NI481XA2eyMIl^}ddm6L|#V}V39B%;)AT%;)3UCyPTmbu8z7P794|E%c zPq{}TD*p>wC}i+qfJ$pztJkfCNp$}qLxc@WVVYK0=afLu#ko~?U;zOqKZ0doSFH|_ zP`QoFj%ytO^_|@tNnZf@)AGel;0W|3&I3^NF`|0q1m-(p%K?ryx+nWh@X#Z4qFAxB zZ9=g$GE8w$xYdMTNW-9@D7VBZ``uh_Vt<~=5I(yFI9){$yhsiBfU>Pu9f!wF4&&jT zs6$m}srYlHV(|-ocyfp{BUg(zboy^64{fmjHmW8qG;$w zr2^B!P5DIYL26lQiqeeIN5hGUIq_f}qNZy8EQ0xbJfq7|7-uz+Zfqvy%59U=sNTmE zxx)uUQT8V=#yea`Z!8=41U{DL1O;O&_Eb$&3VqocEbbTz&BZzOC1_R*aHmNgvwX(d z5Bql#;*b9oo`5r2!TEF0c895ftpfzh<*v``ih=|~yw|-6ds?9{x?jP^cU3&24Le17 zp*wW@(CdU>8gi_utUA9CScFI3@-yb&WXbB7lU@|YzV8Jxi`pHYvoUis4@qNveqdm< zDyPsdtPT4Fa_SWe&R*LHm%BX;8(70NCxy*s`Gw2oHOyk0FlM+v__q2OK=EUp5%p#% z0c;UBZ!rU^sU(g&S3lpPhx^=y9z2oAa0|7SE>)Uy`yzJ9FVdCiEpV_64G=$X*Q|~e zy{ol~H1D*Oz8%0rRCgTqxsPtLaL$Ku))4QWQ{1EW;T$gYI;^{KYEF~RRBA-FmCWkO zL~dy8{Nzl4B%W4+m@WQ)*y;UgPbk3?w_L8d`HfkOX8TYY$NtUJkpOj5@*f-HkRuLq%9{_4?fFG*&4>*mZs}jnx0O({DPI+YUfHRRf*lt zqW?eg(fWO)Nm%}{3;xHv{Z%7oIj)b4eu3l+$hil@qCfbPS|P`x}=hc zqRTCI&>7DLCffhz5lEGytcayL+Qs(8ovs>uUiaBgQkytq>G5L=2Hm@KAwBz}YWKU@ zV{Ti5;C^ZnT>C`>Q^Qv&xe3LXm(S4NVxtwj88i5wI=|3;_+yp@$IHVzoX)z;Jw_c* z#~H*!Rito2`=tdG@mpHopmxj?`y=W4PhAUV{LLXsuQ0@8ulAqlb1+=d__9=8Ah=2v zHS4FeTfSpX2P84-R7GiUtaa=Bi^#55T?@QJ;rHNHY;^hAAL9;o6KveGlyNvX)Fn^J zO|KF4pO+SNAu{R4;3Idydm8?+9c6`AGP-5;lv~24DH^E=U}8l4{=;(Bl~SBekr`!i zee6y+J|4}=+-X*wdlJ@dj%nJdg-@t%!qRFJ%;MQ-*I7E<_?W<&yIr0@ICppd(@ z1R~?kG+L|X#PX_qHMckr_*5mz!VY*3`ZzN~Gxe?=>z7P|+a-WH(TFkJ$yaQ{{K_#rwDQEGUI|1x2de3Jo8PaMjmKoi}_2TXKA@1cnq9pb2hz z#EkQ6m+xQg`wlh6Nlku=h)|P5@VXakOkV01l{r&YGd}5T*gS{pElRPzGMA1KK|nw!13&3facqVeM8BM6u3RI|99n&8Wlawxrvj!<<>UMQEqh$ z(t>sY%jBG(6z=)xnx0!n9WRD6N0zPau6POMz!62ZzJx z;X@@rO8;r;{Ht$j2*5P*9W5mN&j+99QWFy^&}oxEB$}2|$M8I*?^5B%=16BfV~mQ_ zd0SXHgQ@7RauoQ$6v-%1^J~Z)}OZtcT=vH z7164SZu^?Vf+&}#`I{$1RVv-a$V_A;VACcL5o8>pV;a7Kddtr!b#aA=VCP`=J;2dN zh=L1R@O!(Fa-r(Rhw_QP0Al0y6KS?bFJvJ`q|ihmgFWBDt5cqz;K_$}H*WlQ|2kXz2e&pO2v=iSo z2D_%;+whS;C%*P!ZdCa-K*I$}@=Y{>>^@B2L-z&F;WA&2cId(j=vNF>OALM~wxU~F z9cwF@No12%615wH0Lg4s2xA(*@@W%E%ya^p@nMwrLsJ?lnp+$cMQZSuyi(QkKPvM6$zueo5Xu<>B0sk1L zCpHvgW0_A5ehh$e2BZ^NSxrue^XW8<&G5+Z^D%F$5s zvnF-2>cb4V1nSlSx^nuLh{S&)IVlnRjz-A|@CLD?YTg9B#KIQnBfAWOO%!V!EVo*o zL^L7luU)}ZcTSk%qw^YGY&M9lfYLw%Ehj9snfA^V`-ciy;OD}1}#V^w5jYZ8-|m6pYuz>*a$BTZCM&$~0oneLU21hu_hnMoigc zhWcKqUUuc#6G(k`8(YM$sw=;lij~iW`$8soq8@ITe?9QfHH#RTQZ=L527fAXLk!VS zo>Z6}l*W8o5G%wV>sLq$hs zR6)OYhmwI;Cot0Vla}O-8|vG@0>KrzgrRbU7Eh#}7Jh{eh2k#IN}vo{)Osv|6FwrY zEvZfPQcZP$qMN8fDwjS~Yns@}nV$RQZxc74rWH`(-}=T5pMmO+&Wa@=$Vu^vZm~Dm z6G+p)PZoRXBan4KV4j%>KR6oO!U zG-HPMEYOkk{vc#WB7*=vQ4+cB5OFbFX*P$NXus;o)W?_Ut#DoJ?`H4@M~*Q~0?A~M zpSgV^W7YPgk!pYa^Ib7*`3usz>`N1#G7>HN28{4r)tYglY4OdQU}a4!(mDYSBd2@@ z<5a%2JVM#$Mr7<)WyM6qu{ti{GnqZq7p70uiqVPzD3H-|v{>UpFwoVhz=_R;WYfAU zB+obBJ;*L?W13$O`e@rwo?);T9n=4^q-xEZLbRCt=2R6SEGbYY>*qi=?YGYJw;s8) zfY{6o2Naz1+X!a6p*)92vl>XtzydIxDj8==E*^4At0UJ9J5FNWL*!CTbwGBM6`UUujl6`J| zM>{kUvH_kGPT)CNB6@x3(IT$*XGv7B>H}$seUh%@1KVpAv(gZ6v&la#Hp#?99eWqe zpqIE~p(~NT-WW;#>2pfrMWGgT>TL~=)GrlJzkYiwiYfE)Dou)_67Rvg-dCQNd+zxJ zvo1B7Ww(|0xLI2pg=^moBEs*Wz`d-7hWqU6)Jp&ldmSlu+l;3x2?cJ>uKwano`VsRwOAg@dYWvTf3<}~nVR$L74InJ z?_3hUR7Gfnwk3^9ZvA8sDzzM85wfL$=D&$qu@XtVSu>FQJG@(kv%qa&k3C&iD`=S8 zT^WR{a*oAY!e-$MwfH4Qs@IETtRl+lMdYt+NcvziLuIh!J|#1w=?F?>aS2{3mYkNR zGOCS#eDU=)x(Go|m8z+0*b1r#0niS&iKU?-&29A&Ek-zv>+8p{gh{xEQii8$*R{Bw zztwCSqv3g7+;Lm*;Z{4V`D<|8bIcuI!mjtCE?8y-snxUx!IVh(*CzRnRz+%WXLspf zkkc!=>kE`Qwvq`vh4I$vRB4y3_Wr6yrU4)0f8cTZ!NJCN_kT@Mz1l3eHs<44LLju5 zBevaI2Usi)srt~FmJl_V(Sai{>%yB~5zb)sXYUzEkU`!fQ}#Kx!+Uw{Ys2Eu z3%;$ufhh*Jm$m4z1Juxa$_-~D^C$}o0~CdudhwxGH+N$Yjq=t7P>SuYfQNZozA?pW z#Ci57AQb8=pr_0OfGr1dk_LDo-T$!xCcB>G^l~xf$?LbZd0Q1AkO1BSYjY4}x{^nf z{KX0NkOZCnW*@^I8J^DCW_XLmn@K%E8}&@1R@0q_DLudzAmxhwp{uyBkC=I zG!d*V&d>EQFsC?CrMgZfC$R{FD98t}<5$Lj>$jGSX`p#_JpgF*LX31P6u`5XlvYIH zR3W_DM&aF$wicT2RnPAJd`RQ!&a`kYw&VM7+!|m7X4LUI*|RUkZJl_TBxJzJJqb5Z zB@6XwyW;oq$IjQsJT@1AToeNJR|N_w(=z*lf^!HL_TAb5Lsb0*A#C!1_pA)z2YSMD z$+~r+2H4Y};4-fgK7>Z=9t!@3eb`Bs-9y_M3YFr;5_%0V50=2$s=2ns|Fx~?VQ9&A znFB5u-^_gecM7j8DiU~G6MUV(K$cwvG!0OM6ga^R9A(z%hmMB`$bSGP)L zg5pqy5l(0-Dkma5y=TO?!xq@ukpK#qSlnP{q@4=OfMmlc4@fS&K{ePv?Z=*tqw(p5 zey7jR=o|*aKs%sC@^WgBTb{lF;Jr(K=`X>Cc?k$H6#(ohWIDNGwXd4Bb0bW!axRBZ zr>`A>N0_SG7zFz-W%UL434;#Q6i_^dK~G+y#<18CJwN{uMO@MZBF1MGb+hxSj{QlA z?^l$LnEg}#iM3l7c0z=*4fpKNvk^?dYw*P6hVLsGh{&6nSw+xg=`mb_H5gjCp;xkB zs$IKxw_!wgEg#@KN=6^uzxKW=*BJxHJ|VoTvU5>uECs0mN>Bkmf7+t@{#zy*7UBA4 zSz}~uGtV+d>4$Da4|9v^)fXFH)1nzfdHVUA>=t|VyASP1^;=J!h&=^YU7OtR4RGRh z?erBN&^|uepf=O+BTHilG3Ae!xE&^U$gBPoKvq9=q)F48eBA0%>1#)^(A=~n)E5;+ zH)6PnhKKop^0LTsVb8x4!vE@jg*@stNsrC=BdOWmDJ!hgt8{kgQ(rN6GOy53;kYsi zri|`55*NCYjh#5Ehl_Ra>BMhg$fzMX-Xy2(PK4~^oOU&Bx+oH#(L*tjeawTWt}nB{ zq>sTrhenph;%5aDwB^Z!+5~9dOkmQCd}!?KS1wrDKzJf9#7h;0*iV+RkrZHs+9u3k2hC$2+#DHdRrO|whBgHwN zUfG!`N9z#7{OxTbT=@{=n%koyw8E5&Ot#niN-R{f(VW0KeFI+ehkljkM?#-1$z3nI zvkZfKae|DuyuzZQR9Zi>QT;UST-J~bS9)Mx6@k39j?YMZpN}_|66DJdiD!f8% zmM836p&HEiL7(W}p=+n9J{7ipcC9dguvQ#cauG#EMeVFhWo7XzX|C4vF%s-s8?JZm zP|Feksv;ndl}~{^>55H`%W$?1KTp+t&0!vri@W@&r+jm9*1ojhA zrk+$oVqba8kP z5J3?oq(P)pKsrUFyIUFrq!gqX1W5^LX{Eax0VzScyO9>@9>(7pb-(+qy?6KX?tSmx zd-pGxGiOdb=Q+>woF_4dpX7*NzN)Wucb)A$7iv{Z_s4BL2?C_L@)FFIFA+E!ht_C7 z4kP}DU3zd7TJ_=iXa)Kw)GOq$)JQOUN8WEPpsffX@&^YRa3euZg$-`rfj7_YDx*9d za(QL$gBB1eoq84)@7C>+x9rACzkF+EkTWUw#IvI3(eUxB)1>+GC!GWz4)C#N;IUi# zBRZ&d`kLQso`6FW^B+~nNi0}L8?v$3JhG(g@>b?e%Q7p;DTqCAaOhf;jl{b=6Z`Br zT7ZrItu;mgAptt$*Cv~Ng;N!_C%v?1^aR;w=!eY>CLRy^YO->*wh1=mopUX}7?b5A zviA0tDhT*V5zljzqIvZ{#nSG2nZ3n1zA2-sSstT(BMK4~>V<+Kv0KafQ$zQ6{P`L4 zJJDh#TsO3nj!$k|K)(Z%xzTLtYeRUiiQ&k^iu~=l#84A<1n%ttN#MOoytl2px!ydq zqHV93b}`Dx=wWfBFk+kLdtJ-tXz*a#p?|{L{7c3g2!V$sI@WojzwrgJ_Sjibvu|;_ zHEul%wQBKp_M*0DJwT-CUrWZ<7Gr0;^dk58t=&N>!9j;kcl^Ch9b z<-BLcJ0dV7<5hg>F7T;-LG&nGaM8qew!Xk-n&)felZZkNA1#M=49?rANUECGFR#sl zpoX7!X;{y7KUj;-j-YRnnLo_ySmkKv=o7!%x>zilFFqN$lG;zC{rShl&&48B`VD4! zNc8YIMnYI${2eQUHu5iMc&~_?a_k>__B;d+0})E4LsA|LZ3k>R$;fOue*4@%y22{k z@_d7I(*{CkIp+4>E&4SqtjgWp;&|Y1b8U@ZlYv&-;SbiO`2Wnhlq*NduQnnf>%BaF z9R!k5MzIp<)_AEbl&6U)N*zTYEU8qr3B$OboLw{6w{zkbBu*T@f=0a2joxP#uy2sK z+vftj8;1nRagNd?eMl|4em6%RvS=n;2F&$ku(m7~aZ6_N2F-&qdeYO4LycZuF*5R3 z3KBP`v%esn=p)P={aB}grr^@cfh#((n6f9A0@tfTBzV$!lLCv%6<%KY2EhST0*EUJ zEY$d{dy@^;Bj+!5Bx56tP%Id7MD-7-P%|N0ieGxM1LpN&$#v9DyE~G275MKkL0I6I z;K)m0blmif75mH_oC*^ybZhk>aV%Yd+3m#zp)#Mxz+Xj6Xe;3R7S z*`S7RIo2@`8ELsMb~a}Ev`>B)%b?#SJo0^_Hedm^ajlnlGdqb8ZmPExYeQsS4Xd=& z8S$f*p6JU1-KYL4ZO$V=m|ugX(v%4~_f)stZn1BEpzjz^1>~%KAfy!@?1DhDjk%Nf z?+Qp!b@1qaV^jv$0{~H*5I%JRJ0k~>&cpw4R>wdYI&MZTM6ZX3%39ai1mv|mye`d$ zJh#vl{F>_o%8C8YMu#t8hpt)e0RuAX!%7`QYG2U+%-@dX6B?LZE1(*{8dQeACdWpz z2Y?9vq0t%xdE&?9p(~7;U_P*);)mzRYXEkE0Km`SkDK*@)lJD+<;sPb$;N#THTEUKC=&;l z-amR#B1}G5)>jrBxyO~cZv646Hytx&Jyg(GPMP-7NcgX$gujYG|3ITz1F%5gH$Hmw za?u}ot>IaG7W2msiO2*BDJ|A^l0k%_8y+)(zOPe=5xzL%38}!Tw$SqK>(JO?4T;1N zmH}#c-M0JUL$jutg~}R_x>m_gV!`D!2QLwfo`Y+(8K?lQ#2 zHJ?Zj7+3PNd*4>L_u;yRN~YOGU} zptE%GhsmLnAsnc5C|p5UvF~HYITjU_1+q+6TVJ#2VARMPO}Qp5mf9^_f&$)_&yAoh z%VGY2IOSb$5?;_w4zRRP+9#L4*Ms}nZC#G+rmELD-si$BCZTA6@tH>CI|Wz|6q|c| z9gc;DrEDEdGn?Kid-%p;N4?lBK^Pcreg(Fw=IVR(9#m0639d2{zMUHnM$JgXrAuF) z1Zj);YQ7kcWA@w-Pds$3=sxr~i94G->{Hhme&kxvy3^*dYlu7bc&8)ai*nE#Dp`q> z#Cy;Ja25Rk!h-9KpvXcXaUV3l`))sZK2yzxKf#BL?x+)7&6n09m=A{Dr*KM9v))lx zN49V~8l48p+6bVrtE;vB2C-2AB(xnSeA)xX3xorUz)os@_~zKairGQ^ONZtv7FFZD zg+Xff>h~(?P(}?>sms(aWNgr@8e+l4EBNgLQn@cn?_uu@RiCqjO09KZpO5hgrYdJS zUU{#wCatee`yJL@l;>Z>{pP{Cr#4do^1y~(es+~*97dCR_puCBz=C8v6f4L*ol`dT zqgi?kAqvY(hXkYB76ubq-f;GY1dQ=&7VmMEGh8ezP1EJ4cn(=}WcFYMf9uAR~`#Kcvpy}YErHqaJpYh~pL zaDoNer8mP3g16`cK(TW;ISeEl9L8{s0*E5*a9-q@e6LS*-Bm>2UnXHkK<0{c=EsF_ z1Iucnn~b-!P`z=UvLN&bBUwM0BRG+aSozITb;1;o)FKDhk-$$ss~|J>W=49rak8sw zxb|bC@FrE48+g}5Dz@8i+^1CU<-ywlKblcc0vcteU^ zKH5lr(heMRvMVh(<|-0!%+a*4fu3&=0fhsFauw(oPT-S7E^(#?N{nSC1G+s`&@Q~Y z42)_V&*u7YE^xN+wvUJa;)a1jX9)MOKHv(pHM*2?vX}l163@H68VCL zn2{%Xp&;$%Y}y~+ut`mH96=*dL`S)FNu@m4ITGhHLktgLe&F9B_Wr8NpJoi)Z(_zl zykQ&-5EYyh4Quk)=Z^?X9R}zw`ac6b%NU8NT!xMq-z}BWm|+joKHRrVrD8nfO99Bo zIV9rNH!SXZJV;61>@~PFq&QU7uj@JV5lWRDDeq}8s=hhOY5Uw8t6(l3_IjOjyE{3^ zGH=t*Hc>mC&sm6qFbtOHJYOcco}1uHBx+Q%#BF>(lR}?fbmqnv{Tk-BKvTMLdS1KS zi1#Y8Dxrk07T++fR=e!VD>L^H>VWp>QUuRaGfXffh|n!Q-RJ_sEazlb1;^ zB>~mtBY2~^?U|)2v;#WP=R~)LR|ve;w>OWTe}iOZNWlCr5|5vTzIgO!JsS@7IGO^D zs$+M75EPEj8-&1N0HOL#c;n1NGTd12sBaZgu?w*Ux7q)3_~lqHz_WJ(3zFbgEv2vHLw4}YLi z^KiW7yxJ2jM_7Fg{61aUqvRkI2yS>o+ovycgcp%dt^?O)|7`M8|9SAp0NO&O2LNi% zYO(MhmWu-!!Uqx9pWF{e(hp3g+~o zYbdJ8VoZdz_uUeyVz7FRr?)7>nuZBCt=_(FQ|{)h_o@dnbDA6n+wjAFS#Cz}DlIFovnI^xC5IMR!zr)cGOoVR*aEugb`G;A_m~Zzj*fMpMs4Z5 zinQhcL+SI(Qk_)Na{@7^mW>j4Zj4|=UTlQHse}>o%$*O=&#z>F4L=g$aFE2Q#>`v3 z$Dj|BMML-`_@5>dgIv2nBX_Rsw#t}sKUq(3+20MC4fC#ci`|2MM(rIu{U4Ey1)%M5uMZ;~2{7o__v?{w&ciiXFbF@lZn-}N3N>oe<~*fp#J z_4M1kiB(bSnNGZ9z0~O^G^(!HY)VH4d>sWy_8{L>w`SWTgLYci+{yKf?CfOm={*@n z{0Ir-8IU089H>G~J{6``PHIa&cnE<&?2sY!Rnb|$lIj0I(*7@emt_dByssZ>{;M>) zhd-I3+i#R<|LjlyXli|}hy1 zp&%dhuolglpJiy#a{pzWDaTWwHX%zsXvga0@RJ_V+DNIpaRnJ+d1u&XeuN3qW|n9z z2D?~3_fAtmqda_PbUN^vfM46FUH_`g26r{)+nthe{$l!7!(6pS9`L6`UH{Nj* z_u2q-D&&!Trvsrdnl5{Q)yx!34q@Yp+32+jV_E)05gf*6rzY_s1Ppdlocc`(xb?%F z2mH6RnFjWr7OTC{)Sm*}Yn?Ceo%rw#5%8tze%8YhdV342V)kq7B@`a92!2<$?j&Q< zSysvX>c&uUDb@}`-GYgCcvE>mM(G}^sybB)cv!1FUE!-JY`4sfRC6H+1w~Xo9kmbg zf?a7SG_GI+5fY?A03X>LMX5E+1=ty+(J(sBL;ApT{?+9*RA^87oA(=cE`MeYqy)Cb z?4AVzxm)P|AjQ^ZDV;B(E*#q75U0w(f=uiJcZ{_rGU8cFrwhz`$-&TWD}0z(&Bh^_ z@f6@S1l{!ydQ_{o)C4%0awAwT!4YLL+Tl;1L17u(vta48Y>B|In~6t|=qIjZpil;s z0^ER<>#Z-rrA8WeIT4O+{_gFfEY#kr10qMCAD{t+l;_N?p3pB-;^_$PxpyGLS%zea zkN6*x4R*cgu91SDI`B=9haI}cQ-)-O8_9pUPmbWp`$N}iE_6HlTW*&}Kza~|^FBE> z{$}O75s@Rn7ZlsXuV+0iHwZ(-!SYBjJ!3g}!+He3C$?EcsApMxvX63UPvUEZzZYNu z+38@@dJd~_qgq?Ub3G45p;!pm53(<}yEdF$c8W#H4Ipx!P^=0~1FyArgYl^hwGKI6 zUnL+upi{Bq9OGiL>k1uda-vsZ@>N^%A^u2_207i|Bs)PHW;u`>MRuux8QwaYj-_%`d?H=myb3w7)3}#ch%Gg zT*X#38JNH6YqxCW2;>X-^77qw{2sY4R2FVmCy#cijus;2US?@m+L3aESW>Oy(RN2j zwkgCddwY1L5bFij@OhAUs+Qy_BBv&JMc(8bx41)|Y3e*BH%jELdDV~Hqmji{>He;B zgjGM@(Y!@%Wo(DR22*;tts!aqf&JAUCv!}HQ+jLHHjza>gh?aEBmCJBbwr+fCv@=s z>YV7J5IGQm#Lw?Kiqab&bTJI)W0N2AVkQ8iaH}zok^-wL&72Kbx0Eh4hfo3XB0V|7 zBpf>3KLcd1)X3rM>5IIRP%12jaLfS#=sciMmjT7{zwU43>`C~BI^jl~p$Z|SqzaG; zjSKruAb%&}!+e2Ca&C;I2nc2a&<@bQ{?}h+=N#7IzHMV0Uw#{BrkJL-GX!oM1-RXo z>wW6Edv6KM=rS!II8;rl9GnA+LgdaUWU6aK9T8C~*v+ZzNUi|#cvgMfd8xOp=;h`F z$O+ax1&yZ-`d&d&V!D%FCCTjwt$oVM`-vd4FJgJSw*cMF{(?{sr-NnYsH#Qd;?ZkJ zx0wV_1Jir<(r25JdFOg;<)u;#?U#^S#-_#yhQTQD#!gC6id5B9!MF~P2aN8qSk@ER zWM@a3zJ(1SZbi9*dj15NPtfi{Bdn<+rvPSxpXr_g=Un}AAwU-;$|%i(>tv$W4U4kkh(@!MgDigHr$UxZ(A+qWm9H+>BcUj+-M4p9r@Qgh zXzn^Fyg1?jwIyOBi%o>dMaxp4TEI>{+ zlvF9;8b4x((y}j96lCJoGjr{UE{}v6f=>G^cAfJ``#-QSP(pQ+KJQtNwHk@H_XKdAs*Ht#s^Vey`E*6$u3XsA6kW=u5k+ywzQi7s)w1DFE$Z{4Sjuq-jK zJut@cVLKy;fpqwOM){nCElfl%kiwvIg>6LV8}2OmBVP)pJ7^fF2Bv8U0=Zoi$yVbC?0*usf)YKFx>bf|3O=2X3 zn7&F0<8*j|9Cx)5LsEtmS8iZKkl$W|9H~e!DvZ_qqTY7hg^ioJSG3MkS-{Dj(>TBxed6<*QDnb&4yhhg!Kf zuz2&{TJeQE1KumnA~v942BN$xiPL$!S$5d_p}QCs9zT!hsfL4fha_by*l_}E4Et)i z{Lv3XrsC%qDINEG%@k}96`cpDGeWldT-agr(g)Drk-okZnDucoxB&aWL6+D9JX|#1 zpMHaUoO1!+HYH8Z1PTLAPzXo}LQeJa zYu#T|CEy3SMvHhIb+^nR&Ng6#k{%5;MXo+d9H8pa(q9&& zOE?3cpj`()s|L+Kk8w*Y>Z^(|SO{&3@X6FRVoYo)kW2Mf(uwZKj8GHh8u+TlW&ax_ zq+uU)p9QwyCENFifqwa643o`A#C9#aGdCm=#+YZnfI$6@6gLk{g2Ks{)RBD z%d0)(1~9YcJEd0XVJaDJ*(<%svpxx*zPU@*cQXr!>noQQhcMlzir~lDIl;Qldj7)fa#LxdHIY5P9Jf8@Uy45dUqPHdhDN7u zZnO{ILS1(nIx^i<=PsE-&#;Yr(bHg5s5 zBVVmpDKuaaq0c-Z=9|~(>*H8?;>2h6M3iSGC^o;fL_2PiVfFP)MzRyT45ho)C))5! zQK}}pg_-P~*8MU46!Z|Jeu$S~R9mNV{Qt1q^q3)9FAp%bdlL}c)vaDb>S(N-_N@k@1K_N`jNtbB4v)Yh zewpI_8s1#ecMby5OGR!KJyy715d|QqHw)cL&Z_oX|0tT__Ur&jE z!><=vU3M?2S4xwfe7Ht^RD&Wq8>P{1P?|tXufuPm8_bd zXTe$gnoC`XTjT!z_TU{@wU1N%dFa+`lwjbfdM#?_82X6o<$%?Hb=_Sx$T7v$R}=2>-D?l`axq zcCXy$x1O{;1`YOv&&Un!#dsTG4s!$G0e~{B7PXJzvgq&{zDfVx8pHeaNMPydK(_>} z&K6;XL|WTeN?A@1j1fK0kQ85J=BgS-{As}d0}t!whZzS@|*ONu@FtLziB zIUAr2_a)WJI(>)ZqF1%W?+Kp~n~_t&1-XzU@C$2@2|l)o9I#Bx?^aYx^Z0(qb0&^6%{1YgJaRT8}1f4C7c5%l`I8>)nw zxj_Jnyw`Q0;#^```mC!Y3tiJegpg;RKHgY(qauex<$o;w+7l?>ay~3iyNLDRjsoe_ z`g?%7l-H8IxM@U$sZtIN7c#<-8xX}=suG_b*ex^DoPY(?_^LAcBH&pga8SW9(l3})vvFwpiJ(CI-td(Y={iEvLPWeyc`u=VZm}8P8@oV; zm?TXUbb^Y4g)D?KG-lDq5~eN5(-*VSFBtsxA!wVoKz~X{ZecSR4p;qkJl&nJQTX#A~JhVu_KkX2L{m1}t@UQA^RYXCRIGxF2YdoW9c3Vwt;gOkUXM!nUn z*~$F_n$3r+9`^xQWYo-(ivm8M2IjV!+!zbio)xY`II_Q6r#4liAq-Misr=+c>-d5j zkkIrDZuNT^%r2ZG)@oObh`5n0@DYAJ3Nunotno1HSN!w#SCsR~*>E3U*|r5FNRb@u z>!ZP&724P{Hc}fL-mIwMZf?lk14}?mm}R*P9(J{w!hFgHKZ;mz#ol)8+!H4Da!i2jHrb+Yl|MoiZ@+QH@=J=JIpNT9$tBN`d~Jj^Cf|d;&!v&5 z$HIRxx=%nz)!!Jk;=`>kpgYyGN{?RQUMtozecG;V(+SgxdIYMVUvRY+*9rtgX-@jc zsqaxn@MAv_7ILk>@8{S#WGd@EIDX3F^l%mxsJt?=G^Ln*=HQB{2-`i_eFSPD=-s>a!yVHj% z7d9I-IO{qL!(ZQa@w1T_8$UkWZ-<9m)qyLmPgf0bhUR>@Y;B(-QqqF6s#P(OEH%xM zHxjG?f==EpLVFd%_FXsi<&ih(H~&pg{{rX#1CR(-$C7`BC9%?y9kP;T-6r9N>@E96<0^hc4Tf@3TzGgx3d|KY=PcFSe*kjPf@jTR-6W8-{ zC-=HonLGu6;q%K-JbN^aV;FjSHcG5!i(FQiF~T8&=rGEdb*VY|?d(g4MZwYYa4@EB zEvPTg_85`!Y2F3cP2&>aMhx4hP+C|%_;{`48>Bbd_S6|nB)PsqA?4?=S6Nt@vSO@Z z)xLFp3Nn!Gx^KrYu2E%VG{+Er={d?Dl1+YycLaf zI~jN^Uf3#^nd&Ml_wg`{p~-`NvBZ!cw*^JQzo*~1v?oS&7(&9J^@#F-)UfWY!4 zPjW&?q9SI7A9Rp51UPcJJ|42MlV7@uaP4^9tChl_yz1CoGR-4N;5Bpu#0P+&NkzTU zhaU~wQoYTX{kJYB&zZHiAe7ZtQyce*vtYHc2vJhf9C{kW2qM0~|Jd~5`stXSi{iJ> zuW*BdS@luCQr@%sU^xzDdpAlWI3eK=B-YSUF=jz=d^w)-aOH>jf-jvw&+H!FG{UYp ziR*B++xIu_W&zE*B1P!xwP$P(ij5*{)wqek@MM-H(|3_Oc%Cgf7?o&V)y3pM_|F6; zF$x*q77a3~*rQ7y%)qavtw6ZCAiSrJ1^>kK^DC#rx5C4*x}|fOvwkb`6)O=$DVNe* z(gG@n1Vy#^v>k;eGEHSp9$olj3#}QVO8BC)2Uob%cQS`pn9;tqRyx48S{+Emn6M^ zKRtywb0#H(vzY@#6xJkA+Gw=h4W%OKkx28Atv=S==;yl}zzrm+!L%yf1g@^LWHziAHOV9c^plncmK$G>KqVB&d<6F7cw9WF&0=KKk9dd5y;i z--Fk$We@h5cGo&Y$Tfh!1TmZA&1b0M_*&FgLZ9%wGvBu{ofIiK7N4C1^jCIbj+bR?$Ab&kc9eKu}TVR#$)66m5I-E(Qftamv7OoX7IXk7Hmr2_CzUYu6w;wO6Si ze2zbH^xoDOfj9Sz-(>CQt3Ofb+IzpkTzOmpyKCqP95I}qHAY72(7R`I8_hb!flybK z?>k898edk|Ycp!IV9}=ZsH$i$P6nNye1jzSy0+da;%HAZOL|!P?Z(4T!`5 z8UOv~-yryJ!vg4ET#GJWJaEioa(%v5X}l?KOZmANgO@>|eoA&Qfm)CcXJR!jg7c)f zAyIn&T~ss~?(NRf<(083RkoJhE|}@3NVJ0u?jGWO6DhfI$Ala5&$2l+3;DH_^wely z#F(=*Nqo(w>8$sHAjFdVYU$7+J*Dyru}CS?67>7%$k|se4uY3d4gUqX=f9WqC*EjZ z|BO)nFKN{Oi!;l}H^}biLlxK*+k8XBUWVL!Gwi)S$@t6$mFONvpEXnhhwu%t&x*KP z5hSjn_4W8d;HkRVk!gg}W3|1H3{jLF9-W6mH$S^t>GodQWM%{m=iiFme}mw^8w)`G z{EEUlM}Nixx|dmPt;i7P@Z5A6~9ExJZ9Y*<}6P`<>3?eR^No93=j_ zTJ|7=c478}_MN7v((#w?C@4GJdxyp(g*e3#Zu9@9t^WqWe>WEBe;cNo3**iWOK7!I zkRnvhn$FlO33f7u*0P6-NCUT3j9YP0YfcYl+Ar5sw6+*`!}4yrv3=t3)z7z%;$DVH zADu`<_*&V2P%|C*ua)D~0$mMY3?5HiD>Ad%`OG6W=!zKh;XSjnp> z)884KW2a|boaOotW6igm__?}drQo%d3dE&c^|G=2QbpGq(Ne=P;9H=klw;;aM>HpD za6Vz&y@btIa^W$;W9sp4St|!!rhYPPr<%j9&*x$zcQ!XB*DqI^VSJMCotYG+BSS~$ zlg>^@6w)hvKFxRTWJYB(Nw$*u6+csRJr?|~`zyC?$=j*ekXU5%HG8i?vEt5smqsNP zE6B9S&*fTXiMc7;>Fx2m5w_6BRap7?c&_LuSHe6V)J-)M*hD1A6y$i!D-lWF%;f4z z(h`Q1;ct+`XCHz56(6OyPt*MMJH{?3wzGXM;;f-d*P)qmpoH_QLo86j`7R4|iP{M^ z&LA0$_o<4YWz0n|!@h%0MbIub&vzif#xe-ZGAaC?VB@nD z{Ppoz<3!9DoOSigWDFnFe`-4m!jScl?(%()lu?MhJ?^soy%%r}_|dI`gV0R>L{6cQ z>8?YZ+dY$MWmXRTlvcy26tfA|3m>@X62>P|Vhn|JH@*HrtF zyG^3IbnCj$)J7auo;;7hV_uz)E{|R=n5O1nhfN4$(du8_spu{Mm)FnzExcT~bc>j_ z-iz!Ct@mL1dB+g^P%SO-DcAG$S$-OgjmubiFO2NQKS>2B%gONRSFV|>*-y3Fg-g=0 zz9VyFotmzCo@7nWD76@YE0`)qT|L9{TPLo9S7$09vEQf7F|{+jn6nJOLo7%}W_PtZ zc~Bclc}2#Obf-hMkb7#_xxa+IVDI@eLBp3yDe4#T(bLJly_8(WY_vO(Sp(JKEb@g|llwdq|G=fYC$JZFBBRm(@22 z!Lti?tGT^y{cPyDiwnpJL&(^;66$tJ^bL|Oc)0xwY&dQn9>Z}4#^k*YWVjsbi2F`3 zJ;+p=^GdXG-e%@&nTE#Pic1Tw-MZHF&r;c~^nTKK>1D(~JKOWs*kS6yjFgt$YwS0dArbcTR9M;-bH6`MaF@T| zPDQ7G@__~k7xaDDE)ojjL)F{BBJLrOH*I09jpwm4cl-Cs;4m-U_sqRDMCvaNGO-o0eRK3}uZ=hkqj}*wf?}-yoN5 z^3TrJLL_wdr^DsC@`c>a&%m<-s-#1jQlY9f=Uvs7|?^1gnV(=nZUC37A zv8+vfN2Ps6ew44YBr^6x<7El+-;=l&;qMe>_}M?5#^ur+Lf5lc&x8&M;{cx@M2Ug|B>rXwR1I5bRgU?GUiKR|vuq%}KK@`K0on$noVrRqai> zI9W8QrgUtpcCn4+e@cA^O{)1__qpfZ)~x>Xz0Me}cO^NZeK%ga2BPKW_X0~F@rK-b zc-~o9Oxl|lHOAic!{j$eQy)mcuAK2Wvwy!v8zdS}V!Y;C!dJrgIVpb+&7TY-=N|&5 znC+mDGsT-w>j!o?OD)AM_t-7>Xr`(>Yb!tBu>nq zOw*1MJV_2B`b?Tu^+V$wF5->i|DxdG|7l3CW+f{eQwaCZf(xS94ke=U&JlW zuz2~J@{&0@LzTks?-q*i5B5p^znc#J|HdP7s>b?KnHJWt3d(10_MVK832LeMDlevH z`td4LknAg9e0=zUYu=FRL^JgA*Dbl!@<<*zlUtkHlx_o0Y;>gVU#0WCA?tN*VfA68 zuOsC^-htM-%sto9LoA;&r%s*>7EJwMF+B#gQK~Qu3~4QfH|(ufvAYN_&C>eas$5w& zgtYdPJD0WEETU1IXCpIlq>`6r7uf59BsV>(b;K7m~CM9PI#7D}_2Ez}W)(|YIi1tS+(HWrNV z%sEMmGR;uxP}rBOY!a^7Sc63i8@~Bf_dGlCPS+!d5`_rJR%;+RmNnC8XqYI3|f? zGKbx%LT-HrK}b$ra(q7H<@Xq>>)%VDt9tvONF>RO9y8Bm=}1ct4gQdvPG*>YD>?oA zR45rn8L)&J7XU$eD)j}jn)k3xJ2tbg$$#t{BqxZ6ZJLFLi9 zUd!%Ngf>5}zuj^vlQQX7K&E4FDL$niR*ZG5yYbTi=>xX(aI0{e4W3J<4XIw$;jv&(8``|OhE^!))&(dBu z8U7`wjB(Drk;`l91Ar;UGFpf1C1@K>-qdH1{;J~K|K>5>8d+x)f15-6@uxwMP5=11 zTaCp1BaOP>x2T_kmd(%W-+v}Jo%Ys^QE#JG&S}pfWmo&6CHbpTzhE?o;h?GiLrd)` zpTbFj=K>-R99btK5=2USL^an@!~o0i!>i_7_t)Pci!0M_U(58?Rvf%jQZpP%I>{BC zDTzkJT&*QtEI{A#bKYy`Q4Uz96x(=%Rz;B@Q%lg;*VQ^i6C%W=5H24HXVkBLHq*(6 zR$4#Hm_F9<+m^gPwER7Ne*CH`rqS-Na}~KK(SnKx|6xn|EIo<6nsqShEu!m}3nQ?r z36zFVa(ITP?_4qU?an3eBW;6W;ZD3|F_F9XH1$o8(|0g|N&zjs%cRH?VjcjzZodII zhz26-$CH}PJZC-k3N3_6EM#2_VHuY`akM0{J=u6>_>mt$nRPJrt1Cnpx9wf}dBf*du7Q;VC$h zxOq1z$P8m*)BJW)u8EO4N8{rqojX{l5NG>U5|-@jEe;h!li->t*15@-FT><%^S1;Y zNOt47{*m{&hilbCUc)9>Bpvjfg*U|bh>&W2lPNsT&p+Cysb zUXitU4=#oi-^f}~l8jS7quQkMEnRqx7-P(QO%43YkqWcUjnMwn8) ze1;NrZB1=if9$Ygmj|jKlKZ~bE2s?IhtRU)ho^E#Ir=Whd&m9d&eD8F3^c#u(fj2F zmC^)hjz6!hax^r`XUR`zk4Ptz*c5rPuISCk`(&G_n;47rZ8@E!B0H9Vc)uObAbE&V z^iOT7hj={+bmZLuNIHN5D+4O3PEYyDx#=M|-H8VNy>E~cpaMBs6`mUr7k%}Z_CGQy zJj~h!)wS{BeI@1Ny?PEN^H$3quzJ|dnm>Z`OzCk?k_8C7@=mr1r5}jR! zqa?q9POSDn$Beg+hcc3EP-U^HVGAzEv0vF4-!^y-%gul5%u)2oiNUz$F;0ahoNQw` z*21N4|4EOe)DAm;$qJhms9jF^=}w`Ow|4vFPi-N_+47)l*;qrUndd8Wycs{UVD>W1 z#23%SvIEnwvr3kXNELh_&MgJNA#k9o<^P(r4&Xq8XviMM=>s8JSfA`Ft z4I#n7?scs2SIgZXTV;Nj4*I&;W0OfqlpIqkVoN3$lxpwEAo+fyIx*AduZ|SYoii*j z$fU99F+XPa{t?Yw0d(b#FA~nNK{>J(pw{c;yDnXG_n3S?tg|L%aurq~aV{-97prmH zWNnQ4^Ss=@2orCg^Kq*QFMmY^$HgT4dd1>m=1~fgp23$TxMn3>9@?3go4$Z5clg(r zv4+SAn#nL+(ehndgQs22s(UMKx0qgk#Mt^?YZAV^>a*rk(UMsyMxNgA78hi z(B+WWr}|1Ri6N<}-G?9AWDV5q1#A_|q9r zknttJk9Tu(7^ULW)54&G&@?l>tK0^|_QCyxn{n4uEmPlmi4C#_S~qox*7z>eJtBWR zadI}M!Ax#(9KLm^pDIYUS*h!uHu|IlAUWsu*){nfqG%O39c+Y# zo9tRTE1x$Ofqi-{#eH%<-jZ$`_L1Y5uk;lT84nQzi?dLoAnZh*TfR&T?~&7iOc0k| z8!jMNU({V_-X^r0GFyGk&N7~-X3yawPSbO~R{JR;U;D!WY4T*3+{~8s$`xBS`Erm1 z8XKayFORN~8M&$*acG_{D7Pl>QM3-#*pbd}3|j~*O${v+3~gL+yt7GV!zuBgW3fA5 z1TB0&BquH70j9a0?>TRR$fhIeJDn>lA|xW-JVDoa+K9*g>c~a3EzuC{y1EI?-8slh z)pSa^gBp9Im3BHx(}S05e`5u*b-DEpM6ZJacYM5ga>7Y^Fh%8qBC#{K-_-N*?XR+S z(h{H&dH60r8jjAO>vb4~2h-W@mh62zkk-qlPcnDNj6@#ADOAR7t}7?O!NKC`6ZO5< zPxgw7v6C`&XA1chq)r_)IGqxpcb#5G;mlMtl_y?-!{$}pVS?y*RwTmN@_)GNxn&-P zaGXc41&vhHj86TLWy|6-uXTkPl(Z`-50cO31z^$S6GLQ!9!)X@&IOQG$|Ol-X5_!} zur0;L_T78}z4SQ%ez2wRF7&mPoUPuRpHnteB+g4JImeGPYUA_X+B3Xs)_bLqM@HGA z&oiHe>ZZ@FH$-{yaf_U{pAruRWVU?fEXLwBsD7 z&M&msF;{$%eA^^N0qO3=YVXS)kJOZ%l;aT3n1zejiA^zXVW$jUyV+Np4ZyCrlIJ+a ztzCHfd)F#Q5Us@g>KmjUxD0lVOK4TNWrl=`Gg`h{+L_) zmzjUmt9Adyeh*U14!4JVgG9c9Q`7FqUoNT)W8kkCx4MiDf245Y59@w!*0p_*H+|7i zo<3g~V1@Ai_!&9gGy0b@z2KVaAsogzbutspFP6wb5=YEto$Yd$fuzDADbLG~H~w%^ ze~IS!HaY}+DF4g+2kF>jRJ&caITmVYX$L+9ERNT!;;O0^zxu@aaPP{yK(tr<#mMW+ zgArxO7U^LPApdat6=`LRDESYN2oF?`TR?vx+)_n(23dPTKo~?cy&o!e?erC%QuQa! zDrQptaC;e9DODm{>p@o3{4(m0H{}oQ`4zf4Yd9Np#=jm+M=uhIP?S4ht&?v@x2GXB z=QsL0-uRoYlGs?S8Y3(U2QSu%xelLkHp%JV8*Cu;Gdz?m71Pt#48arR2p4P(_sF5v zvBDO$KB*jhM|O>S=SN%pk~lfi1h^SI5m?9{pzdPsz2 za`T?rJjuP3eFTA!k-TIqf!q&j>r>0KEjp2Dhc(TZeuKR426h?AlIxN+x7yC!H!r!F z!#Azrj48P4qaiQb6^u zK44Mgg~M~%NpwW`eo8Zc`iELL5dT6GO{;N7zMK41^zPNW(CQ+X%1#cJ19UmZ8a_RD zzEc#ZPl55p^tV7F(ibC3Vk1`<%6%$vKU)o!2*nO$)ML4(dc#LapYAp>y9$-BM}eV- zbHhkVH_y#Mk7=%1hh#xE5nO+Xv>M=HIpbXce{1VFxI}9qo zb(dwt+Qp}G!^?KOQsC+g48OmkE6I z0a=$>2|gHhygYKTqodjMlh^9=V|_e__lArpoVvY$F2BP1%o`7N(#ZOtp~=q!WH9E& z$dtX@jT{~kmA|Cu!fZ&a7e2%vA=kFM?(0w(yHb+qnPKq2cDae*I4dW~f5PvIkK}4Y z|9zC#7@1ls8&~9qMBdMFnp0Qmb**5(5H#75+@wW1(HLwTw9OPsIulx>LFyNZ6SE{C zy1A-a`hwY1<$Q!QGJIAly{5`kFS}Ark*4lB&J(YfJI9A(x9@c?PoJ$jDOf9gu#*;V z4Sg$ECr=aXbgTa_tf}HUC~EY+8Dt9Z1{|W|m%?XsAp&h)|rsmxMWmFOhJkg_BXIBb}NvNcUH9+VdaR2osRE^HNd7zm+!LVi zG4nTwhD$P#eiULsj&TP8A`|Eg9{g^%I>k}@H^_}L_ze7oTHRf$jn&^F<8M(Dd{Pfe z*-6(wYlH!T59JFGkQhs@i@A&)8-nLa{JOr0{$7{H`O|NZIVDx34?3ohg^BS;6@<8hY-<=M4utMhz# zdS|@SMJl*6e7HPh{MYgJOEzrf%8DdheS;6mvJbEg-+V>V;KZ}$_OK z=a1*F^jOZxa%fl}VYEx^ zZIvgKD&eBVB{>whS$HM=9s(HSX5jecH%7-B)z07Ls1K98csjgzx;>~aanCu{AYba= z&rLmAdxyDCp}r?A(Z_TeWG+Voiwz9Dmrtbiu@8l?nAW_!LI%d3HFOxisIq5`bkSj# z>ldP+*oZM3cEBskZdIlKO5M`P2;&2zByQbY&B}d2U0qp=#PH%M0dZFopwR9xF%5z8Z?OZuU z;oDyy_|@I1#6GNKO-CQ6%G=9KOiF-MDmcA%uy}bK{e9 z#TYPfJs&q?D$xc2H?9HDHH?|ExT&pwjbE<+0d$GO5WGVN2Lke_b2KO8ysHDF=IISc z><%|p>hI0f=zkT%yp96~_xoXj*ib>eAtL9w@sk*=cD4*!!aA$`09C3FQSNsX6+ZL@ z5gF-OeI;6~I3LjhHBv@@? z!^1KxI_+mUHoR?0c(ivyD8A&H@}<5186W@Ukbm>}OHBndNAd z*4`{XASPzZD$0@y%wF5p4oXIi*9?$y>;&jgnk8j zxy?tBm6{Fl=IJ;G_G*58!yGI50h&V$;^5inC1vvq%0*4WwSzp2t>laR%F_aOc}+DN zH#M1?Ri`9>8teJ|sKGBw*Jk_U;NjYHi_uy8*xnva9y7^D>#&@!$<%Va9u2Iuj(Z*> zs1QAux(3)yMGfKGM!1e&Uo%U4s z!cHE#t}%1AHU~bK*#S>CNJce`X@d{gp+gN98PX}h?1)NQ$QgLx+{O`IAb3-IYhJJd z=N9-*NLIq5os;Ap-3-q9mfhVc!x13zWuMmB2RW4or{0Gd^erJW2Np>Ab3UgAd=f;4 zrA)Rq%E%D&X0uKu;l1KuLfNz!u^aq=Pk-=pJA0IUGx=GugeDEvtw!qiZup=x6x?Ce ztf=Q!D!ndtA>50fC1VJ`+dhnT)flH=NJ?^^*zCaTClFKILJr1MNGYazpf+-Gr$YTr zb7T%JI)x9G0NMjdGM(@n|5hcdVK?1cxI`_) zl@m2FL7{O@`?k%kL`^+XGO0Pay(tc&#?NWNB zA&KgoZVC{`>DTt29_X}q;_sZ9cMd`?Yuy2gBnXfE=y?*tIb=22!R_+H4tSy44gd7| zt}0d;fSBAmU`4RYL5F(W&gQ|X9>Y7w#9x7{syH@P=dR^;pX;~)xm{&f!)d<&kT3^a zR`n6#f1=v(kN$v?fcisof>X#FJDFW4{UmoD8y<21)gzjVvKi=-KDOXRDeYi(P)OXt zR~Y3aoR|7EhxJ$jgWHE&E}420w!!$-VoA0_$WpTXmV%%-8(Y528Tp9a%~amN3ze~% z-MAeogomfokw+~3ach=boG5`ok^Kj1x?AEkmM%!thL(Qq?g(XHkD?Vh zq9UNjKCIJ`SSJf)q7{pa@73u4YT=b%oDQDrUV+Se1=LgTQYQ4amFPW+xjUX-ZGrbz z73cj)Y^UfTio9EWHwzm2bIzB?pWZwSfed6KdY>0A{!B0y#a`JQ?w=2$>(`JzlyTpF zabH#K?`ag3g@D{}2Kuw1m=#X)6besES=tE(@TYO3<>j%QFY4JHZIs z&2LuiY1*s50+psYC?a%bb4pYFmm07n>y}5Y)Zb)_V=fSy1h0)HdX}L32vv3rMHIbBK4Y*fzXj zav?+RGk8O%)chMn-1#N$3PxPS`gN(u>Pu9j!r_gRtta~*y-!FJwH_Bf*FU81t{a&{H~Kt;g_?-FpV!29&&n z%Z%6;h5*M2u-QN%;8(Dzm~e-;zZWpdRRlsN(ynh$-zpdr)u$|VNwKk5xyw*IbPs*} zu`j>?VVczGfp9Lb`v9l;S}zl6f!wvZp5tbJ!nd`>f*Kv^_6}RnP0oV5Lz^~0(U6{@-t81WlL)fh^czOsc#LyI*;px@`@co zYU&V8PRV=Abpde3nZNai557m<&%X{_F!%3tr+gj`)Hqq})%8qYJ)a7*v6#dRNI**c z9>nL?JP=2bofDUiO2Am=?*-_Szj<8t)#{{Bp|_2cRN6V&c8`{F_T(_qSZHM$po@QU zhgCRBC|v$0u7$D~CF$4+60BqqDYt~01veIDE3NIrlmh$pSOvugIpBe~Bw~hO@KSb{ zDq=EH_dL21;p$9)aJyW&7+q7RLVO3HOik3EyXqgAs zJL2SxTFvEG1G)4M?CqqMLM$c2viCUWN?$a|>KXI9@ujB12TQ*8vPx*Wk|CWV1I)K% z3S9?veC;F>o8F5;l#H%8or=3s_X~n(GhrEVv>(hSwT%MQI>bbYd@#^_yae9`w9#oO zVm?u$r)WeLKuhucnB}PvdjqWVUp350PDd1NVm&oxRXbCOGTuq1-NyDCJeT)^y!Vd(TUPZ8s9A7|G?SY+g+ulTObaw(b-n$p zfU&{I1<>nfe;eMA3i9lv0t zm0o2%u;tmDX3)|qH?jLHN%V=*_49Sb51}V2c_+6_xKvo^bMImS~KM!hfbL=kH>N+c^~9tIdNfLfn0E> zD{d1b;unz+Pe4&F&9b;kmqF0)3B?`jol90BjijvGM0w3SRIJ|mq?oXXO_cmCzSF2j zA%Kl#HoKt+Xd4Onzud3u2^_ZiHIGvkK$F7= z%<}(0W>cmH+71~6Ay8}^wfyE0hlL-LGLu0ncDEHPjKAIep}0rEppuE?dmD&>|NxG zCmYNqD#kWbHI7wfDGnACko3{c-rP&|-bBZBN3LN+mTEmshzVx3B_o}kgA)(sZQ1q( zO=>fCVP$zeG>SGr`jMk4roF-uV6sSU51jGQeBIyaj`h)=ytVki zxt6P_SHc#l8n}cN2Q&a}6$PCIQSZ?P1E(nDR3S#uHDr)sGpvu~V>K0ibBdYnqnws{ zR|~y`*B^rMGrS4p_?rv5%hsu?kcq&FIp*(Zv7~!TF@v9Zwm&L!eqq1k8X4F3Af18J zg2z8Nl=E<;gaaRUxaUY&cA%4y(8*3cE_SjMH-~y%VwU`k$*WtGDEiB=Vc=K&yXuHC z;K+VY4;?;V+JWP+6~e3MKL@^T*iM7X?{N%RwS$+U8;*~zK=oW2C%fe8iZ`H6mGzY0 zlP0dI-&%8b`}2ysq(qH=yv2NG^O{L$Y|&Y!K9ND>MNsPgSP1VuGRE*bsn|4~g+V1{ z$z@yX8*odqlZ&?#)5ONS1Nz5}^i&q#no|yYcMr)@Emx`0^&ZJIZo06HGBaKWqX0Tg z#lz}Q0l3FH-73}IB$Xyw%NnUr@>ORQvyptF5V{ZtfxJ0DZ~AJhepW0xB}6@de5M&@B){R^$G1Z%S|$&IT`o-C%?bv zpXj20kVjD>BI^DsLVo)Fyhr%=!&>kx)TbujD~S-5KIH^iKsb?y@lw2$q;L7tc@<{S zrOe1gdWQ2+8+pDH4k)h@B_*Dhj`x2`6mDTT<7CILKgoPSAwPDj<9mNdXT^iQrw0pK zR@1L1S1Nm^Y!troi9e)Ow-X@J2_31QxS1Xs!pNC`8?q6y%(DTsQGDrM`)wK z@v5JM&XD4&5vfiUCRxLVqAp$xq;pYKt>o%8hUo&%U-QdBc0`zm!Ah{d$Q_Xn@~U+z zIO3eIIg2pkne(h`JN=ubp{<`@2HIhNHQe9E+nrl;9zMjW1 ztX*n2cW+Kl5tc%oTFNsc=@6wLsk@!ddra!g)N-Nr&h#uLowHqrpSBe#Zo^@n4JzfY z$qvp{n2zJ>8alQB@}u!GHw}mLOQ|=Xh?;jF^{hc<)?${{$jvHA!5Ui0-cE3pSq*+U z>IoYWE(HG+kqhu&=1|Hyl^e6_Vm;jR&f( zNi%xgJu%?(R8F`Iygbp7ao5ThsU}l#{@s-X*3yj!chmeQKwPC7IGZxT=OyldALCm` zh|sW=MFQ-X5ZpQ}uVm8+p}}$mDq}<_S(UwC)YT@vnSW$Q&Uk72N-@g_d78Us(C)=*PF{CrQNSU^M zo!c6L#h$1D4#F6Z7ZSFcOsx~zTki z2U30*$>a1?9cZsksO7ij@p#;{ z(?h@XN7EatKO{w0;8u4ACURV%wZF7$mO*HWH^sC%kn_idQjY3r4vHjM3N}x-d!OxB z>xW%c!#ajt$_k?f_yRHWJlSAmTW>GE%dhH_il$B#&B?I3 z?!=hJ98eNwBbsvHAVSR~&`g>#ymT%McdJu&b^kusajBmbk~2nvnRCYgUk2jeocira zv#?!U^eA0#uo&HC-z_VmO2>3P+dTW;m+U27z0Qr>c#dm0zKGW!fvipMVJULWk$EpS&{;?veYonARkvt-Fn;+*-@=2i(0@EU&|JLJ0sE^e~V|}{EBBt z|BHA=!LN8m9pOK=3v9{_0J8tIYhY6}wjn2lKs?Hq#f7D zXL&>Ux&eAL3RHnQgH1g(gf7klR}D$PalLD|(tZTzI}$beKw}~hkFP+_Ss#^LTr!;e z3TH5+G5}ESdK>)?tAC4X{IVUt&}~lEBOox-2A6^Q+C*P$k=dJCmJR57$Mf9my}vhC z{r_DQOK}B4TmQ(=a8D;(3i2imJ?dUVb5uF{%)Sk8{Ii{HjQs*p>sk697P@ojs)#!H zK8)(Rk1U7+9+`lnnSm9#8V)a>wvQ&S>jL$MJ}v)XpvwrOn06=ZVuZd2n5X)qa%x*- zxm?P6S#{U$?-iW$csExMt@I@OArH8y_^%I}$B=9`Sn%3cOoz&L^>WiR@D#9(l(Z^wnEi zHY%(S(pqa8&1=ORZ?mO<4S@-y)cUXP{~v&DmB2;n7Xv3360V1xW6@7#R`gzA$Ql(+ zrl(0ru2;DWA5RVk5pQu#hSf)-`S9ikpY_Q$FPyrqBW+RfAmpQ&>Dek9Op;>3E>|Ea zs_Zab!Fs8qA3tuk&sm!%eSMCtRfQ*&dRwjwfAxYNlR@ZQEcIU4IeTNQV8nNanhu3< zS-?DotS@qS0+G=@xmi6|&#F-kCpv0b?MTih--}eqXdbsNy))`}$qX|)gIJnHPz9YN zJ8SJCzTEp}-_17SIVjE)dG(aOiBnsBu-QuvNRoQZL-tB-o-szfM=LLNYr^==8)JSw z60B_B4WUYWxnB;lGT_3TWb7SL);E9P;#*o@55gfpNt5Zq{5SN-Q;}{O|iLxJ7;o1w+nZ0%czzMzJlKoWwM~=KDGpQ z;{QD`FRuu~$F!y>y+smMWaL2otcdGwx&=C42v-U0m7K4xif5Dw!`Hb1cx8em0CVSK zb;woe;IgjvQ4O4nr$2{6U2{Lilpf=|SjfrQRkfx3UyEoPH)IX1Pl|278wDeM< zs~Ot-f%t{)8P6p1SHyh79^`xaJ>Wcx&L{&|_A3w+|A2gvTs!1?>d!yB>Sz0u4(dV} z*7BD+qiqPK9LI;S!_MrS4e{vO-5CSaztPt^lb-@-@l4QtEoFi0=fQr$wa&3Vdx#>) zJzlMiLUN`{iGwkvWy9k+m<~Sk6fks91fk|rhqkT%oGT*!jyA5LC_sPxYSxdafMJ{N;7_`(d(oSS5qBX1o&)a?T{8b!kx@rDTKyYz z(E=_(zXm}TmA?SUKN<-w!%M&-c#~l2cpxG@-W(6XapCWh_h=Ak0eQ{?ZUuNJjre%K z`VtQa2>`d@8V3R2dEJ-wBL_ehT5nM)tu}MU-7v2JMV_^~FxyW{(Ed=Qm%OwHAWLXh zXX|SqY$a*8vUQX}j$$wLi+$q*U<{z>xNdGdb%2EX?M3re=rs8B-w>4!{8tf-H84CF zC+p0p4sb3Tl5!c=IDUney{{||bV}wH3Ah4>E)JH~SXlR^Tm+BU&;+Pvz&uN%7LFfg zxHQ@un;#4H^I+AX7L5A1wrSk_+-&RS=ezcYdmA+_HQq8*Mw1A`JMirQ{BWMSzzUA$ z91MB>J`fHgbqeTCNwzFn;j{p=FZ^ID`5AEzb7S?iPKiQop=8o zsTewXKtgy*5vH_nQP79f1Er;#mGDQSGNZd26LRKT(+M~iah8ZM1ClU1=D@?V5#;0E z%aIAVD*u!GJZ`=`+MBu0e*~SA)IIX7b_4WipBMpkn{yWBka%guw>rsluOp~U_Y(0i6?mD#wsrvucHxXcGbD`oCOE@3Vk#@ zZINW#RMGfpq`)8Qg2d~Pde5&F7o4z{ul`O^QQO*i}lBg`J3Q5UY(eRu9>v>;7Y@xX_eQJRKtg)6KFZBAU)wiX|gV2w?W0ZUc38GRipju=O ziRFN55sa{24=9dxv*D2S1^qd5@1<#aa(naY?oWEQifw4DN|=~%qi9xVyhxbesd9+^ z_s+HkmVu~YzqoHo{xvR;gRdggp*7Xh_AyAjI1*grC)C%X=^Pl0)T#B0!R34FS%aMRtM}Ql?{<8Epzwx2 zVZ3LcVZjhcewvv<1wAFBtTNJ~NZBJpkew4OzJQfTwH8FcF_Dk54E$gcWaaypT@UX+ zjj~;zERM~Yj=*lRpv{2!2$%+#X|*v~@jfEK_M{5cN1?svXr85TF4$+x0P-C`5A_gX zw=4TSD0O|J|2LD43R6-FS}ebO->ovr$;k<${O~aJYq1_WWq$AXv!*5;)ZCI3 zuVr|QbF684rX+C@JY4`=@-rhIbBf*&5<6xV7dvSSxOAlZ^0X*I6~B@6mBWoAlxw6^e+G1ut=%K`nb`O3Odm$)_{Zql9P)^6D~j1c{(V)U@@MZ*H5M)S%fXW?9FG z6@D)jE7|IW|7bO~uKQ&x06m}jcyy*>eaw0?<-)ONS5C0#qCR&+p8dV~Pf~|8Rsec57O2$;nD?F3086XVvuy?#(Hh|@cCgp1%15`An z#~6dJa|o}~Y0r5%gZVtx)-%;8wQup%V%_I?M~E&yZxFb>EpIe1t%D&b84@m>Y*PqZ zR{2K%EK|Lyq#U(KzpU)Ac+J}G;RwgzDoN4+`^N%m-Zd9vQwx9oLH5f|1y^8jVNGyj zo*1ctD^QbV>jwb1e-Y5+dZfzr`@N$wPWAF1=>E2nWKw$}cCc^U*7CoLyXAJ~u}*=iEW-sa zw1<;Ejby&+Im$aw#`EoJdWsP{m7bF+#s2)og+j9fb!i(__#~!|v3M0v#2o*E`qP@a zjLAj%yIS|pzIa945nq*~XsQm7P>eSu;^waH?6f4;33%WWxw>7gBHVQAu{G34fb z(o zas$tb-Hu6yW{_)ol+FEfwolcc0;JbTKR0;l4+$=ip~GYi|E#-55B9O)@+fnWA1-K8 zV}tDPLz}_N#tWRf&Sv0UWHO89sXWf`hGnr-(y0bh6Ebl>?P`>XH`Aq;d zjTa&MWv!9tmRuO_EY>Y20+tDnru$(ZS9(fTsAhO;ogXuC-|pRI{oe4(M)fRx7|0CC z;Q7Pe{=3ptjc0(li)>iGv^zmyJXA$!?a7K%&z#8rN+IAfBH!=Sy#P+d8RC4mQ>?t4 z8>E+(bqYNPch^^3LX5Wq_1GkBDiay@ew3lTp?!2G2Nw0&tYG7uyRL-i{wPIH&)5_< z4yWyY_J-@*;}g3MCrm0OLA1kY&;CWuq!i1ws1I{P-29}FcXMG(ctgeMPepxxd%CEaeGux(7mAS| znRze(d97d|BC}MI9Fbpn!VctR^W(R4U4alR(97!HlOy@i`EiUJz^YOLjCvd^WH`!h zxIy6n-CX;#x0r~Y+Kdc0`U@hgJ#q&V95=&Sail8i`{kY-dzV>M$9vgjVZT|eMDxBfAfDEd?&y5}Kf9!yvw{p>4ebaiE(=j!+%KEWP6I3jF+M9zHJNjW>k-7&Nw@opB6y~+WP0VORf(_jBRO5Mj4I|o5QkoXu|0qc}j z!CnjRTokj2Bp-Dg|GBE|k6#zE=FGt9bB9dBKGm{^vuRVs-u-Z|y#c-o-@2DWT42` z$*}pN%gzIapSiBT{^2IjRibG>UX+eZX&{Hu97W>Z@dZ36HmAR8IACQM_|iUtS^$nf za<-M0blF({!b#=uQu$@Z%dxN;8c~W+2@;P$)cme=!vWjW*9#tRnf6Ghnz97OBfTP% z`6w**sfXY1&Zqv)`obVB;JxSV_1-)Z9H?k~{8U4q9nrL~FsZ>2HEZzJ@>?~7g4X3S zpL#f1yrYr~hD&+j%zs$idEc-Wnt21IFFfDn+yd-q|;$ z?F5x&Gw))eDUYeca7zm^E(d4OoW!d%1XZWa6NNtx<{H_r!vLNp+MtdfI?>eyK5Y{M z!g9NTXGGw1k72;iE~S})>o-_55CKIx@z)Lz(ipy3P3X z8hZm{4_gG^BENVs4WyxbqhRlzR)m!_(JTZ@6BYXBhLN(pIGG^Yv_V za`N{6C@n08hh}~4BH%=Hll7^HWs4{8y|;MVjwlVk{^?d%Kxqx9+!}^Ycb=cKbVmtT zD`)8rwnCs4tl1gc_@oxEGO}D$xCEE9OOO zmnR{ZT@ov=B!=Y~*W``;#q{eRLCVXj8MH$VAr&QD^jrF7@21P}!0FRyUFos>0gx!m z(sk>;Hkh?I_m{Md3i;foC*u$n5?KlcwzKZb8I}stek=WKQAP^VKAWi4$mxD{nTQ?I zXjj^w-_ly>P(s*0dk6>D(DTX&1^h~W`-Q51$rgVL)`nI2UHqgQ{3Zohkb^JJb#`TNcDr=0NpuYr^QrFc0^6URU zssR99xG4(oS5YSLC4UxB&iqROrE2wshq*+fGB8>H;GW$8F8DgE{+DV6in7lUdm*O) z?f$=b}KTtSvMLv zy*+)qET&Jc2vZ52Vw2({#nA&@0r2;jJ(+gTuQj%ih|(zOhq_8dlEx1+NyI;?QtQz- zXX9N~&3r8m(tm5fiRT!T$J#QkH~Xc`{wUa{Ky9af>18bzAu)>dlT-Xk=_}Cf8uVG{ zM~8O8TaEayhW{2)R*Io(Pn6#ng!0xH=v!B}&( z8&bnFxu1!1>J#@BO_=V-FwC9Wms8QYpH`&#E*=-)b6*O zhQ|6nJ&#O!Z461CI|*rx*EM%x5I#8=B6eE#;yZM3B?g>N*pWp1o|=X0LHw?C+WV=y zVSQy)iME8b6t9W~bEsZTzg62P^ENOW5udN$2dy^`Im9_O8hsZpe`CdXu`1|k!7s<>Z*V z6mo~+-BG?Ecv|s!pQNS4v?xcv#kJFzJ(?DBUdA5C zrVSV|xpHxy-ER8^dE=i<*gry`^-ko$VwZ`mFz6mH00oK=r0q`Zi0wj%S3#lU8vq+< zLQf!P$_|R3v4$4@#L<;P;Is_@gLTxIf0i|@qn|k?5|IOq(zjYi=MHiuj}3o5$&RO& z=FWhI_K1ooQj3?3%=GYdgygs((NpJVEgp#$&oF}05ot&{jPi z%KHry76hj_a0b8S$G=Z;s)8qk3JGiS`yseD@v1b^ygm-=vD8WJiBbVBQoH{7s4f>P zJwht_SqFA%q6L5!JgN8$1U;U3+W_u$oSy;5spw-==J#EPM@l8seVxSk_F)g7@e~a` zD?)xScA(eFKHf)y!hnPB!y_=ED_1Dy;ns91=c>~_<(gu|9LE-~5s>l8r+9?njHAy7 zJP%6`1JAnMmyPZ@*?NB%0wwpb4Fq@ImvP<7L7;^443^UkQZKI1k4hQ5AMmw zcK_=+ao#%r9mw)RB7643Nt##FW@_lR+}m!H%_@$^XGp8Fx}1jf7NSE7Yi)andBK|v zFA%D6sFChiL1^$d+5B(k$QM+6;Qp4`9Df+D|40q`m!*)C#-?9h)rzM6Zf^KsP8pxA z1J5C{^Nx&cSB8pNbmj|v7C&Vi(GA-jdLp}Y@ztzxwKoF7C?Ek}HrG$sdPmr<_sslH zOAvSn*yYRRPliMFFJdiuGNPaI_;NUcxeY(|msX68;(XX7pkVT6GW|h-semniS2{wOmgxF*AtrBhb6M}7OK}~Xl8hf%NYI4FyDzrdZS;>z0>!tpCeDYT) zCO-^1_iX0KDQ*YwyUw8dD?*K#Vwe58^Y9h}^4HM4JK(LERea5oec1xXzk-`T{iS5R zkPkrIvUO89g!u}zDd1Motc$9nQx*PWz^!L8g|=E>UUD1~E!Jt3+;ltj<;F~(pDYwxCsN;o zYTbm@U$2Z?g_+3G@kL@j7=g=q|iXMEb?bZXn zn!=o8g(!TAl=spC^h&d${121?gUa;7RRY+uCtT2WXH$gjE*KtE9|hc=0WcFkg8}k` zy|)-TH`sa)u#!oKx)Lf83+inzR7BHpiInHUP%C}enttZFzZN)t(0MoPOIhyJse;o> z{Vt`)%q2W?rkAAZbeX;13;VJ%v{deP55WDUIA6#%&bQdAQ#V^I*f5cy!AvEcDt81w zI@`DSs+?wUAAXh*eo=p6;xaBtZom3cAyyC)D-G;M?)ai@5+&6e>mj@{%9>9Wb55|z zPcfL1(W;!Mifxnc$SqWhAEYqnl!R~OMvkW`)S*dyIo{ennCP@m za1ZJYj&ryS3fv(08r;3x4m|P$+atlM2-Rz} zHEiHemf<#4F*ZWt=()5oE3Os>0tvj;_O2|UwsD)1l=Lk;so140lRTEGy(elu{Dweg z7cV+$eW0#i;ryd(Edf%xtqDAV-PzN$Db4@sB+4SHD}8&hJ!-3lgvK)ct75W^H(~0+ z9j49+W$hEq_MTB4b?Z}7K_k#w8%cb=i7+3(4xs=KVIPbl7UCi8KUv@3EWl8Ih~7g2 z^G2B6vT3UCgQg8bGZ?+a7l|*?oCHSE^tZ$mUizT(_TyG0y%~O%Qig-xjIHN8B>H27 zaPJ8mbss_VexCI8;R~^SwRa3h_|?)+*@m#nw%$J{Zh0~Uucjd`6G2B`#2U91E8Wej zpW+`-@?k!}rI;n12e++}&%7ZqH5;m^2mp!&#$Yv5Ix|o>2CJnj1PA+BQssh_BLlG= z(7e~X(}4?4p56b>DZ=!u4e}h-BEw=GJ52Ci)Dkr+|I63+z8&u&RbrSmW^|XR2GKWk zWdcbEfolzmbO1x<9CMJf3XpkAQGhS{tU3$93Z!#}CL_$3uRuJ(zB=%dZ0@Ly}Z7qeVs4 zw9`ZgEiBXlrE*5SG!G+WZy{t4?5<*#_T@~ZSzUzrQosF#0f-}mEo0s?`7Ej%yyw9M ze|H5sfRMr0UDp~q8ES!E#r>&Q4Zn3Nw=Fd;#W6b*ybA$b_XB96m_WX#x-zC`u<4Wc z3;Wck7|ifjOD*$QGD;H+;oJqX-47Y!Y#HbupgGA`iw$(z@Ygd6b?7un42?&WDVWx$ z1>^+m{N|o9+Z714zIDe{n(HMozk^IGDAGYV@a=e>P*;kZNNGZ(I}-NSk(!6}WEDgL zPxq01zx+tkJzSyqI%*j29A#ZL7k-aFI{NdiJVkp=wz;A3A%FAlt$B;|Ziy+0eA94; ztO*ivWWvPoION1I{zd;S)alj@*wSYun!x4-h>e*^xE-Fr^o9Nf(A@w zXD>q{9p)k19PqLtFo7+HGyAyL^h4o_;;0I|xDXmA4fq^{gV6S0Lo|~J5h(Oj8?a;t z3@+ghC3;5BF_fV=ba=rGv*_(n0n&Soc9BBR*11b*q_pE5-=-Ez_ zTr^8k!=WsgcqyM{9(XC2yuZts=8{@19JPZ_l#KdvVp|)`k6*Bd@iT};rX+Zh-Wbw1 zj)JsYjG+c3GZ7W;(S7XRo23^oC*@#26k?r_tN}us-=N*R)(1aU@vVOZN66sU-IGb5 z8}n}P3V&){2iaWcs9(eTS-#3nv~bDO{lbOF^&{^p;n&dcZ&>E!W*(Eztd{P-J{h1X zNmqD??Plvi^Q+q2_3+jVvoC;mfVh)?;jR^qu18u=5VK>v)b4XJN?M~&a5c^WUa8-X&0YwDI>%p~);oi(-r7_Bd zy4yX@CPp#=`-+1@D7o+0s?GxF9f!Vq;)s4FF17J%PVwy((H}7n(=mee+Yf||h`l>| zA_@7zZs=U%6ZN;@)R+Q#=>^5&rOU0OOdc_nUKKRyO1OOm;#E^Z(tSr8#Ovjela1C; zTkY#iz4-~GV{JN3S0~#KaPG;Not~Xe`6`4+tj>t;!P(?Fn@yw~H>z{_0ZWM_0I^>y zO8(vW&R=KwkH3J*^~$d4YpiKKf%A3N8~8REO+|-{C_MNa_~lF(AA-Z5iJoOpK~O-o zQ6eVj+vGAOl2L#spc-yexN?F~QHhiBh8KrBN6d@t(nsQUy%m4H2gCtugk1M2qW*|^ zOY>9rIi`}MS@xPCF@5%IU4z(NN5Z1XiNp`fhV2@22X-apwkHm%+ICD)`7`Arxi4{^ z@+zg3i{&Njc^AoB&olgrtNw2)zk5&xw*zmkc_;!KVrl2nZ+Ba$`bP8WR%#7sUavqk z*JWWp&w6HEqT2fQtIgdT4TSE?flk?7F`!7ZYokiAd2BY8r8`1sf0@d0ad!;tO; zvn$0qmWtWR{!`&5qo&;oax>L=Wch&_EL_}<<>Vu|Bz&E^N%jnfR|nimQPpRVgS?Jt z^vLz0!ac=#)iU%0jsw9(8twNtea-I$&#@-~sYChN_j+{VisL$!jhO_VzUEU4ISG7# zViNpx=C!hXX*t}K=z{ud*p~Pc-n$`G=WxrUx*}t~mQE-Chun!k5bO~##(@L~8mP%- zW36^wJ87spcK`TZg58m)QkVUjs^7f*!GN*=MUB*5AVZ#$jG?`m+Mgju_sk;XI1<{xQn=$Y-irCBdHf6V&zv+x{r;TbfwGt3VX;cz zs0cMa-a%`L2$KT61Xb8Wd_dxgE%K3*HwV`{OVd{72|cyXT^}}zpm${PXR8R-2ERnH zFIe)-v{G4-hZkIra%~=IY|_WFac~<}S2Ihi;W6h5w@a8{nfX2~g{SUb$o77($*s~H z;ffz1p{lpSDIV;rLR^FhdXG`URK3>|E&t+6<^FG}v#U&ex&i%}&l7hA!tI1!VCzp} znKVq|ukGQ#bOgW=b0&BSyhJnFOCEaUU~)`|*UzFK^EAFpt?HG={XS2sk5t~+ zVn~|$=YU~&PiZ9W7#>0AI`(Y_sZb7knS=-}DOk$$QW+?jc0o>s1oc z2P2$rU=O|RT}R;!;p!Mx03QV9d3_ne_ko;^yLZ2K>Yr{nlo}$_(wgWe8UA4MnRmiI z*1_oQ!;Z_JX^Qo9$GNbgJ(5Qi>pD2$SWyNt2vyst!rJOLGD+`m({*~28i?GPlo$T{ zL9daA;GX$#tf4h3vmurh`8_(k-YBcvzNlc>!tb%UEeJKh)Vg2RUANQH%EyIVoSr3&xR^ji@`fk?eb0y{p- zn94m+uhy6X@rkTEHqqXSPCqGE&r+NiFHW{DJ9V}}jE|m1tv<)ZYDwA^+jgok%R_%zZ&2DK<= z&-kOc2*PblpczNx`){i5RqK=|8>5fOMh+Z&q`yu)RI+G?k4qSUzkcH1vr^|K%4wEI z8JCOVM-Hj=J;xYz|Ha&I0Q=yBY=lwo^6<Y&z2LQmBmiYy%uqherGQ14pF8pv{x4 zd%RT=DnD{4-em7CaEnO2&Vqdq+EiD%0wHzcJ$Zu)!^0x7Tll38mb*99^I5T+JuZZ` z6-Rt~P&iFgmr(8{+qPK9;Nb#WfP00g&jY zt^W~;{&$ND1G9U>;V35E3tD*b7T=e&bNH>CszOybQGkmMy`k;TGysh1Z=u4AXt)FP zTf_GyRc@h~Q3@QO=7Uq9K{4}GecW?RIR{_5$6ixBN$cu^K1IGin#&Y5evkQppwiEZ z{CPZY@1ujZnaUZtO!J>Tit!TLwdG{Yq~tr?X_w0dwJGGgnFWapxB~B7W7m#!(7WVy zAADm_DIre0Q%4GkVlu1i`DtwFh`cJix#ACYHOwJ~p-JRg>R#d*lKG}D_bI-Xy&iUHC?cMilH5OGARW;|Bee}^s zA1Z;Pz__?I26FfvG&02HT>%0P`!F9mGKg1vm|KI0TQsBigt?>$Z{8nhHuqIe-P7M# zqF~Vwx$_+J@r1OcR>N~-pO_RQTZ|EBh>PJtYOaYvzOv6#Kuovl9QTd+;0v7hmRx7s zms(>Xt^9ZKBf40&%kMS6pAw=vJ2<@neC-)?07hE?hi>=a z)dwNf`%nD?@cs+hPN_q`07)RL^ta%XyB3`1Jt|x#Qt|#;E-1-V=^q<>o9&tzs3vi~ zo$08C+=x3R@mTnKty6OM{$e2=I}gJ?zZqZu%cm)LC8D?{`(cZ*fln z{$`$x(4Hqj@~o@e$Dtxx)q1Yl_KpL(k>3p%r5#F zdy59%mzhl_WhZdH5^%%P)DWg(%-;vmzt@mgj(G~05dDH#CzTGKRPJHXS-_3(y`gW7 z4<>jJrie`w;1i6Wz`v*KkBb7E1(O2h6H3Ll%^Ywy2+BO5ce5#cDVzeezt*U|ECkP{ z(gPk2z~XtF`UoC)3$%J{QKiW~K%N*_7DCaWnNSIU;%CNzQh-mb zfpTsy@Z4g^^?71*Yi7B}Lr6{L4eK5F7El({fo?*8w7m(??GhD23(r@E-|t;{*B@We z^n+L1c7Yyu^MY2HyP!SNlH~?NFYmqT0!uI{XJFA-*Uv6!xGV;d4&~wkSR8vr#lc~t z{rvc40A8ut!xZ1gTUjmQ8Y|6o_g^}2^m!6=>%ay_y%SnU0JrzGsy>4#oDw+&IGDwL;~ns zOaU0|3h<7lTZtNBp5}pH++oWk(TtFc zJFX~tt}J>Qq-GxUk=7QGy9yo~Wf*o5MHE|V{sYeDd0EFn_JBaEjjpc1g!!j=*lGVH zA?oK!h-^|w#$(#`02NDHv2t&peEthY3Ec$n{d9$c!$MkhD2?$0+1-n4ui=%dg0EtOKKOY54KyZ77&P(J-XeVqmcwj z1LQDJUVox)`Tr|T7@Ih?hKLB7J8GPw@3G1?t4yX@z_znBvDrzBNh@PNfBQuc_Y7XQ zU?Jbx@RT;%S>JUZL{F~-BiXJY#Y5m^f6&Wdv^^_2ozV_KOAD^41mI<5TuVCbd9Po}Y>5Bbq?bD?>D ziN>U0@f?YD)Fi?{6Tat&&yJ zjdg~!V9_Q!xpbhgEZN%vsnT)L)Q(Q=n|79tZgX55hBmDeKGjnl7fY5XMK3IoZ0N!E zk@eC#i%vH>e&#OOA?d5pH(ci$doJ>|g_7<;kLJyF?D}7}o@fYSSaJHPCg3wn3nqh{ z^mFOa^L(+$C7)_5$uxxSAI5LE`AK`l7Nb3p7L?o`=Ow}*cb$6i`&)cGq5o|Qz@o_0 zMu?c8m^)Hevz6K?PnTM)T+*Hfm@S8kV$woX|Ll+gSg1_b$6kVIs9T9)brU(S(p#0Z znCFJ~v~5H)2(;UbpA||+etkELl%PH>>iGaPRW<#=SP`Jo)( zZF=Z&9Iq zrh%RWYQDP>tfQ+SWTll-0%>laWP3{{H^wbeZ)=JDc&XSF`Kr^M3jwp_cJyxeB4DOc zLf5!#w^EN@b4HajUiC`~|A2gm=&+3jNOAQA&#UV$t*JU&ELZcbIp_ZIHXC`TL>GsB z-L*z6?KDILepIXrskN!+&uT*T&~{w@55Vvjn2_~G zjRu%$QkEGA6ZSIo)-X0qRE%|h5zeWEZN;P)%I9+SqTM%se zhnE*I4r5aGea2F%%nW=LsJ{9zcf_0-u@M(GNbt7=#`UWL6~3hRs(x(_g*ug>7u!<^B0UBY+~5tmz~^w$4--i00lhdJ7zEKA)hssp^ z{-l2YZFeE6=2Axzr_s%e+~zeVGfsK*oV`_F!K@ugZglksPLaJe+$rS=tZN>p`=kgg zpJ+~83I=C{Kp~EB2DG;E1%gJ0zz%S8OcqJ5rP|| zD6c_RAq5KGAS?0Bl6t@+PzJp+v7}al3;&sHmW{h4AgsZvTdDGNO`n02js=ku4x;jX zaVC`xQ0xg(%#zAhEJ3RBp4e!YiBtbLnoKF>qj_qn9Es3n++(p|sYq`u+>RB(hiQe( z$i^t69!*pI!9iVhO;K$oyk;;*Lht108UUZ|;jc|#5E0biqiNE=4Dr8h6kgXGatLse z;!%z5*UNd;c;nZR-|R?+iosVd!x^zYFE6v07|+1r%wC6b)J59H>DOG5f)%Ezb9}Pq zv5(>U)mk14spZNujN2_9Olp*p9P)C=D`?dW8ogG~A6GiT z>G~`Y-%^wp@)0A%ndQ{ts;A#r$CNZ@`k_S*d>ZFW%6`fbZcN5DKkwH+RTR0k$O2?- z>TA(xRQ54`v*pKhcRr0s=%w0^W**}?^nyK{LqPofH}rb{z1H{t^h{vp^O&@}9k<*R znPf>exOByT9VkVeU!Ie`mO?|i=14Xh{zBn13OX{C-6T_k6IDmf{#S|01!jS$Iyw;< zf4>p9t^k|u8v$=Bu5WF8^uhWM;t_NlS90d2uN+YQLu+14L3N%+yfTuA`@YY>+Fa52 zL~ZJAW8!};bl;gWDguBP zna;mBPqDOeDsnv$a6r{iHggBfl&OCUdr{W4)8F!S;XDCy>b#mQe(L_91l9WdNLW=4G4)Vrt%J>B%E;MmQ z8ZO1H`}wr#%Y~@$7#$#(E3v;MzxnPCz2pE1VI_0qB|9J$z0|`Ph)(0i7Es#?PpJ{N z?)jJ=7m0a|(V8p(g>T<^?)~kq4?VXQQb|iLWuYB(`g%jCYmG#oY)X%VquPasRv8qD zTw*f!N2{OUe-w58b;aAyi3soy^IE9h05D-C!|<+Q#%}%|De6W=k5Pv4zoNn$0pR98 z52QD_?%spnicWX9XG}szHg7E0CiQ0>YNU3o?6%ck-~~3GDW1vy&>Zz$+ow>nEn%*884vHYxHn+_JKx+yLXB?yJAQi4E%3n_%*;@FLuf2%Qk6?lCXY zu0{UlZ@_7?W!i4Dge~PuBfRRoX0$^Ac@A66PuBndzd-{P*scPm2@7P|*~|fH6Ug7JLo5wVle<#2-!+ z_YslLD)X=NN0c6fF3mEl!XKNP1yrPOw;*lU>$s42QZt^e1#wUK$)RR%YTxhzmIk9_kRyK6-*q+0%eKs=^ zV608+dz1S^NAh~sO3kdbZ@C%a|3IcStPmO z^s!_#hH7OiTZ{@MSgnCjt14xwQ`=YV(~8cZk3&}<*7Zcy;ix{<1)xmNCXsD~e6QQ| zo31PiHA~3JEY%w35pyycdR|0!FM>yC&kLDo8`r3t_D)|K7)D&myrcsA>CiyY^gg?& z+$Bz-3QK2{SI35f!eNtLh>KC{X^K^=y7s9Q;g{|Ge)g^8og*dmK)PqS1~{mBD-OFj zsvzM81g2h@wI6?ct5S%{JPLkEd1ppMdGo3B)iXrr?p0`&_wU>}IB1=A2TbjLda)7e zSV$DX;OQR!v25TbmbFzfm;xGcXy6%gxH&O{87XTru{rho*1=N# z#Sf8=?q4uK-;=~-Mn;as)>JyXwdwA(O%FS!b51PAcQ)nE6}!_V;<1$`*pmB_8~DmhXLS zr?lLp@i!L6$?qjz^p6+c>}d!i+nXnpe%$`qNhRo_7O8A7C!+_tq^#%_glzv+0dC z2(JCYukKm%=)3r&Y-&+FQ%_S=6dTFc^OYG}hO*0MDCGy8C?Zd-LtrYjH?3z>>FX zX9}WZdOaOx%I0zz0*aqY=Cvv+vN|fnM6kyk46NhdyW(l3QmSwy+c0y&FuGQ|*2c)h zMK0(XVa(NYIT@tJDe}LTnd%Pt`byuu)~Uj+Db_4Xecw?`8dAz(~ zH_kVT6lxH-bMxe%A-cqWj(J&&Eq#*?Lh_(v*0q>L%lrVy671bQ;%68)cUI%AR0Q*q ze!-MUgk8{09ItY(Enn)286nTvyvZMVI$px^U@&xaj$ZucO1xI}sYk{>>Dan=1#;OO zlwXoKVv~TsvN}BU~>s8@yPmCHlq(eur&q26uru%nVBs(R=!0_g&#u*6p2K2*G4d9(+3=|^rG=r$#g&8~uIr#|I z{(^A=S|;#DLK!VEu`E#jlq*Q+0hZzqdKCZ**FlL2c$H<)_ciiE_k?>$adc@8{GeJu zvBF00e#5B}{NytJKNXz+;)wo?KmShaxWsK3G@KPmX8 z9{2Z=`}a@EGX=MB?3a2)nxt~cv7kmuFID@Jd$TKh0rh~>yS-^2IBaWegQuG`p+3}MM!`G!+Q;c z*_&FNA5#Tg^e9B|z)*Ak%dJGrcTLy4Ik{6bhJBXAs?2uc*#Goj`^TG}`#|mF7mOU~ zLD&8a*XSB2R01LGx$!&pc7K%jN9H-YOz>)9>?ILDYU|s7zD}-Q8Z!Fs&o?wu4&Z$1v5yX!q`$4 zG>Mv&gT<&VAdGJzF0W>98Lf&@HRypg_VsCXY8ZRz@R~c`Rq3L|^RIGCpl$V)l#kw1 zQ3@~Bm&Y?|GMj{Q&G5d_Qk!pC{YL3L1~&=>k!xz2_&gwDT|7%=5heIOlGg^&GSWoB zGw2n0zL@?mNlZi9CxW$r4lOp3D2Y*tPR4D)n{s!Q9zjI+l(PO=0d{WbL2qlyggwm! zpcjfoV4l`t*_`J?;Lijxq9Bc`z*;m=KMP`4Fhr2CY$38`!|kAR5^NqU>sRR;q#P(H zA&FY%{cLjJ_d3?00Lni`ZF|QYD^Qd(Kr1~Q=d9GL6);DxqPhDM{G z>F*ByM8BZ6H6`$w_)a0DZ3ui;fY*-bQ4I`p8G`me?<;A(v8d-PMluTgm+8^JBR6^n zw5_U;K4=Na0~>fb3?Qa?3!ea9735H!zG*FIe&7ZHcpeV`ygb^XXRH+w69q`&>$fI^ zxr1)r1u}dPA=eVK5^{qWaZUxQP^nq-vWO?}&XkXBg9XH9tW?=zG;$(&$?cMt%?4rU_iG*S96)a6_v-6&AOx%1ribUPj4fz zY3CYJUTZ(jXPo48^n>S3FwWH=kP1`%mbrQl1AMkRK~fK?Z!gwHzJ;l6*kXqA6H(_f z>_p{kzj+N*WpIFIkj}Ef=ijvVX#~~UZq?M8eM3cU>iVLl%6dG7dgHxiwX_|n3Uh>< zM!aW{RR&4;G!Ei=hZ_u5bE?+=ta}?})Sbg?RZi2YoilSFg?{BV*4m($e)Wx(pSm1c z{srE#KV+Y4MsUTVsY;(1gIjnRe$04-ljZmF%cjid1)a;Sp;xfPg-(#>c>okG?aPOE z%wONfxvj}+D QuZsF}2Jf*jLo#Wn6#_>XZs{+696?35!^)$}4Hu-}tH-6#cm<}t z7`1+Ee;p$;cWb_Uo)P);cGtx?O&s+Ffe~L-^}P4QLgzzJCv+Lfh9nc_GB?b5qubPd z4S{gfV{*}pjj5C`SKGZx|Fi3T*Rrtk%Gp|Ysnk(CBPBo8WovRNp=yF64<`d1$_}}fFcFVD7cuOcP6lf%jEjBdm$Is z684aH*rt3|&pi-%DLkQ{bvK;mggpI*$gG}GDuoiy$qDb$enu^Qd~ooAfUgV@sQ+v>}Hz7Ir1Co(iK8$bN05xGMz+cCA%%CcpkyV%+JWPKG7cX z8pOIbImxTWDT-XQyBfjZx(?gP8GuWXMQ8=P7!UjADYpjaJ2&g1^0)CgR58-D1+-bcW&mCT_mdRvF`Bd3qJWzAd5^2s=^8XPl--4u3u z^$vRDMWuyP7yptVr#MzL7UP_vhj;Th#6x3~9s_ke@5#%y`pCx@?Q?EcQGmcx( zL#`t9Qfiw<-t%n#Ju7s0ZwbD-+biB+$X1sSH z#PDO{Yfe}e0F9BStgN<Iiw0s*{zDEC?6h$7Z?u` zV3T=+a|b_664?wFdP!P(0SYo9$@(EHP{-~|o!zw?4^=>kdkFby_Ay3$w21CATsEvm ze>p}CYJC0swa%vS`S%w*nM>5A4#fo@5imlP!rwajz`%V+c~00Wgj)LY@ytto)nbA+ zki_*~|N9nTV~A{2RFpiFz?&m_#!m`Uf+A4NMm161psOhJHs%dnUK)aDt(W+vL21Q< z*KoRN>GDf1L)Q$tV)E-~hLZQ1>wKxmW4=DKPxSl9xA~{y9b+u&UyBD1Fl#rG?wiWk zuD^lFR0)TBCof9)uM4PhSt0bY(8?KB@M?4bcyk2zr%pP6pGnsvLn>mOfy9yts6sbZ zKAbksw0*kkPx;5)@OSF!f1{!JU#C|;jNB-+6=0n=?wz$&Y90zF`#Vkt523N~>)=#s z(eFy$=nQ-U(bj89Sk0d3#lBj8U3LKKKC{64sx{n3`GHy46I6nA6XNStD7BBHGh(k& z@uzk~jTK4>*e?}`K?Iwt{Vv=+Glgt>UJ<>rlgXsSk$ELc6f3|+XtH~NuYsJxY?DHCX3H5i!v33V6FK7L?aIbb`6 z3>H2B{sTde5`yeAmxrd3kq2pmUoazpM(YABwsHq3ee_pulR&Le`0(Aa0RIu;-8RkUiTGwv#Km|i%Y^Yeg-CQu89C?I^;_vQer;P7gx9|iF%3gY2r4iQh_mOu!f##)|g_fL$Gttv&08(Kc_IskziH$wPA~m z=6t5lPjz)?`E+lITlEqlf{CD;ofyI5Nxnh#h_o<^JSQMu`?q?V|HVl+z+wnw5)BPWJ|ACd6wu(U7XnSv94r_}c|pC2ZSEs%cB(j!od zBa~|pbDT%z7GuV{FYrB8@NE2{vb)cGMlsxBY5!_L^|wFSM#*)Nok*s;(~E)7iUgG|u&So`o)UAF>cQ7q{4_)=Oj_u2MDf-rW0#o{pI=9KxW}WY?b1kwPxTI(y zyCMsF+=XQlofcbbY4+YGyH{4-XEaVZi2&E={x_~M=EbT5QT3O@Ke$HDpGi=c`^EZ z-+Og-Mi6@UVJ^yXwD8>*AYVmQtfH@nLA}( zU0Zy?oS++BvAxHYoC$sc?mVY=%AjvGxcl5;*u(xnbSR%tHQ(1Yp{^?0W6ro85IC3Z z)0>E0Kb41#uQyiSt0f=oa21Bli2ZA zM%_evD7~+ccK43i9@`w{+4~qxrB%~I8|Iu3iurZ*<ZdNxfo$Vs7fB1jMCpNK02J-OXdQiG28T{qTqd85j5f+odV&~K^^AJJ_N7Z~{tAva z&REw)S=EyF6bt)}F;rJ#2)rM1zFNJF3cWcf7r%ShHJl{KAKZQr+h10z7;rk+)He^V z1Suj^T+}Ni_-1oWem_7mG%Tc?DWC2L2rfJBul*`_`0Z+{yUfln7+^POaqoOYdBm!m zLL+JkDJ6}3IiMwIfQ0XextR}EX_Rg)@)<++zW=15{Fe%Xj>uPf}s3Aw|Yf(8nhtNxu+qoIm{Kfii;JzUK5-Hva$EXle7ob zMr0#&!KNzv!l%$S^>R&ZK{RKEx!)#g0#-Aq5b(5+@LWe2GvZqhQ@#yAyr7^oE~}@7 z2M>G9UuJPp-H02TC(jO_l)EM-jZZ3$fe!lvm4S6Mr|T0Jqf!dqlRR;rEhZ(~cc_)P zhBg$m`WFjJkXz@8@;7SuyCBVvxn^>CK z$8i}lCL#Jot-ZMdVUaQL&$0HY`AgOTRK7+z8vkK@<$2~Mn{LRWLL{gCzJd=J9ISPs=1@x`ZsBKLBF_6F>HR#CQLJ5qTGK6gdn*Tdq{j&55*% zg>^U8U*mc{$~s;4ZhX~N|5+-RYHBK!oTM#P=V)yzhWhP=l2w0}IR1N-qL^ly&h6B4 z#rhyek5}uQPiM+bBxuBKEA(v9S)SC)L+I4jl?@5m@b?tc7))-X2Q$wS<*$DLP4{P+ z86c5TZ(yd~${)Am|91O)1-M=Lczfob-d2qrYC?PV<6?Ajo$Kk3?Iv-}YdsNruT@SW z^`UmxcEs2(V>VXgUz-1%3h{?_2cTU3h3=UDz9|WcL%aLg0DU_-qRoCSdZ#ac&O2;a zDr0Kn6-`sl^|sr*TI8u2m0)La6-`&Dmg%=F|3glap<3=53#Y{veL8G+=~W~xHYBae z0@D0Sh1YQ7dLlK)Z|Vew1;aV9fiZgR%%6X+r`jbcgkkr+W0Mz%=XFW4|)V0Tj>-qrHM$s`Q-f z*+(_vWOV&r{?GcO@O%nWoaBn=;q$`%pHudQmMoWfBmII2Tv2~TZ&z&pe4yUJtFXPa z#1w$dK6#RyAvTu=q$E$9;;Xj!38mw>+>tiMa9F!lr+yc;lr`wt4NIt0}r&gY(iBc6()|0zVfV~ z_X-fQkSKvJ-ck!?lfmtCdVH}^D*sLKDG_XJ?~D3PjN~;(1ZU?Hi_zFzAT z52WPBdcq#9(0KpeF#aB~LvDV%$>z;`nIJC|FXkc1`gEwWr@!}LHo&d&1}XcLlzc+_ zg*{zG+9QQ;bqSoYu7C|Ge_7jgazfiKKKTyo{|1a1|%s!QD$KVdb4D)((Yv>{})U7 zk41yW-%VPK^*(5m%Cwz;283@d3xDogJq!u2M+VqA2m^K=nLzI3@bIAek+;G35!pXd z*NPlac)dV8xnmU`&>mV}4^RWttj@NVA2DA>0>`s~gOZ~{V zZJpQ9rAtX@p5_YF`CPAG6B^k9<+8bwA8y;|dXR5BVO%-s0tXyFeBFzDkg9Hj9B#M8 zRX>orK`d4dQq#e`fBYqlwza@tFla0HE>om;(T3pLg%L2l;hoSqV3#?^!w1Zo!n))J z3eA<^E(|C^+tqQ2@Fm4}AirtMfGYQ1cx?k#zBSfPk4AxefDqjWNVo!DLRi6vE2`i< z@o>QB%tjEBAGvcsvH|?+oE+L~=VbA7ic78g8S$b{^H+n=~pgQ8JFBd#XZ)!;tF4@e)z-VESHdu`$+MYmHT|B1pWoOpy zL&2KG?y>Cqj8OzRdu=GbK$_KiNSB$O)b;803}Is;!)xmM>BXnDg%Fic(48{f+)3xB zOW;MNNJ<=6n4H22-2C!ijFSqZ3?_LQzf{klAuQ4+nD=~+*qpIC8!ky`_oO>6-Cz90 zoNfg$A!IIqTtsY$M%?qu-Q2k6$Y@06mi6at&bd${PnlYbB7T`5b2sf*3~&`cU@(Jv zq$pO;P@HPULqTwO^Gpr5FVerdU_4Y%(K$&aoxjA={lk9Uq7VYV*>|D7oY&@ z=fjj~V!b9kkL+*CoZp7jOWw-}uOw<*cUT!B{N9nC2f{Cp@A-Jtgg1|XE`7au@OhJK@q=4=hlMtn9I|@YQ9dPvy zjxf8>eE~FH_bAB)SO#;bIGUVCeE2EV8| z+`!P7%9G9|`j~m^2rrduXNItaUUfI=-Y2}!2q*GC#jcaaTUpYv>&)+YSpQps7r;rO z^V|RdZQ)xHfH58oONW;|e!q=Y=#QouVDx-(BNV|r{btHtZO0{dMZ*;=(<-cw<-6*z z*~0(K-KDI!geJ$lq%RN5ULj2dQzpgS+&PZ{g3(HEW=bnN(dU(Qr4yt?c}~Q9(HqtBm~>yV}~l z%-UsMHT8l)k`WJ8vyFwDU(B+a%WdFAdU4l;PlfJP>{sEwN{=mn&WmC}6phouQk){E z=PwzKzR;FA{;q+QXh5t5VZjZo1tA<)B01-^M{gqB4|pXSV`?HgN^KvWund-NiE;@U zSn9Zac6~9#EX*E4YHvqu7+e-`udne0}l!!BlMR=#?cwBfZmj40^X}91_P9#PL8=(}PX>ks|=2`$O z#l3XZWV&<{Rg4=~_LC}XZq%O`OB0C|#oG$`O!0IXRO@CYz4VWvN?A+*XnUe?;%6R- zZI1R++_;fftebQVU{YffhRD#zM@6q%71HGe1XGwf)lW27U~=Y@i|I>~MJk1B2$?~M z1Q87nDOa@&a+z;G4{M-wn}o6rTFZ!W$YP`2Fa(ecKjTkeKLxx?BntLfCOAB({I7jJ zfaOtW=gBga(9V%cZf8k$4L*p3QXuzE1{$ySuI*?tHS<(u?g z7YHQXCJCKn^_d@*0hCFPe|T@uQOVvBZewj|y69}vlkS6?dn6j{ce*$ph}7Q7nVkR} z-?!7ZquF)4Fe=wL6H^$~M1s$AN1juPqE%j;-GRha6f1-WPgZWF!l0q77w7IEaSz2Z zWkXF)(X*+}Hn}2U8}6sM?bG!UDIg6eEE@gQ-r0rT4jcpvE`2<^T;M2KHC1kr@e`+iLThr`E+1G+H;PfF>HuaH_VUOpF3N^NhBbq zUkw{NS<@oVX}4$YzZ|nhI(Y8s#KID1I-YrxI0qVB*;~_{zsTXU&7a<#Ea7fJQS|kx zs9RrQOkC?!yq~g%1+PE5?}aKfVp>KsWP~y|sUqj}bcjD28}GxuZDPd$->gY46&|cI ztcn8D`BLu1nRp}OlxRov&Y!RAoju%e8y=9TzHm9>WO6iQoclPWW>MUts5f_iBov!< zccbEde1fIT)*yLG$o8moBOs|V!Ai-zD}M6}CO0^+h_dHvi5Fbc&B9yR6cF=;&9zc>YA2~$;gRyy}F>PGiTSJ>uBtIR;oj16IZrX8XL^f8uHL+v614-^{t_UO~Im@cAitn7m zS2AHLO0^$VZHQ3_kLD1em0A|z3Rc>(_jRLLcZ&(a@n0Bf~gNN|s?70ri zsu;Q+(%?4Vju+Y71oIeP(h2DI>_~I&;S}|d_=|_bl2s|fkUnYWp2wYGG5-Ut1K2^_ zrfjxy*jV#ql1E!Ap!aU%n6!TpcaBoVq#(FLjUgfw+0{n;PmnS38E|q3pIAU%`Zuna zp6BxG7Q{f%$EN+f-&eI3MRgw(JAu9fc=YLVHDS2E!0`z zR6U@0@w#X11eLO+PB^J#-CR6hCJ;KD6Hi98l9@ct`FwN1+w3>7Oe3_ zebit$w@lflbgj1c4ZzYTrsqfACnY3xt?=Xed-VI)PvVhNZKc)`?kqdg zK{xcpMTSiy^M|gS=DDbkmPdyrR$8-Z((C!eC4wf4WVSiI-JBecA?SZ&-SVF)=ZoT? zRC<>mh=Gp!4t_T9CVC4~>+f-&TqVT03UaL^Ii#BDF}_!5UOGq7E7Ut#q73& zFY3_}(_-6uV{RdBbdRtmMwlAu73V@3O`J-aWJ_5|O5V^M->TK#XMb&N(v5=*G>>Z1 z4x_|QVt8@W7Z>jo=jP^x8-!A;zI_8*QdCpolLcdnL~|J5%P#XdW#n%b^re}nA1ASuqT|`Sdr}eVES~OdKISYdL?c%S5#I=kDFA=RE8l+Nl<3q7hS1zYhaN(} zt7||Vbdwl~`d#9EHTU$weO$uC1=v`>xcB0C7EZ}uz->j zpR~{6x94@0vmH=0*8$TCZ>mix+xiy#**yDLUkMY2vFYZ4bY42%$?m0t6XQ5~l+zfr zGR8&?G2u-Ug^d|j@i|thpGCo#2)r;NNFV=NZ^_RWxv!Z#AF&7KFg!oNn&%YwV5Q<= zGV~JMli{P&X0KRb_gJuc1W6v;MskfAXWuAeYQu3gBXGhdK$2Rre1V~+v|>cnQiaXU z*jSGsi64Vr|8jwugY#8( zAvz3Ssm@4l6Q>)EftzIt>od5UoM#1LtQD!Bf8U{q6=aolY5T#SD@o#-?n+cUyn}GX7 z4q!z=*e~<%cr2GY0O(2%eEIQ~2bies&I+kpPea@WF9zMLP-+>?9z*JMfdLj8kfJ3s zw7zf6{Es9Q5NIV;P9T@yq=%>Oc>_!KopLd2)xtjvRio3*tNH_#)Se=V?iwZVM+x+e z-{}Q9yUP$sZx-lhMR(?Gafrj?Zdq#9k%6l=h2U-odT{Gx6ZP2Pq2J7?o zFyYmyDK_kE6=u9*QqDfrkbt+hl=1Bj4+LD~&>FN;ZJ*}F#9)q!_+mxp1C}?BLYT)j zDhlcajgsEe-GvJp?;aAwM!Sb_UMiNEdp0wvK=Q3f{LHEHZb!or$9HnZ>}iLrgjrg} z3|f1GZ)X%dIF}>0oih_02g*L7A1ZTG>wfeqxuC^Uw73!RrcNSQyPOhG4=U0ma*a?P zrimd^Xh^~14S)8jEtZoxI|bqV`45B+K0e}s>e+$HfRu=ApM~+)JGtQxi?HM$6zCN7sT^{PG2Lb_?ItP$Q`{i-(UJqa)$SUtGr72ZgtFm*4-OASQl} zsN(binKEX;ntH65M%)4Os^_47d|;PO+u4^4nsq`6*XjRWP+aY=Q}6dz^ZW94X1cI! zIm4Z2*T1Qdtyz8J@&}cx5H`uUnjF%Chk4v7HhPRJ8pG0@2wh8ye9p(iRX(sxT%(GU z-g4@v1y^#mcY5#<6~G)pl0q1H&r2_bZMn-y?|Q=r$CHYDb|k zQ$@)&aLW+fM|tOVJVKi_)5cVCU~pmBaC{37265f~ydb>ZjGqFw2XF2#7#*kxblC$i zUgkO;U@U2ctVcfBJU&kFcJgnJ(<1NQ-Hz9fbedfN?80oAQoD=V5lgK4Z~_RIL)U~E zKgoxEsa5*{Bs)*#k+!`F-dCs*mSVp@uw)T#48?Jcf9@yGu$U)LtdQ7G-B$;+!x9`t zf01@1?;1hnFg(OP-eo#tsYsC%`3ZkjM23;wR5UtIoVs?)B9ea9M%AA%**c1~KKNe!if-oY zbiIJ^SdEBx0<&o;lq#NgkS0f>$k`1Id&pubB5(O_zpKmDf0ZT_n3tRzjwVRag~*s{ zP)2b&{@r+HUOaYx8Zy;2z8JyPkQ&e{{xSk#=PcDO^rEw>Bu)C_sl;(*J2uj^>mE*- zk9^TC&Rf(JwIVayGq)TeoABCI5^Wp(l!t z18OG?ZG`v=7p6QeT${80z2Yz4W}Xbi7FD|+1C8L3)CdUh1xF(3O6!GF0S$C{G6xL=*CI7H_q>@g>2Y%zJ#H=~z%dorUBD{*kJYJYM4{S;t|6 zWBiAmi0az8#tlvJ%&WKO@Rh83u9-VbpQ?YBVOQ!3)mLjs-aU-5D!z6#{uW}~!pb?S zX&sAM7V7Xq_$xalLC8bNnx<<6da4^MiZoJxwGhxn-PAvmvsusWj*Fa0-yTsNfY$OC zeG5c!&B6>+O_GBAY2FRx;e$77iY(H!Z(xqxN@j6Bm8f1e-fX)|UDQfy#W$PikScS; z>2Js|(KwkCx*_!J+fidYoo^3}AZ3@`>0teWSW&BfcmfK(w7lesx^J4RhdPTcBLueP zRj*UtPk!(W@flU`sx9O7r?hKLNkSd;GbJ?iz~1$_04<*5 z8AX94;^2jCGQ9sgq_}MU|Bt=5j;nIr)`meq8l)vBNOyPi&eWG~P z9Iwn}J!>x5$1hg$R8A*@L0jmGhbfhLemWzh&qXW-DVF1Q0gt6j8Ewag1)8sCl1fU* z=PLUEzpSiaB~l4pRW+vOJ@E-^qn<;jdR{Er0|2By$b9dm^kXyhz+ubz2j``yv}rsU zL`o<3&z}ZcQcxf-ZkFO|$E<}zB6E^QFm{&)YTu@Wfs(BwY4M;MIICO6A5H`o)!wpQ z*lka=(w)((Rw`We9pA5L{O=JfUw!ryTt0VmEP)IyVgeqIE`(-y@}{{>{7!S%+yJcF z0hR`g?AFne@5XrdvlyrpP@FZ=e09fVhupGKqJ1@-6 zUe2<6ODv%{?tOI6gLm~X)0(2szZnPhOk~7j8=)fCM0{;r`zaksr7=Uc#9Nb8CY&_nTdn$7_KL2+YuDqjTRSaM+YDdzEhkQ0GOD@qc z<-?a3P|qkazzC){qr22^dH1qfPBFTC|HE)7LWE?Dt9PfYTkgiCHu8CKv#oUIaQP&& zHqI*nyjR9C_n8i<2^t#8R?e$Ze_G(sfpU<-d&U_^AzR-V#W!O5KAICI+rfQXmdlFM z1RFch_{Z~Cv_2MN@3_}66Hx{+a6vBF1qW7+j%V{6@8AS$pb7b#SfBn+Z%_k#Mw@FV z7Wc-f^4HU8cf`n5%~CvSyvRO~!N0|DtACIq&vmhUOvV%4BlQqnXwF84WuCRFfn;P; zF=sW7Nf!gP2-e>%M8V{En!D>Xm68c(pSBPPBOJh#G636<=_I9rJyI7QV*C4Lv-M+b}TV!3WH#K%c_&Vm& zAK)Qxo5XMfp1vjM;)hU*>;x$_wbi+Zw4 zO{~O6%W9e4F1?#!Wp59lk@kNre@)FpvmhUjhCpat?9z?Ymmxh)bP85mY z5L#ZF%dI~0Xiw9aN-(bZa(s*0vm@+6TYjPNAk#s=cZ!3tur#(*1X;~hDM;*TuESXC z{l|9P9wcb^8N7Rqty{a3K|0`C1+`t7vdeg_$Fy(jEF>BAxXl2*YV7d2U2{#K48-_l zhlun12v51}t2`1^G-!^j9#=;3#?xNtDK_dx_jD;RPAQSj22ZVN>FKY)GowoQMQUCR zW{iazymlLP$&XB*QGF|yOvbrL(QP`&0>;i|h@XU^a)CCj(!1~ltk-d`soa(!o<>-L za9vA-h>|VgIXRuer-7B6t%%n}eTK;dTI;FR%q}cjrO7vE`0~7vE&`{Pr{quX(;&Ud z6Iin_;>*YuC?=Og1NG{o{&_HZ*0TbE#*TXU2K3BVl+NK@?8m-Fn~1t*4=j3 z>&5Gk69Mkjt*2)M(%3*-=?f1~^v$KabCZJF6nf5#1`A7e;LEjwfE||m4<2LkQgtG_ zs-VA0W9m)L5u5UX3Yhg})XVGktrr>}X6>J!H|{P3Ic5&!O3ZtVo1*%Z%RZZvG!lL4 z1I2ad#e{#s&jYF*KO_^#%H|}?0X$CJFARV(K^>q>@XZNh;A|k8+zK;FQ?%BkvEj?qdBwmHod=J^!}gs%I8TfSCy5aTuBxWex=*~UcNN<6#?7(@bI`cxPbo1= zxxHes#HLY5M0#%!m3I#kaM?Vfud^jzfm~3gfz(Ue=WJ{EnAU|)Zds2ks^|zQWGshS z9VZK0_^8A+bij~{lz`MwNp^eJ6tksM9`sD5*WhGzUAo=6Ft|UAXy0yP)jZ-O;$m2V=zN4EeV$N-L46TE1 zc^Xc5>i=nf4-l;Xq1XTQ(_((qi81qJxQtw3QUvjSwb!mi=`G$jEzK%UOm*w|IpC`D3ZpNFvjaipn^A%=jjgC%)#>jDRu5%h8C#N~2K3+q-oo3>C~bgWIe^!zp~n=@ z%0422TOI6|z>Oh5d+RW*Q4Xh(xOAR=xcs%8~oEO3g z&u03A96(RFELHaU{SCrQ+n`MpeoQ;>R1BHW_{^vzKRhvy_3sE?A(!5FiTF0W ztCPNdUf_9~34NRoDbOrkmZ1%p68(cx2cB?l98_a=tE5tfPxlK5+QGwsgk$2R$2xsD zk1@(${5A~zZUef413q}J{-FGukcE@SM%gyEm;}sLY*bKxz%x{79%GP>u{W=`_cl+t zt0AHh21Z>A0sL%y+*?^^Fl==->IC2)$lnJAAVNM5(F1K)nP;+Kpa-hw(2pte{0Q*L zr(PfdH6m2rS3uX)#07458@ykS6K4J$q2%F@?j_Khqenn*S_mpUw+RGyK3N{ggeNum z5gnZKe1xns-l~lrZU9$E0>_inATDS~5#`;qTu)NL5&%;zAtQn#3Gony? zY=81_QOBh%q>7l?Z}kPE`fze++_8P+!i#w}eMSX^(BK4Cd;|tf`OhdM$`7)C@8%#nBk8nl#a+B;o+cpm%cFV7Z-+vvFm=WSU#BWg;<^-;y^}gEHViP3UP`&mllkeqS!|Rc35Sa307s#os#JaJw#ZAPc>30M6`1pjl1m<-z7C z&}Fa)#<;j4HSaizgBNXGx*uHn;r0>0ztDlMBUd0-J);l-$o13JOQ3J=@6wkir~x6( z?A0iv6z#v(nW8wuc0^4sD&e}mc6t5mh}33>mRrGJ*LvpBVqU~xXVe{BvZv-JAb z{k0#Vh1J}?#-=H{v+AncN_1&_gpZVi!VF|KLAXF=p9^wK73>bFB`R6FIS@`A!~ z-3rO96@M$${fE&6*ESJ98mihhre}MJ*QirivPj)?DSb%nVisf~_(p4am7&(OmJHw2 z5Y37^uu@t*7NrO!w&%spK$Z)|Nmik=ZiCvDP}2kuKV4q(DkFw~-Q@wCDO*D9(c6XI zjbYq#)x&|`700WNKZ+LTzat6Sf0Tp@_8%Xom~T+ze9w&<_`F{PYxxg?rFNO%^SOXV zoKd}~M)SjQRVZHsk_a&kZ8)+F@^%RVk_5tvTT1Z~8uqK0&c_VOlyqx8BUGb`z$~hP zaO12V&P$f^B3e>0Uen9|5^uGl00uI4eL&KuJy+)WMbu_<{K zHO$WuM@pEixw~9-2i5>>LPc4~=L_xwERpo6$MZOG^MzMliEF-H&%BF`za#uv zyLcY_e$apl+kc@3oVGcuECV$B6r;wGW3{_-4#sd-q+G4`l%%YZT+N4~>Z?Eux%Iaq zC@9HHY_pav`0}(BNx*>8!ifTkMh-h>3xRKU?xo?Qv%KcT&rx(U3+xI&`;sK~%*+4O zDy{V7qR`NRi%o|hN61%kR9aq}Ds70OPe5Cmg|ma~-7B2LwvhE{zXS)L;j}9S@{2jBKN-+A^iWwz`04@Dqa`wCY9l2 z_e`s(>UOVtekD|0g{sE8)n#e)(8cy=Xwd;kXX5=*!{+rn#O|Pd64&x4JRX|aapsA0 zNHSrXP)RjSg@h0U0v5V&cftnn#%cmHRob(KYoSghZj{#Bkqk|b&3f<)guM##3yaa} z@3j)(GL$@g;4t2TtvP=|EPSNF36KFQ-+hx(q2BM&)%adHj|Cn7aS!c1$$4ShzQ4!| z&hPTV@Gq2?oY|eABSv6~7%Q!s+xnEUZ4^b<;m+j|6SX2MP9Cdz^w6D;n`)E2o8yD) zHuUao6^LlYZLVK!^kR6ce2PqB_x+4pJ|&ftGpontrQ2yEzqyx>SBLQ+jis394_-JM z!H5!%=A?)$ojoa~JXRTY7qTsySd^F+HIrm1BeW5i1rg^~h*UU~qEnI_KIPWQ!om1N zGNeguPVq8iTPqht8xr(1f0m3 zebIHd)un=}-sr54;r0#9&d3e&DJAo|TB<@@8Mq}?cpVs*utnvGr12Zx+n}!m3{TG_ zO7Nh{eVt-;nJZB9O4%PoUkkw7=gEAQTlxwv>X*%If}Y~)@wD7!sd&Ue>^4>O#IOC6 zXxdR%J?--K7~cK>TRg<_r1)g4(qBV9vH*y)h6wk>Gc)(&PA=Y?(j-P2EiZLf&DEw6 zASkAy4OHCRaU;k>U`jWNG7ym9EE~Nj21V#Bt3g&}GK0lk3o@cuuo~lg-}yfYGQs8h zfNSQX;8iq)>y+dagieyyS{^MLw9Y45_w5}^BXwo#^WrB6pwkffi)r<<$?4JJ4h^DA z!P)SWa7%M6O6Hwi(1w}G&A8e?Ln!M?_XxSWRanP*gL zc+qw0>+#l)uof<9!gA|f)S-@A&uQF4klxzrjb-!X;nu3G@#5^1_1a-<_-+_WbG$R! zgI*9GKo}qD&dAz;7@?_3EkY}955B+914lilqHMb{M>)Dr#^!Wx%Y3e?*R8-?NWmd? z_IlL13)0fK<;d3fxc;^L62P_urWEc5$9}yQ_8@*qG6KoJJ@}yOI?Fm!`>muVotdz4 zjZ8~MutI2d*sMR4cYj=p1dgMmGJ^caaWOCn2@grG*&m#u3ll)D~9RNmZSu?G3J;DrfKs#1@sY z?p@S7UGY(o>eH$Nq)j>)Z?DD)do51vC?W?vqOxqRj#x`)a%|2g^AL5J7aXJntK;E5 z<_?88gCf2}u~z_1z-h+T4wWchqlEp7(9=9lR=qJX>pU$@ ze$9UEiQAwmPaGn43ri(V1-D?r)i)9ontY)gn!3L1S+L5!^WdrX+x@YNECkx5iBnDI`TB~X#bj^QnzuhW`ztu@v}x>c(sMKJ zF;B-qQH?w1dc5Obyb{U~v2IJS7RJrLkApv+z-QJ(MVVYR+kM2hOnw=G5i=Kmi8t`h zxxrk+PD1yMOc;;b6UxHqc8XG?d*8JDR?}5DrS5Qf&Wto-eN>I6eInR8eL{jZE`%x(;$lG9-Zx ze$Ks{rssAm7{(GgTd+qAAtx;7>cC7@0J z5{~%4@BDq>-#@<->K#n0>ZS=tp25$){^_fqn!#gC$Bj`1;NKxP!*!T2(>CCGRY0Q2|9oxW@( zAQ=8lgB6zH7q1jAkk0oH7k(Zpy6rH=D%Z(Z^`KxTfKH`sm!b?d&0Df{JP9jrRCwb_ zg~MlZal~4*KP4kRUmuc>OE5Wwa~8${;prkFFEpAM2YDK_DxJT+#_5o`bg*t!!0C|# zDU$_Z&}#t&yzHvuI7?G7oZNtY+7>!8?F3tbCyF5e)mGS~Dk3B$g%CPNq63!c2*TR) zL07*-Aa5k9~~U_(VO8f+S2kt9%IuE}Ck>BP*iM=vKJdO9~i>|=nJT0h#R z#X9I07TwVpL}H~#o2B*~NwXv-jp5IFdW(#&hE2ZM6yaJ$u%W&t5?8v)A{6Mpq?Khw zwz}0Mar(6U0*9j&k!hRo=|FIT>Q%Qbch1nPqSNi=ni-x5V1B7hyuhiD4z7CVdyC)&ZMX>N)K-kJE=f(vl=Zg~dUJ&b|qi zY>egRi=glf@`Z>fF?#Aeu{MAzqrzsn$2!+`H%XV_w4i5@m7uMjf)2}Z9c4oOtsmBA z1F$wf1P-sF6lgdfe$MVQ=qNf4@Xq#{jV;ZMVXjy?odww024MMGkag+j%kMzrMEfhJ6VD0y zKzJhWTul3tYYnT#>5gbeFUH@>FgaB0r+Nk^{J0HQS?fi)z&<%@n5QPot`?hN;L1ZFMc%dy4E&4E)H?@DK$8L;KO&P`a{a8pgV9<*K_ z#AcZ^9QlYCE2?hS`}1pGr#|+@3!6y-#qxAZ%k=LE|F$BR(UGX_P^UkCn+NrqMZ6KX zJ?<*e!B9r3ytXE6>dP}?Vg%gT_3i<~1Fe$8Y%*lcr`*c3zeuNMs9vAwF1OKwQ(^x+ zs5CNLA5DyKJ}{dw_l=-we08vmMIs$#0=)&udG&oOs;vw)@mH6!z;6Hx$#qT?dU&*(|UxK0-E2n+)UWYwzehxi9HKh$`JurJmsBr~QUWojB0-=E71 zBs3~6w_tfdpk8MA`X%JYUd}pu+5NJj-@lbFqq5R#Ex)f!_u@U>{MPXr$(MuhZ~5$k zOQB9{EE+$9h3eEL8OdK5s>B9dE`H1qGADD8$6QcYBo|)$%MXn!9DL6&#=fx zBI(J7B-i4fSvM9TKg%|H2a)Ya$?39&67qEj$NQWGN^w2YVJnQS=-6&akVUw@z5#|! zkJ?z}oj?tegi)k3D;^f8RI6b?dY^qqNPWH4@l06B`#dNondSO@YKh+$8m{jMNwz?? zi{z4JhFIE>&d;(j|angPyY*m4)T3$5+M8gufv`H=wz1tgZFKMYZR38 zmL>;t=R3mKY*Cc7UM||8UslxzCaVD(^U|Ey#1i3TbGfg_8^%0h$T8{p@#k2D0|6ug zrS;UD5eg6Q9?v_m5VmOH-!qZ$M~k6F>RhLK9EyusxM)^7`?N+ZE$Z`Hb4``*V1xnSC{yH^eH4rNg%~Il?d58wI7K| z!p260R5=tnC-P!5P5VisUadbFG?qxte84m~Teq08q3=BrvX;k$YkyC47^7&>7_F}g zOS|ySK(R$mMQ)-T3-b=G^+u@f=dzfOBI_QCO5P_+UvS~bW2U?J$REoe zryFe^TkyEbytc8$e-!Whk2I72Vc+EM*x;rxq0WDcQ-S~Gdj-Spi&A6cN7sBF&dq&tcH$&ymtW= z;3P!NJEHxzqIx7qO@-Pm@bf(WgFn5)`$}X9X#vPs{Jv!C`s+{_ov)AAvq3E*$sO&- zu|6GA85YVk0n?K@eRpu{>5eAlCi}=D^ku`TDHnn>hBBTh1X9wH-M0|ANZ8`Io8m$2 zjIPVfScFd<*NG&iW#B_s{QHQTzo5C2eefR-R}&Ll`9k7+M(DqA!~faw9Y;EYY*k9gzK z7h%u@WYc3`LbEa~GW9HwVEI$DMMbu!DvQ1-`c6t6t0K$6ex3z^m3$f--?Hect&a@N zMiO)u8nHslIi0ZAKRfh%H6LZQ$K}0Ll1fT1-4N(~OG2@$Kn-b&&vYjr#)K0fO{WHH z)e35Jh05d3>l>04WZ`D#W!&*w?ZJy-4GpS!<>)codBZ;AGlSa2lF!z0nB+4-+e&Gb zXVqvqgpUZxGiAJV1t!i$o!%~&(^1yZ!Y!h%i@qalx)E%FFBPMT%f!?3K8Q&V>6X1w zO&FUUTVBCZ)55$J_VMhl;g1MKod-s@HSWcZSe=(OWgaPL9Wm!nP9Jn$00lD?>Oh@} z8l>#hx!6n`(MUyKsg*d%Rh#>oM)2BGeN+3yhj9orled5~;Ti@L`SR^lC^qw=Oap$V z<^Jwi*%i7kCZIj~S;i4HWO897x0l(XAf0o;hnJ=K*6giuTA%w~6ZVi7CwI7wj70p3#g4@d|Q$E!-!p{s?X6g8k+C>y`W)K4g#ylBGr3YS} z&pa%x7tKcY&tJV=EjIp>X;}WvG!*}HrXlZ(3k(%}?^{Sbx8XrDmmwt+m#uK>PNJe? zO0u*HffBP8Sv6Wg&p^Dk`Y|FMmN3+)!)8uvD8zxqu;^_R;=Ce8LHpfO8rHw~Mp5A0 zj-JY?*@_Yh2;S+Hq>LW1er&Apx}Gr+vvvu7od|NK?HWT{i_P9@d425umMl(lgy0)* z`wR1IK^ciM{A78Z1%ldPwIA)nJ{|bR*Cs0+mjG8$=5MRZ?KsyO1m|^}2DSkOzE^&9 z9j85AkfR%=(cdfuz*}@qY zjSRyhQUrpa7($&(|G)$whpR0Hl|C#!gY3!{@VdXq)^()R@$WJgrx?`==~0CHaQCsj zl4WR-8f*|3z7cYFN_y{_E*Cu4ki&N3`%TdOU}!M`#Rj8`E|~6hRCX?qQ2gWS`j$U_ z*`}Jm`yJumlqC`YK-30@)5jC?XiZwAvDA;@(*#jv<9tUz;gt({e)LRSbbJs*fm)|g zw5P(T=3gIKGoQP}xTIw;6B+34J=x%01kE(4IkVIsU`{Y9wtFzIN49Da(#1VQaL?l- zIS)BGfmt`Iz6PU0FWI}AZ=aS0ti|lB>ZquXwrQ=!9wAA}5{z&BL*J8fR2CF8P|Rtx zJOe)W%FZc=mjE<{F>tm0)f)n2~7o8YCBRLwRN(iWW8y=_&R+B*+7L>oqdIXDUj zPOZN6_o>bqYMHSoC_l?bhpPL_*2QR-PCz>Cj%aqmS@nV~6h!lts8Q$!T;yoXAP!>8 zMa5F0(*1=&dmEe&yG2J=`O%g=(&P1>QD_9g@5cJ>OvG&~Q7IM!-6tWtXBI5to;!}l zJq6T`3S?ySeW?ATf`b6=rj8^lq}7|x{>FG%GzXt^P={Fdgo2itnJ;ZG$ASPUYq5lI z0QE*WNiS-zE9u(-y=q(wG{!#wPhR-dX~9R(z9Xb)z)Qea>8KAI;RHlG7@-Nv58Qb< zO*Ifa^4%#N@H3xrqLt9XB}HxY$ol(mJz>^!k#i235Zj&*(3fJCOX%e|?#=-)X(swK zhp{~WnSoW2=$@{j8nb=hsgzpnXm(h`k3Gq_j--gn=sgD`-tUXF{7HdQFc0BXVba!YissoRqC^;IR}sF z9rv2LaxrFA)pLIt+RLT;e}MFcmQpbEj(d@LU+qbXQjG6*)Aev)V~9SE$?GqU875LK zJ4}==seVj?T0QCi$U=n;1%Z3O!R&0*=Dk*KsEzGYU;|_MYaO0AqML5 z+L$O~#4H-Rgguv+rGLzc%)`?s8h_Btc#(7((+I)#^!uc{3&m8F+%1yk+rwo4xKCt% zok;AD_!gfVVegm^2%-FO@h&RA85f|U?*EjBm*-&$yW2rfhQZ6u@F>5J$QxDwe!IM* zx07RbcE@v~CTRfu3>0y{cHTpAe^$X~!T2LXXr($BJL>5YLqrL0qx^F==llyL`5<{C zjD^0tHBc4fFY5N zc%I?t4AmXz1)FN!o0#aj-!jDai8hpvifvy4L@-20be}MNR=pcz#)KI;ZVVyT7sXfN zmd4AstBv*A>Z31WmlV;Er#ayn{^KmRX-BN-dScL#`;!mA>|w*Sy*c7-Gz!#%i&;|M zZ_|29>x~LEq14-eZo0ZFXsfwBp6rApD`{x#4(hH_Xx_!zs!%I*# zTs?k$m+?Mh4VyO$`6}w2xfaS@{lKYp=)lowS0*2vnF!C~n%I?B#Se z$pc&HKlW37AGc9Jrn65=zat>SYJo#O=%?JUyf_JGx&(%y0gQ_8zayM)ftRNrONUuA z{_yt8N7vt7>P))NGHH9YZQmsZ8b&GZDtZJaf+57&A?`)GQ{HEFMWT11rsQIqa#Q{% z1~{iLg6~JPnH>=&QNDAk9-<&7QMipAeGODEKUZ57y_dTk7l{z3uXM~^5%$Bk{Pm&y zxEcd?j}zeK)OBkF5=tt!!AIhX4|QP5nZ_6EhPivJa093B2*8~VvQO6^N~>A`f1^bV zR44>@d|M(izT{rX>~}<|joKH_sZpuPyt0cruZNXN`8}GR{2`pVjhFV2xlS(tWW5PQ z0=o8M4kw0gYQX(OFCD}gnCk!yoQ#jnMca7Y{V zBqP^T%ir7&9k)7ag0Uw}Sb96DwPE#y`w9-%3l!(S#!Y&oGbn#76;&;(>SzMCAUnLt z9mFQKOavtfkDq6{z`*Wqg|!i9aL?JV`&u{VSm^U>vQ+hqmrXHi1AUr^oe>u7f87^g zvwUseM{D(9{1lr>OgJGFP_ej^Dlih3?+C~5ugJDCy?ol;#~k}R)^q^&K?3-V{zqqA zy|`aPH`>zMFAi(Yw00YsMRf|Bbh}(*Ki1@Ouf1N&AvdrOQ9$6)8f?C)zLOo0=eduI zQ*KYu()&zUd2}X+^6Ro4Ss--3a7T$#v6w%^}^+S1L<2rhkkM%R~m|@k8@`){jR&%F6zn#gy`8zn`BQ7 zxQcd-7k&BOjO%a)t$y|+kR~P#kfdbi!@y_Ck$EIThVn@D&g&I~m1wjwKlRYzqK_i- z3mOlb^MBncWJOBHa#Ci%zABTllzt5t;PhUVBU}kXj7qvELw1) zsiJ<0a)nK7u^7L!UwfycAWyI`~Qa%MHT?^6nm(s#xfZj2m<#OIZTnGw+i#0rqeo7z#&9#H*$RpqTTi?5dP#CY;skwi*PGH0c?>-mU-P z$^82H|J70NbrwJ)2{UB+dgB0w&s7O~zm^vEykkH2H|84058#c&j`K5sxkga`0w}}z zEt>sbWF#tS29yG%XRB~I7*%SA$B`j=Z1WpEebG=;4`YXty38enW=u%OqIv!It_Aag;ahjC{H1I zbx3P1L}7#7U-ap({ymTrfAZq?1sEm`K5_qu`T!0-@pJdTtpgIJj5m$& zvHFrk0rZ75&)HM6{++f}6c+F5ENZ@K{AxT&j95;(@DdJ0LE2D#3zc*xd~0yZX2X?; z%x=P4Iu}tD33B-zgwzG_xD$+#Jb#WSBTm?Byf4zW&%TBN6*E=eM}RhaPEE6g#a8ZU z?rZ)&SdO0uyFoF24H`El;Kxv9=GkK4_cihO>xB(G^%bkQd>`}KluxOda`I_demc<* zo_Txp#v;J)&$ZydK`u+g2WTe(Hiee73$|Ih9eGhwnz}j~YQ$;Xl$So|rtL>(#kQiF z^5WNco@YdVK=8n_RSl*yf=?lpq$bB!ddHv0Q@I8@uEptPiKL3b9i%bx5d&{%uyC^8 z^i$Yy1gj#ukB>x}xg>J7HtPR4;8dUIg(Hc|3(GSvm8qT#*sz?8X2bNVY|oY$&zwk# zURr-`Xkpr2b{i9{w&G!C)T1@>#AyE80k_I7YgzgP1uCRrdg$JUb)M zPj|P_w`dy~5EEF^YuAYsQJkAEg>6L{ z`0SJAnAehtwFsK{evZ%^WEVp6qfntAFzGtNO9*H@mWBfCPgg*~i#;Be?$GhR5TL}g9pJAyXij1!EY78R~K)IGGk&-xvK5XK_C z1iG>{fHVriVXT`vAEa7Hf1LbN3m9NZzkN;zgsxQ-0NotZttqaf`{-wvsb3qdjgRMm zkRd7fAf!}gbx)fAFZ$xA_K*PU`*K{nosk5laaKHJMIDBk5-VKa&&v=AdrFp`W1Tv0 zM5!_mLTQ5Qy8;r%-?$q2qF%23sl4MzQ$pO#IUjDGHZ{_aY~wIqTfZ%H zkH0NBCiB_Vj?#cf+{#`He*#S%s6I3;M2XdQd7@*&PNZLa<5nN@h1so|1_mCKJpQDQ-_`E)%41Kt0EN-bQzux=|-vyPeFA8 z*Xkz6r-AwqHiG`~C*-*IwtGQ^@#<1Vgt$Em$GMLS*Ou*h!nqIlBKI|&+Cj&I0A#i?zJXR2Kp1w``+gb_8n1VghEX%( znQONkJIa*A?61x0CML8e6ghxd6V)VSrAVe_Xco^%<*2Fn>QE6Cs_%x|A5c`ebMoXQ zWckMm;eDbF?wtK(cf@dtngPazcWAl+ik)Y6!`@(|bf~GcJV1-u#514ePH2g#lw!^- zV3Pq#J=^oKxe6i5po|C2qs`7LU`Ao4lb0juzP{C?|8CTF@Ed3EI_0CUcJ0kAPsV~8|kJ`2dl z&qX^3UaI<`SbtU29sl-%;$_l? zeyiiR6)>o~D%Vfs39MQx!%$MS%Q0L6ingMn(15!-fYp&PZf!e^9Urz4V;4G;B;*57Y_`E==Dl)sK1FGRb)FBoG%<`2rnEqWwzD0sOUoFSA4O^ zDjk7S`G-3`3mF8TV3=fHgeUuio9RtIzU&Ps{|^gJv=am`&n(KkWHVd7 za(WkHd_(lrkC65w&?nyjS-=A<(ni7ususOJeCSU}X#AfUh5p|fre|E)e{G8P$)XmhTCju$sr!EAsg>;Yoau{SXq|#NpxZt=AIRm zdR^2kyX($uh1lMZqY_cYjm7j{qw}94o+fRtXSIhmo8c-7lf20%6wofONV1!m9YzU1 zOCZLkeKDW&;niM*mfP1xC;KpSs02(U-F01bbiXlV?QAgbP_B!a5}g9Ib&oEUuhm%M zhe!YKMwaNro7xahac2va-qD{~zLgA9r^_6488lZj3L2dMA_(4~F>~S}76ElMuNo=r zm*aUP>twI7$Sk#OH!lUX^ke1SZi8(HbXU9xxvA7~KdDTi%Goxf%(RY=+b-${QM_X_ z<-hMeYclaNev*&9uFl>anUYeuWHmlk<_cSM*)=C6nLEI}l%<2Ty!HhRi-Ze}z9w}D z^or^MHWExp3giELnD_I1O3rFE%T98h6t{J1j)}pv5Y;{H;8>!MEmR%@~}L z9ckuixd$+d{)z30><1C{l^H{ZxLh>>g(mWEt|OzJMeP(Iec(fx5qLba3r_2`Ez!fU zLYJh}=8yw+KdhwpR{WDWsf3iIM{pDZCuKib(<}`hY>;+U-akU2)HHSrLTW z@8Ae>KNAckx=X<$61hDVnC_eL^7|p=b>B}h-5s;NCrorp)y=Ic2X~>L`k{AIR@EKj9BVUc?42hr{Jbf zuh}pjQf%2v;@}8@lEkz!jiRi?)|Edqt-bj|3)4W99#@5Y;xAP}a8IrOt5H^~lm(qx z`vM`TE-)KFDqNEpSKfHR2RNiA5k~N=6io*i_Qfcy2lF z0%J6}dKtO{-*7VXo^&YcbiYTTVvK@&pta8@ zVTIqTHDAIVd^Uo%9p^N8Gi(mf)6hr!+~@S}tPc#G!fO`s?~@oAFhI$)w!N@%1)V&p zmP+#$>w`bn9qN1L!6a9n&5oQ*;YtdpK;ue3FF*2Xk#L4RzmwL(vtyQk+eY*VN8*Gg z1mzRjX<9sHh{6DbTX)LW-n+HP-*nftvO> z_%-LMhV;~6Dp(($bJ!(-B*SfA889{AR-VC7-MbZ0XX|N>ES0DC73IU{u+|p|x^}lE z{S>Gw#;dwU^OiU7BwTl8q(0o11RcJCA)LlsP$6OG^?wzO*+sDXyNevBSNqT$7U2IR z<@pS&tp92i=8%rD77-ts$kIki0?Dpu4lqqHIbYG>y=yskUa{;mIl4^A2%S&9FcUDg zW7^=e0k7HsEg^vh5+X|`^h<31|WA$JD9M5FC$Fq z_MUrp0|jS+k=oGb00-GC7TI3ff|BSP{KU$^l&DVn%V$gL%C{v{^LLgVE6fu83zJ=_ z9falO05WqPu~U~WWJ6nAtRf_%lfN?5m%Rtc-i5b?f73i-^ zPjXyhlSiGr70~w8S7qzuX?YUGhY`M7@wxm;%l-J(@1=K#E-^5zt%_pBt4cDGhu!NYiWUH+)E`JN;I=H z_l5XH>a-yCGY@uA)$F>LE~vq@;jffE4b)M7qI6$V*C<$bveoUH24sT&D?B}+CkcM` zGBbkd{{n8Xxuyf}dkG3aJBD1AXP);$Ixwy*Tv01#{&cDV zKj43esPex@HV8nPa$X1(xO?Ta2UK=eJ0>dVXxLY{oXU0WR+U|rQV#3qQ!`P~+ythu z#0O5BU(njm_edV7Rqaut7S1%I&Lv*^xzKVn>qO9=D}Z}rV5m1=qZ;#t9PaPpcJfAwCQ1=rfHox5Qf4YRXnXn6FumSa#9{RmhVZ@ws5x^G2S=vS)>O7dvg_kk(-n3yke=u%$fYJGR&MQ>(KdfnlBiWIhZh^bgtRwy zm3#+5pa8iB^4ye73Fdb%&_vM5)O|v;ytk_w*E=qa&|9mg^g;o)AB$m+<#d1sjDzOq z5zj8XA>A@Qih&o+Ub>lh8DHkYL6jy%fl+ksR)|-j{=`f!7=bV$SyPt%^n^7oK+aGYE4_&D;=vw}GfVdI>#7a^rhoqS3cFxrAg#uWJ<6fyU;_&Ak(2{Mp{n)?pq(dNT zRBP&%3qL#p!&KEhLGFwGQb{6hd3+9iEH5-&51p0!_Ah{k&0FKeT!l2vcvXMg_UVY zV=Hp1sD{zGGHYuilrj@jzsW{^L2+h(zgIiPIHF@!@Co!CY38&}p*?u$!~J@R`wgTR z=}lsWqZYB$;v?A7@0j+m-cr|y+ExeJBwe7k%FcU4Moq37k$&6qQ+AFq^MKgLtf+lQ zFqB(?bO1J6k$WCpcCVD$#%o1EN{6t(eautvff_qObuzKJ?rWP z=gO4|=h%i}i*kIjAE^p9JsVyan`*beP_Eia$t%P|inLDjd#vG+&4p9iwe*i-z2ti{ z9Yl|(euOWzyu4nVl?J#M&Wsv}adbl)3ynWwW8MYE;A!mj@sUL(mNJuGX5tX?KL66( z0I+C+D%ZF=(#;j_rU(C zEki}uZ7yC8m{+dE-s9?1y9T*$kR#C-fQ=^9^YDb6MRti|>k8-DbQf22IgnMs(`EgP z6A8LMFj7J*K3bBPI&vjaivP2-Wz)%Y^RK~*|E~XERB_#VnlkYnK|dX>@0w^@ z2r|B#G?G&$bJq*wbJvyEpsTm2dG!Ax@2#VvY}fW-R1igBKtSnHx{(s3hL8s71}O>Y z2B}ey?hfhhl$P!i>F(}^0fzCrc^>@s-tW8j`~CL**52z~>-X2a2A#Rc-zIcEj2b_)eBZ?x9d^XKP6!t+zdPN3Zao zPbK$#@LGv@6xZ)HF`GLX6fP({N^aXUXP|$P(c`{YUYvhW@Og&#P}ySkG~_@Nh}L@6 zS4@-KHjggG1?mU@Wi3Fr$xP;I*Oa1(*YM`y%U!|Q-JUMZ)w(GyB)-rkS96YrMeF1R zFSPn7lwIT%@byJuDB}%y@?BwOd!N0UkGG=o9$uAmfLT6qG-;PvEP!bc({Y2s-{hP= zSk@NjDUU8gNcBeI3(*X=`z$(X@vuckmOK|r^}xYu$1!G8dV_Rl0hKZ`gPgzU&G)0C zrNH07b3jyFJhohzkeAmT!ilXr!je06 zC@+#V8uh@^fvQ`irx>%C6L(rbTLo0&+8Zd%B5_h(bQn{SlLV%P3?^hH#- zDfiJ#)-<)KquG=OcrH*TZc0Xt{3mnU2?JhaTs`T2;Gz7<2cr`cN%0qw zTeFt)mlG_FX6O5GVP|aEXS>5=CTT-6j+eT5$^8dtRSGI1{VDdOEzU*|Jp&|?KCC?t zxo_Wv7b16;xablH2WuU<3-)}xGw61#-16W!oEev59U<%Jc`t9TH7^(bju%zddFiA= zTQr6S_jpWRyp*T@rQDh%Y3A0;-6@x-upC)Nq$oZ8v`{fyLYaOJ??GQUuZii5^cr`S zGupsT!k21b0J*y{=Y`m^HrIt-^h5QBBk8HMJxv}DUOo5Bd45D#RO-p;-}xu_+)(;j z6rn4{ia4Zc+$1=HzU^7;1;jSL4|GKG)KlJE*4=v9Ur3|nr`SMACeQ_l1CFf$+o<-++rLBmh{v?A`g*lb0x(ws!pyX@f0R&F zLiZ}?mue1$&2^i1b=hxMl`(L3G>zL=#8G`V4GxUeX3L$M?sb$?b{n6HFC`=V1BXdAiU|riO`SqncN0KRRTNo%_zyFZFD1Z}!JAWM(=jHdl0) zf0JtAKGl?ne-hyEpF))pm8BBjS(%qv3=)!Q415@Mv3V35BI=Z7XC6l`s+gAJJr^4$t_7SHQgB$m2`K1b~8sS(fT z3f%DYt;X$y*hhvJbFQ+QO^NfYYMME8DX=Ts7?#O}$ zWu^$;k4m2v1mH8<$F_;@-ms?#0RqC*!Xt=p(BWC9;U+FVRO-1HbAM z`aSj63$d+m?R~luW*T3KKs!jPPx3Cg_tAyDq5Gi*Qp31}XdpE3Gu0>ym%aghqZ-s- zCX-1_i=n9hR4w^&UyWCquGa$RyB8?JN{TCBrany8cxRWbV@@O_!9BMe<(YX7v$#+h zB=mcZE;jq+=dHRGFR+)a#b3}6>FFu`3=5HZ$bLMuU8Qjuv<1m`TG!wz*0g@?@=~!v zS}OjF#-zSC4*8BZ4q8~@^@uFbYf$Z0vYdQn{8ocnba~{<07K*#cTxL*Iw$!d+Y@VE z@C_RCFV*R_DHCiFl1j-(2<;2aWAJEYUGVq>k55bdg}U57jEw&^Ld!7;{LL?|BxKF%C{eBN$y%d=Q*Qc-x)?e3^*&!6SB(&9I0brax|(f;gC zA$f1cKPCD?qgUT#Q3iuVX*_Xw4%w0+obe5cTRwPDb$Y`}c8e_N2?p-Se<7*dp zB<=rKoW70V zWmVE`A^7@V-1`>$#xkN1l&KA)T1|#mDjeBYBG!@*MO&(CfLR;u02WmB8iC0BYhwJZ zbIa9kFFu%Rs&_q+8eip1&D`Qs@mDM1>L~M%ys}x0Vua3QlO6)p7b=GMMRopWcuSX7 zHmKSRPHQuxwGY(HTUN>z@93we>evPIlA)J^d0ks$O8YWH#(<%h;tcmGya(7Bl-M@p zRSmuRY}!fL{cAaa$cJ>sXCxe$WRlmLTg|WjfJPP>%lG-xgDujyzZjhVb$p&%Q*^U% zbD$xXi?;Bw_{MP~{sTbAIu^UHA7;-3T_uyVT*o3s9HG;G+N+G6C@5$mnreZXgdF<} z9G;C?I_NeePU0I30utEPatqL9qa0vFq_&T5(3_9MV@|h-(H~~~{}&y-{v1x#JLF0X zI;2>M3 z3aNuNHJ{aR*T?9*!iH!ASpeX~b~1WqrKZ|_Yd6#v-2t|k?(%d|YTJ)D8p;zZc~p`p)p`tnv&F&x!Q#!@Ypr zF`$5tJ$jzKQ?qq1J5V`*I2)aT^(BkEl8BVOV)*p=BB1UzrfWp9jUlbPAMgbwJILk{ zPxow>MU2Vb2oVV7p1&fOZ)d*Ypa{opE60u1 zbfGi^y4qkt0o_pvgUNf zIIhxmU&}xj2F2I)(EXsXdN}Hs;?0V-1hVLd4FGBi)zgM?0)X;`s6e+01!5UteWAj7 zr3nDKz}G*BUL9>AUk4Oj7BCZsbev23WDTvy z@cC~1vk{UuEj%1U6v7?KMGCZukMTGwHLa7lJr=4jGZGVk-0l<$7RX=xH9LP_f>-85 zd)!q&l~>W{nh9jd5?+W|fIDJ?eRTTme4pqOVL*5=KH;1~*Sob@i$4r3yL@kcZn?e<5*bhT1SK1IKk$rfC^? z3$zZpPdlv=j;?cqt;sTN2_XuX`F=fJeDmXM=lSZ?v3wY7Ig_lqWO z%m6St?9tGYbNy*?_@$)(FuVhmSiGG%g`W=I@a5yN$och@_d^u;<8<#cdUxHQNrWVCZLrgYJmA#!?b$W&9lpurZ_6|cSd&mfB$mO7SV3DMD+U&gB8Ul<5fW?1%M>6u8Z>Y_@-zI}7UM>W~xD74gH8Nd%=T~LG zU;+rij>RUlmvQtr_JjnATyQiHi!ncskzeowNaX)eL3JfYM zKQz31MeJX9Z|4QkDq^>g%p|1R{6bmoOeO*WeaV4F@)Vm_eRNT#PL5bdIbdXz9mgJP zbVzJa5}PhP9oN8sLs^-?10y9^_$ytegqWH$+Im=CZ(iMn`c@iO3_t#EsL6> zMh0y(ewoEj_fY#fRt2tyFjD3tywc}m&FMDrE)I)cS-w($hhJ}7Jd_(oyf~z~UI|g> zNMKI6>5WsaZ9GU1kp!C8xrcs$wpj0EA2YvARNl!qKrU@%ekw*m&NOwD_vSHETE3VC zdo}lK{qJV<<}yu)QZ?;KM%xRzRds6EySL!wG=Q%yyUnZ)Ibtjd{o5t3@=tDES#L6a z>w91PhS$F6SyEZeuSU~?E#2z8il;7Z6;y`w6?My;w=6jnZ=FV=b|Z;p%fczyy@J!4 zpY)usbXpcc@m801=lLHLTW6DQ0uFwiwrR!BuQ z7TE7M&%Z1GUipi55bC0r(neLDQ2{S`n*K1+qh+do(=#h@kt|MBKdr3O!AX+U%a+mcJ@S- z@@0DTrP3$jipj4ytp|BF!cEa-sWi*v7z?byJX755$id_wbKH5YtY)u#6WRb*Z(o}W%?*$Cc(aQ0$-L)CtOr8fcaiUsX&A zqw=CQ$mA>e@{soVogwkh`^7OndG3vd+KTR}$Bx>z8jyZNGZE*Epe{-@E7w{H9nrnv z;XDz(M_r7IOP4(Z*jM@Gla$JV(Svo0W`g`w8M|_)a#UlVNXY)C0Q48fXpRL0SUIP{ zYB86YU){)-N0ApVyrW8YXz+q^wZxv#cKvjmm6K8$_=NKdiC9#P>>zD&+4p#PKuH&B zhEHuDy+6b(us1#yB`$uV7;>C%!zhmX>9;E^2FL=xkW$%zKyok|Yphl*a(v~n$^DNK zGyX&h=0qDH@2%n}z5&Ni@XbPV6zH3)U@%@m?oJ5ns~Z2pADy9AhzpYE(4pp?zfH+&|k!anNMYYe97*dQD3H7SYshN=FqW72NSP1(MAG&_jg8y4m zdXstm?H{%O6|SyIcQ!D7Dty!k-WY>W;psdzFIuiR1HCxNYEX{+?P>n9)o(*#H+(2c zQ`Rs{J%d+TKsscumP`hnTx9!izv9~2hy}T94E_dP z*o|vET7^8nn%@J`K0LT-XF($pM|DzC3vU}Zwv}jK_$DhsXD)%awVrBr^ zRu5i^Rz|wkQ`|Up38U{ZpX|xV^?7;vt^5<1h*R@hw@Gy#IMM1E8wz419WfJ`e|UsU z?1QFZZj~?kWa}{v8X9VfC)vC5_bho5^{)vLs`o%$Ih!u2hJ}AAF1yhr(6XNR{X>K<8R-~e@3KOeSM*R6MCIp z@4Twcqno-H}WvqoxF1_G7)$s)v@`2;LlnB!x&~Y#Lv){?&eh&9HASZmK zyeRHfcLk8g6S+dt?Dd=4-LPwzRj6;a_D#4BFortUM>OoK#w#hpUe@$gi@wcSXj)X# zksVDwDG=mGHG0zZ!t%oA^^>)I$D&T;#qG|6lcE#{tw;a|=)IRmLKUg(Y+xorD*X%xnK?w$y-ky6|0wPKt&~ysLOp8JbVHsRXHEuW zd>mrpe%M!SZ%C-zz^5c7jm>g}FQg@7#ZM;Jp~-JgL8Xf&63r3NJ14ZekU>rT(UbjX zX`4!t6(Uqy9>>LEXOx)h$jlYI&zlvkP?)uqo7&8`XT2v%taTrkpQ0JkG2Cn2!F6qg!o`Tcei)b}k`N zovHopj!>`f(nBJXeO{K!(9Mv1M*oFG`%Mb%eqGK!fj~3SL}{)~UH~X_kbX3)>cGVk zUXbB8@sZ=o=Z={J05{b#bqsYZiP8!ao28y zIfie9Ip{Lo5-hWiI~u8Mpk@r%V1R23< z#gG8CKE(}p0~TekCbXh>RUP|?vMA~1JrOsy>LY&K8!Uzsy#&JxLcF83XTuOLbLL$k zt5LRyp~mxJZ7GEGXy*+d_r~gsn)0}lK=k`s>Ecr;e^lk=4CaN zoNCJBJUbUSr^PWD{M5uM;?HkV56|ogFaFkj2+p_%cAO#Gv?rjrGmqayW-<*T$BZfx&YRbl(vxhsR@28BogP zQ;~H9rXDOReEdU^Lsny~DDY%^gi4aRA;?}ILrMXub2BME5B2B7R=CLZax~SQW$QdH zZ-U^L+aEqs(6$l0?kz{P$sV?9`Br#e;DRJn1lbx->4bMgM&Mlv-KA*4z0pN> z!n_h56~l?`4K-iU;aRoxD{QL0>grk+R-tFndEeeelz+bAmygYfR3(k@&7t8j5Xl!e z?TRmPH6w)_6lL7hV2TS>SkS6^PvbA{m0g%X8)kdg5$TW}lWnzBRqEC#& zC4M1Y=|SgB)fn$=vzUxFBJTkd8^{wx{Lsi7imVo+Hs%i&OjRp6w3MG4!eq&+I^Tg; zjmIvXGp*6{uVfWhGEAdc=TXtJ=bErR=*eZ{J*ga)(PE>q+*(#Fin|n`skk ze=f5p%q|w1Z}VumK0~iMKFV~AEk1p}?$AKGmqY%2@K)dKa zA}IPc{C9k@t*l|tK>88s&MDd zMzo-+mM%kltyb*=cIuDm9lV>kCp{~(nje2-oEoselT)tD4M_e+)*kHcS1RpWI)MRu zIqBIZJyv{tc9NDMk1Vm~*iuKJ1fh;^_m9*P>mrc2KDkEPC0nx}Q>}otf^UB7vTYT7 z;q`LhstuSclwsO^%}wXSbiH*9A&@;fi3e@z!)AFk%2#D-iKH9w-sOeA3@Nt zRsd>)2IQ7y-m?PID!9G2zdB5BBc2=rbO%Q@=sqh0bckLkoKd*cJ(;U12}loWr=kAq z?YI>_s1;v+2S@2la2iQO@i-H7AosX>XU~ z*!tlZoDuaMOO#9jiRWV7anT)VR|oGzUbwOc%1|nn`>X*e2o@QJ?y*Z`BJi=$$CbOy zoY1**iH?z*ZC+;8;?bi__iiJg4<3!@;X)Lczrf*%sd$43`=B#$hJPD9MVlYm1R@Gf3%>29r0J8jB$PFi9})(EndfW)ch!L+N{CL9#-#kRf; zi6@XDFIPo3lMNU}0q}JGXpbqp+qlSoE#nH%go2kpnrkHyC4SMF;L^NG`3~;OnBoH| z3&28rNHu4j=g3m-9AAvf*3p;2i)5pK(Bx^w)d}dlC!+{uo9uVU0(gLb2sHi+MbY8# zMg%tDSbWcBKB{7g@RP26%hf1n3-W1rHkoHw4lokyOxZ2HQqt}dgz-D-$uLY&=ECq< zq4wFyfehS!6qvI3!}Q`wV+L>J{uL`gV(6;{^fj}A5x2cB>rH@_p6vl$E-FT3+0J)G zUBB2VhL*E8!h4Iaxu9r(Y~iD^;wxt-Dg@USK#(xvh5Km_k@TGZvp3Z^>W#Yif3pR?bSvR5=O2Mvvn^$@g&iOJFsqaiFjjB%=gDcTZ4W0yX3#XBRY+kDY1q z6{$NLT;{&fjAQBK?!X@PDM8?NHw@ByMO(oLaRh-UpMi}yWj9IS%aOlbBn=MpLF;cj z-vXG@URtLNUhr1}SB_`iEz=Da9;k^SZ6kirkQXz91Yi|Su)V!wxl3dJ%Fao;Jm?~i z8F7rOFasJ{{oB;&AAb7JNa$M<0`c{alSYKeHML$+s`Zv7)6*^94{Uu~LX6LR%9~|$ zX1`B)a?coduO{*nMV1+ISDq&3!7!w4Eh`eXkH@MPhv_gSDa_|*N>`D^?F}fdFdag{vm08n;W}}!i0`I zF`HnQx1!dZP*SX!T1<@i4k>E2ynfKoR2jHMVMOJM)%l|9^;oU#ee3{b=!>@gChe%U zT`3Yl7jo9vEXdJ*XB9@-d7V$V6A`HJ4fi<^@p1OdnLU&jbxLN__feC|9%~5WiKh$u z?T#`m;nY_EyZfT*l>PH%R1;05ZlXZi5nUYSuBHrzqF-Fspwx&rae5uzin;xYZX$E4 zu0E`R?4&5rK{V0bqTj)kk5Y*SV58f(^~4?H7D4VrMW6l!2l(d!029Fdp?wL$;NO6^ zx%B!PB7vHk-^d&g-C1umaUJL>*C_S8GEF|zfaT@pAB4gsn2Y(ZO%O^6hVj{17g?%l zoixvE;RVThJn~7*wrhfHQHt-x>ZhNoo_UvT4;$w|K1{>L1T5z^s6OtVzCv; zzxc1(7Sed(&|{{EJzo&O!xRsW0%rt3_w(98P+ z5Fq^5&U1DQh%-IPX<4zM5!97+nn>YHFY>2_jH?$MNQygZ z2Wts`-l&`?+-YmF1`n!;AxT*!jX0eZ^(5PKDboVNL9Yu{W4X|k_MsztK%#mO%ujiH3$vH*gN zd6iE~?Hs)5#u_xQ&f_0`6OTBLkuy>>p#5I)%y`J*fcJhz;^>@;OCKM@XeG4>v(KlfCz(#@l~HZSYcT< zh{DM?E~+v(-!%-(TJ+3)^aRv(f@wwexyADxvncMr&d%5Nfm1z0_pdv=pPv`1IcPwy zXE0pK&L%s*nBn6w6Dc{rmK+6e4@^ymqwWlCpoz)Z)U+_oGDboLBUrfTH(@tj z0c1@Z_ac&U*FVCVT-=d_SM;XYst6EgtQm%u3WdYt18*lW^uk2 zx>ek+`*ojhT+#oH0x|i|4r7BT;4HNzWsg4l{7f#i<`RT3xA62{d3WUCHX=hf2C$2fB@q8nf=87#BZxS6H)Gr>v=kz$-0-`rqG+0 z;c(xXpzYoo^%l}Dxs)AmqD1Yins_|J9=6S=Z;0hV&Aq4Ei~=|*3Gj?g%h__L$oE1kbqrQNj%%IcHtbVZf)8H0 zWh5u6#5~MX{cThKOfLTZr=0Qhkjrm;QP73tXz0t6N7+P!BRE*}I*}<4ddO1`^spj`yBygNd&M%Jii#b~fh<-7dM-K`ksEZ&5}+PEEbh!Z)E2n#M9VYp^)`}viziz4`sL0m7wDj1Byw+l8X{#DAPh|>4#hW zh2tvJhVI7~9!NVFTZ?|j&$+Au!zUUtC+lv{pYUKz?jK?$e-+IG*9raiYf;Ol50Zqh zxqmTA4}w z$o=Zml%PlxX?2~8L*04wmA7IAE|?vkt$z8N#?2Egrh-$ikLYY>XV6onz@+&@u(l|v z1Gw(L2qyN%$;;t_`ezI?)?%uf7T>w(og#byZgXo5Q&XB`RJiCp)y#2H1E>^5@{l=u zFlQmr>!;)5H_30E@_U^PGm(@K4Cji6Al9*GkG6LrSC(zo3QpBIUr3Lwk=BK zt%!89nez8L2#3zE6rDl9kTJBZi$E6!5!R=ecP(t?eDc4Sirml4&SZU)4u6}iVHLux ze~gaRrpF7Bh;u~cLWsvzR^SN^I5haaLrO|1q&>ntj$UEaGWn}b`pZ3ec2u$;qY@dv{!~*=!5#wQyTHMc5F;Bn&PN z?m(3gogGSW!k-BU7uNZMmI*9m>|`yHKrYkm{1na@;o496GV+!RU-a}|4lSN;WC+QD z^|~yS6|3j*4XR)1XxW>6&G^9Qlk)S*p~`D_s#dyHH7Gy^CEFK*>4e9A?6$ox$Q84h zoL@cvrnUxvvK_N`_kMv(b_1!)SccVHD< zH{@dE1FmkOuu>IKg&H+AGo{D?4|degDSBn-;*T(`QSX8X<^vA&;4(CBCMN}UA^Ge% zT*;A}p^7mtG;q@-MPdU@mubWE<H=^W&=2{(-;PPOwX}t?p(_-c|-#A4BVoXkfw|_!!wYRiZoO{>kOp{BF4(IUrvyI2%6_KF%B8FHBw3G-*iQk-YL1xh;{S{m zmG@~HQ*aR6uw;{kwDmL{k9i*4|E9B|rbY8td-^uPxIMV`;t{_!>Z^9pNJq(Pqt(gc z=Z=mR4&vWm`e+vrI^x=DtM1SH7el0~bb7siY>V^t zO;+o+fo19o2S{sBK@(f;DA%mmrYy8*@X-X2*aBJ`4YUo`3Ep&tqsH1^K1&SoP!OWF({IU=-9oTW2A5B_ zHGD_#p7HkPFhc%Akr;T-SDjT)DfF!6WQyr(_?zoB?X!9FA(4}5K^-1HhWsVZ>#h0@ zkQn3*&({Q-D%r9m!lW``SI#U@pn{N^u*%!TrKE?UJT@Y&wPm3nnJEPHM4pdR?=g&< zRIn}3$cv+)-hI&IDe>-I_)Ww@!TLkAM@y=^I$NfE4~#+QvCK9sH#VfmxUuG~L_^AX zhUpR{FoG!zn+kvWds9fS)d^+wov@H^ z6|Zfvt=$eh9u1DXe0mYD;bnQ4tA<4AU%(ADG=D~f(Ry@@J9^za*KwXTF868cIcHJ> z8h9?>M{ZH*BH(dfen7spUXR+#COjTFz5Sj4VC4KTj^jn3#T|dW!0xx= z5Asu8jHTfy2m}GM<(!^_9Pa44ja{~7%a^d>z6<~{khw;WK$t96t;!9)0Vg_U&(PU| zU^0wynLUVrW1a@Vp3(H?FIKBQdoLZCFUHwM-fU*uN2FAIfk_es#9g4P% zOI(q@O(+!2TunZY1VxzwddEQq6o2=1{`Wur`}MJvyXFtR4oJ?gKN@k+Iei^VQ?)pW z7P4}1X7_W+$)C+&F!)Qe7{y}-D?_2I7=o1oQ#Gq!NEidGm#(ImS6V+B>5K!&!PwdI zjcp>F7kPj*qDGmGbdkf2p}4u7f#buiQ#|L*?PL5NRZWBi*6@@n%QdHREkR_x$n!G- zR9qG|hUH6&MpGNI{-phPWFA&<)128@14ysUx9Zd-rx;Ll8;F5S7EmtC)&Csd^+&ZD z*u~TP2yCXUmwNaL5ND+h6DCGtw$~Alf|7Q06DUZY=Vb3>*f4ObeyCLjC@KMA^)IB8 znU61AOz+m|_1JOtABmu$RD2A1VpQOfx)MLt=sDG*4tT#JMn6&#1e9qN|d+i_PnT%ObITY(Fw!KErcQy$3r2>-le_Ee&8~2x`-< z@$3(q^M&>cr(ik-5M`Fp`C%a6a~ws@tx>B-^viWYWdRYC-70hq@D@9|J(5G%$vEqk z$V}skjJw5*dvmKVc-s_6B)Js^{hb~*T0AnvyDjPE#X#}qTd6QkyN8&Ly@u7rmbV-f zvcpQo31ycQ(~iWw`qr9CIZPb%--kxk76kK~vXsJy3?NpMj+Fw8#@gu}KKCoAvZPu^FyW^0IR0UPo1C0h^`K*e+- zxa4)dd)heM_%11VgR3#3Qf?`9H?!lQnAI*4wDRyl9xki}q* zbFimo=v>f@qXT15U2L`qZnOA;YezceRN{_?02RwuXZB9?<=)~GX&KNg@q5=>bnE(EHnr`4O<8ZqkXKrPoL1 zek5wVcjQ%9aW)`SyBu}v9dN*HsF2`TXYf{sqZ+dOkb$Q8b3HSZHz(l=!@%#YYiE0n zo9<1?Z9&k8_v(w+ZRs4UpULqF@F!cbA7if&Yen7!K>+83*e2_#C_L@|)soEqttBZ5 z+{MWK%Oug5VHMfsK&`Lz%S|A^l=vA5Kt8UR>>%~&TfN;|JxTo|Ku_|&Lr50)094C4 zJIaPhHLvTOq>(WNg6mzKe4%P1+*L`}=em98$_P?X37)Gtlr~oYi+Pi;#beX2Zg^SOpX@>%hGPvEU;K%(&yJ-ub$jg zb)EtF4;O|+4bwUJIHad0d1@VAA$lbNnU7I9Z#LIQ0Zyj$-kl}+%W`A^ zu#Y}*sn&8Gu$9`GVxZ7Sx=xSXBQp!`VecuvN}ny(op1&%xU>86W?5(-lM}U!xDVbt z^8fAd!8_{w<6?Hb1#1r9B|PDKcXR2%HkO`fyzYZ0@LKy;>xtZRIpT?{7Xn8NFOuB_yrBhLx)k+yp7)=1J;ssAjj zc0DYJ-sNUD;wvw&)9F#vBL48wPke)vPya5u*>!WjZXxjW{!iK=ZC2nW-~MI^pommcgfNhG3$}>`se2`;Vlb@eFH40BnRQ z*f_@z^Hi>DHSwamg@pm{EDMq}+}vM+gcwQ(mKYJy-S5z=K6jh4_U`=#o0AaHKZJ;q zO3l`Tu!rtkxaQ|1M$aKEsRcQ|Ris?mhuAC+&{#^^(s)h`CV1*#`|sm>M@W(us#BY3_w}{_KHls7QkMS1EPlIrF=w&?fih!HP&u1 zw4@h&{Ic=H6Hfc&XdXa03smO_s-v_eOMIWol%(V4k{82*mYz2HpsV$bEQE*h8v8Go znK~$C@RbiQfP<(Zt7&iZ0FBCu7U01uTzov$pM7yavd$+qO z+XK8a`*Xrpy%yTi<2UcZ9|D2Fg$XBh$G8>Vo~kEd@Y)Qe{f|>&E%s0~O2vfe(Wkj` zvTv5&VAIl0ZodKLs#zp?yy@6bB<-7iFE?#pga-<)Ld>C9#6RHjA&4#jGoqt<5744< z$_WzOyq9#Q1t5JkY}*vQUR;7+I25k*MnkP+irdC1>Hua*bblwT8_+0`R%Zg4;Qxj{ zV$YX=o@Bdj;%rJqcX~2RU%qHLk{(BZlU$7ShaHrb2Jgrkg`h!=tw} zFa6bJ8CKf~u62cBtY#m?h?NMx7fMnU3@3v#M(ddt4u`jblM&!aY8BDdk!4ZL_6(BI z-29l;X#`rLK1BO|rQgs*k(K4@&)RoMZZuqDj43!mehf5ys z$i8%TMR}P(Ns10E4BZ2srmF`z;R2^FA3n^?^KJk_Fnd zYU13r+n@UXI{XJj-UsIod{62TJ?>x19OT+*8Z<~cmtK3~q1>&`Yr}mnmH}SQ&cYJ3 z-^V{YGU|XPM3L^+Sf9qgrE{!iQS_TNUQ z5jf?W(97=O$EjQ6ObQlxz4oYg+h=h!2bGzl{nZE?x@fDO1L}b(R*w=(b6+&K-dk~S zL9oZwhpcP$?x_|mEQ|q-!XBTF-$K@uzs>}0rzqVz_kUW-RXKnDAO~_W?gpH%J{SV* zRBG700r!!M354V^6zE@^GthXqEOwXE;{QjXpee%Bp6yE_<0G`XO3#j8ECpO z46|~EbtN@k-jg2DBj4pHTJZr;C=PV^tS!nIa_X2p`*Ab+(I_Hn ziFv8|K13=?(ViJ`^va9)Z7#VP8W(#U5w6YyvItfSRC$~PfgEb!Z?lxCt` zRA4Mxn`~5P&W&JOZr!sMtM5>>+@{hV<8A7Ey>K*NN@1r}e_52^Y8zuzy5a=txjJ7UN8j(|cp--K9yu$cjHjYrejhdmhX_(^gvciBX(y z&KR8(MXmEu9-Z=#IG+*r+wXsl;>mgjrx`{RfwPifj7k9U#`cJPfbZnObvtD?2-*gC zuu7`yvx@Kql5)x~2tRvPtc^xBmSmNKPwLrp`gj%$1i7V#>AArv6xTszoeTEMyRT=d zsjZ%0QOB^9;|JA6Z%r)Wgq$?_odC+427%q4M)VavhVZ(Vxvhul#&k3W?}mhUOzGe9 zJq`p>MTVx51zC%)MYi;sV#pu&4X|ke6XZbsfbKoUo z`r`bpm^q7IIpV?4jsjh5Go8SYe2H9g+ZC+W(1Q z{xQWSFM4`@TdX79ljQU{%ca*?YE&PJ+6*QCALiaVs>*d;AD)DCNOve*N=qXk4bmyy zA*HmyRJyynYtr2*A|*(JG>CLJlj-kuE!N&^?Y+-;&OT%8Grr*;&v@t1>3pB(zVGMG z>zd8dC2sN-vzZ6O@cO2-ZBB@kTj*3T5`8HRncZ zyV|Dn8Hq_cP2{S|;1PkKMwJWA&q6qw4EIs1X1(Z9ex>}m$4^i;zvbkGUJ<8;nOtMe z#|unwk_l^QY)8^#w0cR(QT~wqY!(4iUHJZ^{S(p3Pc@e{tXsN^Pev?}rSbvZz>wZ4 zpb}p|P+&;Iyp;f4X@BHE?^B`wwEVBK_y1p@e>z6NBb?&gOWi)F_ODHK=^0nVFr1I* zgv19!@v+|%l9PSNmz0vz%wvc!|6ZB^sEydD+GeCs(_@FalWZzw+M~jwD{rgIr?|~K zxtCpwn7?AEj_^+3<-0Vz0;(Vfq2@7XLb>2o7%l+aAe?67tx5SBz&V{8*n`lGw(&DdMlG3HYCP-*(+3>wDpC2?Y6+`ml_A92%6D8Oh z>=y;e#8rOe!-=$7F|TM=qJ<%+I`j8>YU)G z*>$JV)S|7*-PFg^D!uQJMA^5LFpECFB_MPtd_1ri&LN?u9ZV>ie>xMpG`_ zJkdOjk}66x65MunPa^ZF@}hNPm1P7aU40hM7I~AD&6(kzpZU^KnKLxYY5lUw|7mCc z*BSGR1PEKZu#6QMRxQhrTw&u>6fY2BYq9fhEi#WY1Lwu3CTalV*}!ok7ly zfTZ-dk=C+9p3?Hgu6^;Q96jtLc)mzrN*-ZMq~&a8vQg&fWy4uyyAwG&Ws%NGw6*}x z#Sr3W)H!ZW_}E9ctSDC}R@Rdp(fNBkpP{2{?-;x_M(L%0>{ag4{>oliZ*H7KBrWmN`I%dha5)Rp>m=J+7og!CR%;%h# zN#C-3y@yi%II|~O73Mg^q(h^cf7+2>`}1$2l55@>zIQ8aaZsiVF7`AJR}bMrR>;>Q z%pnQ9$Tl+T*__7;euVk5V*+lYK7PS%#9B}-s0h1vy!qX{kIb90&uuX);7_8!gxe1h zpyFbH{D2_xm!{oF%BzgiDQ@PH( zwC0lrz&E3W;|dctZCAqV&=*NarARomu_J&stn{dx=ejGJyEfS+-@jEXQA}oXKw&J#|KBu9+MWBgw)m*O<3; zBH5dAp`{Q{toejF_^G&2&|#ordO@dGUS{h^px)k^YM%b*)uJysr(}w|JS|_&vYHcficlZW_0g(g1(>0}?EQxP`1R6y zhnvi7y0ANpGGStAs74~ey!(6bs~?62AAFH()CCgM-MZ?{e!JSgiXJKLl2e#M`8W|r z9=BWQgcd#3V6MO5rQ(u)Sv0Fi5>O6ntc|18{^368S#{x)FtGHM^E7Gm5DWcL_e_~A zP$ilaW}dzREC1msc)`FFhBC8Bg@>=SJC)f>B`CFb)Z`<_21~4_t7pO7h4jDX9dg=s zwLx*z6j6_brk9(YA->h;>sL23s)AgjfeuM@1yu7KD9Y}5-v)TlN-h?jO{pKi!LIk zrlXF1*#(MKimoie&c#-o6p0mP+Q?EmE1=AS>mAdQr=v?ilr0OKpWnTv28jj}z;m@g{1 z6FOUT&If$oovGI8$)OT7xesW_#z-BZQ~?0b4_UnELrOo%6k02*w+--B6Ba@om~G9W z841yk1Uob#)@FzGexGESnhOQVy4^$8cq83OH!iLZ^3uv(@^Ht2nbnv+hTDxj;m6pd zH3t-vVt-f3Dd4D|ca~t^G=|>-6Wp86euCn;oVb>aftg{^LGW5(=Wm>{n;8?y>k1_Z z3~tuw-h!WDpf^wIDbaS@kCU(qp17O8I5*4k|5427Ep#(GL|n+m?uRm5aB}w!&m2|n zGt**}QHf?%Enc?rV(U@Q)R%qJO0*ESWfquq5-Y7-%nay^JC->7>x7K<%?ML zpoq9$4?Ds37FIYj6y$18Y8d_Asd3x)0wlx@uIl7nLKN2Pg(>?^PXd?VIn z5vFgDYq%R~KQLaVN)^mF{p&=whOU4X-SdRIcIm~%NBWN2L~$PX>b)@rn?f2{D) z`xn4$M$QZ4)EZQ#nvZyN$r6lADk0iCLX&OE8BYSGsi* zmjW8{ca8`?o3Cgh6Dv>PqbZZSKfF=_sMnpg>W$Btiq3c&VZL;7OZuUo7-`i~lV^aJ zs$Kq0;N!X<>wF^Q=$kVXL%LE^#@qcz=VeiQy)^t4CX~uY9P6U%KS7|=ETZfB?3u^9 zJHM)j|6OQz|5w2OVN`B=W1!4ha@KjLv5jSSZko(=F}2x}wb7(jMTC#f)>0}n>Rme( z9WGqAuJk%CmZN~Kw;ygp)G*-fk9z2CXOn#&1JWm;2pwKoM@73P+Fzd8#9zky$IfZ* z2Ca{5z0Jpd$sjt?I%=2sPyzfT3PlKD@E`fem#Ar1J+@`2KIby9+)wNCZznpuqTb zo!=E0Jn@VVyq*gbaRsjre<%RMdwX}b%X`wPXSJ{l!xgR0@A*`{x3HAvUI0lAAAG$$ z3Kp=uJ+#~f{u2|SnS!jdou451_J96e@2)3Am*0u%Uf$wtwzK{O6=_}Jzq!MQvA`0A zJ)pIyY4F72yQVdL6C|t`GoNc@j=EUFAJ4{XjHlLx0<#e*NOTIj91A5;9*W> zA0`UfQo0q@n%|=Ge{-t7T&r_c8GKrC1AsleBL~BxRgRD|?@CTZnMVvhfm`*!p;4g_ zC3|U}tUr8C;KtFCzT$(?&j#gusQ{;S6if+aaE#RBR6Nd4guPPllBQ%p+dX&>Kgl?i z2Ny8Uqe?&I(8*lReEV{#qVrSravkOf+2?+*&v&h+y>~aR@su4*LRe&|G~hPDhuM8c zR~)aI-FK{Q@??!u8L-J?S^}k|RV7%M5Ffsl%8Q-l(rZ~teveiP-B&jEASBWkGlf!VPQuKGz}6(aPL%|o56V5T4A`#BOeu$*fOka3c5Yciy9i_i+GVb ziUwHZc)5MpHd@#O-bFNV9TTw!5mV>XEgirb=VKUP1Pll#E#Rf-Mxlz<%K-G~D?6fn zV@_+PHj&u2Xe{?u`hNL0gA$EIANZ#B6n zaqmiBRmyrvL1_eipjEm6fLv+j>_W>~6D>xHQ@k;ZF+)e_>-#%i4yNMfP6DvG+C{!- z#gwT9gfbhd72$B2s;pwj;wT6FrL!EtM%RXeb7^m#K(NbmiTtFtywX6MVf=#EKzLs!B(U|B>_OTZ(H(^ei|pO3v$UAxLC^sH z5=SqL{J~eqQD^b)kOf{W+Ia`;Qo=uM`z};j*nR~T;LT!6Ayt&hm0E4?#q~suCwah- zne64jI_Bdg9U-hKdEechce^<@b6R-sPw$vD7Zw*ZPnHVe?nXoyu*JpZgqd%p(~o#g zT|ZW{N)Ugv;qcP+V74jE_b5!&dP*BBNa*Hfn`mJo%b@DA!K$!kTT%S{HDdmAcn}%z zr&w=_|Cd74Aw%gar-YxV?qt3`i%YS20dT_w^7uib;VSZ?%*_~|zByIhkAu;?^r@bf zfh7=*l1nFlJGjy$zUYi4nzO${Ap8Ot&Con15iwPvNtQPivIL`lIBblRjqSu|jWbH( zEH^BZecT7b%#Ooyw3=Di<&uw?ll@*fRs-bF$l^neue(}y=AOxnS4xaoxwWSC!1ndR z5}j7^Gs2V}`cNOSdqRGU?y`(AlsuL2nAfYG_QDkllx3kq;+Kn{C(~<5V4u_-cm%)6 z#QV8}#TM`3!u*rToA_POXAMJpD2$Aiix=eoFbWLpO!-Npo z%aqZrjkZR%15vBZC4{{53QvchAS)UHq`}XbJ6^W4dXXSIlys*)>Gx!=5-VS-AKyFe zkRQh{&Q;{M(Z9ZYv*RKx<40Cr_?ilV2<{tBg=Qd8IPMec32!l~)sQ9?Z>MXE(55{~ z%ajuNx117L7Hpg^@+p9(XB@s1pf-Yd9b?GTA)YYvhkyNlQXBzZqj=(m_uOgnC+NkU zM~~p_oyv&(PJ&;D39G@2a4qwOoswM5u{+b%{J6W4&IZ zcEJgNY?CkvO2YXF(1OWiDJGU z>eD8J83XLG%!bzs;<6aIderUWwsHeNMoJ{(PXcX6n?vIm&Ad1vBz=$tNkvIXB|6C$ zUtGT7bFc`exr02M^UJzEj!Peq)r&bzA?BN?=YlBBR#;w-_L3bT1tG^Km3OpTJub-{ zdO{VC;VUz?rKzXCE@=BA^YJhjOx06m>eZ_9WTZh;h7A9r$1^Qm z`l_F5>qFh%gK0xCSWL_A1nP}^CMOnJvRaQxp$)4*YN97iqm8_T5Dw(|_G+WVg*Inw zzTS6;eUYq86kDc7n1|2FvUwEpyoYC@LUA!m$@64oVKC{K=rI;l#1nGma~`V@mFBmm zp@b|aFV9oghK|>>8w$S#w z^{+NiwI%dqCp~!6ClvH!%hp(>%QAQES2yHrd-)YLN&bwgTP}w9s1^mmjSc$_EkJ`? zAXU27^W3QK1KZrupH5x*qp+E4^sB*rR=kxnYd*CfUkVlRevQ-oY-!YA7$+qEH@N%% zyV#dVfG**9Z#i?Nn{xzHiq4R;Modv2eT2J)0{~CWd$1VX@gA>No_ogtpJIecv6W8# z5lsj^h{A03l@5%G%2(60kHZG5@@A;unp^-$cKc1Yk!7)weMAT+?&Gj(c6j|(*(&%5 zns0M>p?6skMh9oEf&R4BNhu8Qhj1_kS=g=7s-V=kb9{WVH?2B84(-F|VdPGqa#V1n zrZhCF&_GoP&-v)fcvVBg(`D~ju}dK2{p?<~zCbpiPlJGvDyWxDQSrqAE~9Y>`T%R> zG+Js|)&%JY2iWDh5v>UWIcZnlVWE71tkxt^gJP}iCDmaR z>XuBGXC{w6Iem%E{ay4wSUUfMECs4*bY{@*N!IWF8{|cBrF~9=^LiqYI7@P&DVoHl zRD`Ywk#uxsaCNIP74|q*^BImk%CWhg@Gs)_2+j%4roLjdZ`Wq)Nav6kj7TR`wdhi@ zgM_wK;EIclKx9S9Z4pKDrUPFC{TNZm5w(a;z4|=bCz4p^*FxJq@L`LAJd~IF)G2;B zaR|bSE|n-}>)VI?E;D3s*@Lr+cAlSPC|`g53MyK#CZ4FPG&H%&q z25tcXiI8nwcwL$^(%2&=5ZoB(y>R@<;CSd?Gh(9PK;+`l3%&D^30Se_B&ip(f*5<7 zdLmjDO>k6v#zseg(GvZQDfzRicTFzLtL$Ep1cZl$YS3L)k7znEfe9F=khs7w{>{vI zXPo{UwEIIY@jG5T>xPXj`5mXv_^Dkt{dieG5*q*EkW@RqKScT+UgLM2LYlr@1Eg9_ z!OajdwW#%jGT*&XewbQH0h$PMkgL}hM$zqT55}3%i5LPG-e2GQe-R4*GK~#SSLm+t zLP>xOp18ixN2Rz&Cnr8M$~QC=;5d02s+xXX)pulFSXIT*oB%M;dBJbNT~JMxL*3ob z6n{qXt_KZF9pUaL=T|@78$8=e9Leyo= zorSBcGg>);PTUguk3Rm|Wpd+`YG7Dh=c&@r(S;_;MMm=rCY_ApXeSSv4?uGWRvy$F zxv%eqT}1bt(fNIP5LlGOrk0iNuy!vnnvh>{M&hNG;KFoGoFA$u58Q1TNIu2$QuaGu zb0d-O!G}A=e`lPk_uXdZ_1ZUp+#=*OqN83XW#R}A%Kg^GEMZbdq!Hdj)`HmenL)|h z<<~B_!=WN*AW8&(n?Y?YloR?w7N3dQKtv+(&QIbZ)bQR`{6O1^{~nCOc?!uoXE*^( z5!%d=O|;eK$l~K=c`YdHGtZ-Cb6t(Y7Ii9$Yc+b7b>s+r)*`BzocUma6hpNfE$bOs zY7stSgxCp7L1H>pE=S+U_p-)nni;AH1cyd|OKBL9>73S?8u8sY+~rjKDxVa=`_-zI zD+*S1b>~4`M(PODNFIs=9FPXWEHYMC_=6WLy6FH}a$H@y?0FHEecFAzntceN(AvI~ zXn7lUZd&S|&L;x*VWM5pT>aBvaC^e@sSVefTt?PC@nYH!pY$BLV+)KPkZR8frU)bw z%c@Qr!A+bXSPOza&U|q@ra9kEzXRX+m+WZL$BvnovC2v&RW%y@8l*xFPqfr#z7AC^a}r??K1W$33xkyJeU{5WT_0)E1!lU!t#V;~ zZ6g8S+UU?_Whtcd%IuIoeCkLxh`7@@yPXCX4I@|T3q`YPr;7{Bi+AVe0C^A%>8Fuf z%X_g%XH3+W+HdR|nur+Dl@5dn`hk%{S~s_fUvy{b*biv$N-0fe&Ft{4np!^8|DLRx z97sNy*84(CC#5?w>&?p)q>&Tpsr#8G6IJgsk`PS<)m#izI*WhRlm&AC55GV$_!s4e=LMiV?EAK8}g6;(Iqj z#y=nUrW$TCivH`VEQDe!s37a5K^psIZwpRrRd`t~qmEflUIartR*y|A#`yel$B!U3 z_CS#?HLXjrzv|I$uHxyHse2hKSE{c}ivwB+I z%~kA@2TpXlOH_(c@5_k>2*0=+JVVBxqI6`_p}V&+uV#R0V294V<=G?{7I7y`W$cZ{ z02oLyru%3N!)8Rkmx|7-z@ROi9l+q^uQ&-pa~uyatXa5N!pCNxD4GcNd zuvWlBCd*J$*j4HEr#HkEjekmL0#-w#35Ek0ODUc$s&d1@xA7C|XWzJo@U<7255Ng= zFQV{^!u77BBj_}UV%b>rP3IpCRVwCY-xWEmD@)xSc)DdSs~I7GL(JZrNCHy zT~~S_XuW3Y$LgzQr8gABO34)wd>OP&5jE3~TN5tH*)YBmfSMzSypum*Ro&zkbydTL zJzXSmj&tTXN96B4`TRf+i;-i}=4&Trl)<>?^W8pP*Srh}=a(lmx&2!YZx0n)c7>ek zD}}jmxA)1`o3LbX=k@)W#ZC!O4}E|du}}kB4Ejqpi2oVhSb5OcQTY|8tK;CE*kF^7 z`+&w*6Ze-aN(35o>XX#1L!za*21L)obPuXPe zL*7yza|-%L@^qM9vO;)FvNXUyWI-n|xAjFiz~*TvOTbSdCN1-+^T7lMN$WZBJMDE( zw7jNznA|EXg{&e%;`-LrR7qx6VfbYu2OgM036)|<$9X`(!&r?{e;&y$u}F@RS!Sh* z=unyYf|P-$^C(T}dA&;(57M&?rZ~f~{xIY3Jo{zIwjG(2mlHmO=?QUt24Am~?}RHh zLmsLQ>@up~CL*`^@8tj`qhl!V$#4igm#?YLd$x+EKta_H9a+)WrkAgxiWrRr#Y8NR zLg~TvLW_s>K<`$u(hZx2rT4?L^6lOUbM!H18roG7ZMht}#K2JQ;@?&B`&2U-e7p>$gwYwksFOa@uYC$@EOn62czho4wM{8k-!hvZ+1&FW z(5$LBuSEF;Pkih{ujX6^2@_)6ND`9xJa7MnBDn@Hfh~>LA4(g~P@5asbLt$G*AlR( zVYw(s9nKvs!UX4nV?hSCo-C8Da!uhTwP+u62tzvp7RQ+bE``BZ8JDpdGM0vV{ zB{myhn#Z^+zC17ODR+GaB$&MykkVuF`V0EKosi3agWyN4v<-TCS}jSnuoY zpP&eBE!ex}%8dwT$l?`HrH8M*f5 zfOmJvwb)k^B7HwWr*Z%e@jraq;qTw3)%hR$GJxZ;7lyaJEs>p>!gcwIv%!9(-aS#h z%Sk~qk+d|OCp%VG*DVFtm;#<(e8{vu{#!M+b|4u@*I2Z2Q2JD~mDVmV_i*0V6H}dknm>1UVxmP%N@& zCvms@+j%v`UDN0@U;5w!qdXR@{V?v)YA|%X)$mGTCJ)r>}@bK|!u63FgBpn|kAL&3a0tI80aIj?YY_%la@O z)$o+~{kTmqN-kN&G?yi;=Not}7rZ>UGkJrv%`hQdD1el`MK9CyraMMlUYL#|REpWN zNJ(d_f1-go%vYLzcp^-UepUS#C=Vx0a9WMggnj9{-9t28z-}W-ZYv6M^+O1`_Tfpp zOFt;Cp5-=@?CRhg-Ow&ODUPSAa5Dxtg4PF8+|>sFZjyDrRZgo^{4mxduZ~f5C4ON zFB)gy9?v^KcQY$>m8Mzkv7aGrCy;!6(z7!gq3!7)#682O{-=7ZE^z6GlQd!K-uj-CwDp zQFsmfOxw@!aybA$$@W#lP2-2eJ7`0r9h}Dm07nOlxx}6ZVCWwWg1J0f>oG_au#abG z7NRSy${F~HXC!y;>w|w?wq33)Ji~w-fT6^q=uSJpKFkryOP=h?*uPH-6-pIIa$2#l zf_{ZTA{85+y6w~HnP4aPG+lO(D4pmHY=ZoECbNz;)|9Gug?D*uI}F6+%GbDWsu&TwaV;-l}uzEnpj@utsOTO z7Piou7)Pn>Q=>kWzk#8}9hpL3ZVTHw7dyv62#N)OZoFs>qJIU32LF#>XxRtWi~auu zhSqxUCor`CFcR|rDmU;;0m*~~ZhUtf3OcG+GMu&voN_)@@qa9Nms}WrTdSr{P2+|4 zh;i2`r|HrAM`}-*0v;4uWiL4-aAkZeojYbyK1cQq5<|wh(i$iG`n1d-#dB?ol4$xM z?ek;nprJMQcYM}WHyg^91*y~R6{NgX*kmm4pBi)PE);IV$(~anHIudJAbe|dhFCV4 zBsl86?8E**juHBdoIRW8{`{-~V}k~0AS7tJ2!@^jzSe_Ayd7;9F%;N(W@>oM@)NWV z^gVf(n>8Vq4cr@)vdMFT|AOn&!CXTY?V%G=BLrm&}0 zLq5Q_3fq;BLkBBH9TR?%>W$K+YR$WGnK>UDf)(fWuCr?x&-=8~xzh5%ij$2&-jbYp zVLF6RaQi%;OqU%CdCUmu2s*Mf2!h}Uz->u8x(k-EJ!Z)g{;&Ji0DG9NRtd`$;NN~% zSl_O@8_BQq!Km)}l5E6AsyuF$ns{TZN9NKFHSNwjcyY^2dDO{Tcz=Ea4X5-r;s!C@ zD7Y%DSXU5VT!5A2Ri+QKw<1)GE4j<@W7<>aA1KKkBz_)#UI$Mg`oZynX2s0jlZg4$ z3F5=L(jk?VQ|?AaF;!k8&WCTGJ<6DN(9LmRbWiHS-pX~wimTT%dQKqNb@$TcK5DyP zEU5yC)+94Kv3;u2RT5vndb~b@uMK+`{$#Ll;FSzZhgMPb-}4%w0GG-bJb>;<09=-(8K9gcF1cpWrIjB~wGCWdy~Dae{N? zqrUzjHh(n&_fIy}S5rP5$(j%R)|MjM8P3|PupI&HKBRP4nK`Uefa8iRMU}e(3#X^)Y`lk2Sn)$ z&~`)c_kH@mi#mVBpmqdT7(PIO8GyDS`2(Vpj(4~uSY70A5S{aALC@Z2RWmV_er`qf zP=xs{KD9(RfwG{tzFg=Wq21IhIR=1}Xs+JF*acKY4q_t5EZ<84ZakS&)M7}nEbC~gLIdeB^6(&i3f41ddD=@U8D zp6C>OzjRb|O4vnr7Dc^6)#FKUN8rhVLgQKXVUj#%N+B)C1-*#4oR`PfWUO40&W;vB z%lT_*?x)1+{qZ0O;E0yPMDY9gLBzQts_)H`%n5blc!h?R|jbD?K9Z>%PxU8KR%-04ys$lv!et>e}WNxbYbb z%Cc#Qud2k{i2_b=9u52ik#i+Mxhj`v5v@}JP(Ii50ZX@Qiy0T!b-3T6W zs&`@hjqgKL+AfeRLpGdl9gKE4e0UBP^>9l%U$@!X(L-WlO(()ll1G)wWwrN1Z67)& zsi~z??gK-QUZQ8IZHyPw=JtszKH6bvD`*V`?b$AsA4He-$jw%GMpk@k8t(nTU#Wk> zM?oqk@#WHn#Id{w*vH6SQ1|gMLh6anjt%DAuWIczi-$B?C6_Yglb2!U4#ChXLXX(5 zJ2{K>T}^oX!TDJ@UjzQ7&AR^({Ga4N8M8flde-GF;EB;y4YkM`==bz{H0Wa^WDu_XY};-h_!4y*Dd#TRNGHbA*~0T)}wB^ z=f6yb_V+}_$JRfLg5TG2kpWcUvCbeY&j!SqFTKZ!=e8eiTF-kUH}#@)>#q%2#gO3q zmBtC`B}n6h>a=^7L7tc zM0ga2lT4^!@OL39vm`j$2KO})7ypdP4Q3CvP0WySZhmsaLDZdCF=6ZSF8;@w4`zO! z7}d-ZJu0M9pn4i&7NynfleJi^Iix*QENUk-SjMY~AYXI6HEefbs_$I)E~C1-d&9?2 z;7dn;Z3)&a>)u_i@PIbpv`~AbP9DbGS<-n+wUdLVa?T(>L5NUb<{@#==Vin4!UT-* zN{fgK08wiG3A*?px;!N%5oX2kOA$&hq0HDYmt}yh%@k;{9Y+%Zoi5jlSbo=m%#L2# zMjfjhYgq4p-mKmEyNupjx?=UMBwc~9?e)?o2AvzFU|y1_6#^DDdCr~uFi0Qs0&=K2 z9j!zj4T11gPLcYcZ(N-UrFjxp8au@8Q$DL06}$inpI_I^*NHLg3ZIml^DSX)jW@6Q zWy96VoUaaWHj$FB%fB7wATL?bR$H0XX9&(sO77brOx1?I|mNd&U7d@Gw+PA&d z^oNE(iV}eHmUcLr1v0ho&uo9un;`=k0-Du=q)Rh!IP}zwqpP;(RyR(@I(Kr{U zwfl|e@Bv4S-ldN|1244#5&!oF$`>B@bRn5To!=vuufBjgQwu4PY#;se)lg=5GzNWM zTe*5S^O>ujAh%CYTV z?t;>FVR#sKN8ufTgmg5=C9K_y**m5JC-~)%Fu7?jW_hg;GqP2>&~tR=M|4fJb;C1u zAC4Kak4|`^ZokegW*+12h?*V4xWn=wwtI;7|)S$_kGyp z??Q&_Ct-v%#*~V+hw~UPoZ<2}lo2)3%6oM~G6J#1x?mj%A5sq1H~Hb{rU#=%XK!Rpp3Q?V5yRwlc0imhtr6Zhn#Fq%Sf0A z7Yi1G7La+DZzw3eb?YBk(pNE!mV<9Q4V=ny7Y_yU+1VMSqxx=II99 zmj*%ia5=+6s^fObDwX)>~8t8KM2Cwkuv_)#PMl==kj}+O?%YtlRdtniLqlA;e4~ zZPIctDWyplMZYSoD{fs*?bA^j^c}zWtTSj7e8tNnAWdGAwU|?BSEUZ`tRbQYxOW># zwkeHo{kMgYH*7P>`sOx`qhqTw&vT$wh+VL2zWDQfrOPP?=D|S{#qLR*ZUQla<=@Ze z{+NIzcVb6k8oO{*mfyTq+VIp43lu=IQV^@eSyKoAaH`m6ntDGeetx%x)b`?hknLkx zfB&5LUf!d&QLWsm`ltCHP^s1#SA-R9kgq~u!tJ`}bdvhxh6)cg%=A)}DM>>wqMtks zejyoJ`%&(zc&~U2mmqdMqw2utol{=5CBh>LA*EHF?H2E3j`)Xn+gME;r~p-u?D_4e z4hSnEu>Qx2$e*wBU)$YS3DjVh!E>F}Pod--U}#?U1|;?;$O&M@fzny|44C|L$1hG<<_Y-~OLP zc{B2uFY&QPye|qbb>v4X2VvdEww|Ni&yM8_&J8EFyail(aaAVM8($Kp=6Zm43JLSBr3{eIl-gY@UX>#D56m|&Dqjkyko zzVm&*`lA0Qm=7oI8<_f4fBm_T;4|Uf-bw6SrV_L5NNVOP*^f^ReKiwfL$mkc(y1Ti zq%IbO2aX{v4fxGeyoZ-PK_CSv06QbI#%NP`Glewhu@@q{GXoQiN6d@C)rB_@H7s);F`=UbJZGnLd3%Pl64IQXU- zSz}pPd&*!JhSXP!f|>ss!BX#-_Lw%-)2ikuHgF0s_y0ae+JEI?sxU!iI(NDRMu53Q zS0ezXk;Gl+-CZAeEh|9g9WV^UChu?o#FoneUK^gKNzr*0qy^2*G7hVDB%MY#*97xC zA~f6j=EGD`pmQjr+3MW`TQ8*^3E0TugyHR{kDVvGI^FF>mmH&{n;lT_5Kv3u25?U3 z%iad^;NQDbzf|b2zmA^E)n%X=0Wpd5N1JZcgWhb!0S!Yn0wG3FGaKu0G51BLdNBi@ z+-&2U@495NDuVE0471Fc?eZ`_SfYRgrWESezO-WPd42V9gDfm*00WQ3{1s46iX#tw z=U@P3d~?n?7u8ebl~X^lA=7R1ct0+Xn@RD@mxmYg!dXCxbEOD3sgN>;C1cn zj{ZGzI?0&k+;m3WfCqpFNPO%7y*^$3vbd$xc?>`%9sspCig)u-M~tbS5U5T2+nc%p4PoBl zs|u=wBeXNCU8|b!taZMnTsfg0KQcYz@X!Mz%+(!bT#Sp*?YW-WBgHLv6Dn=dC!(4DhSu*}l9`yZ%Trz{u@6=WuK5ik@s3SrZtSnD{ zHl;q^rY6Dqnv~xp0ma$b2(60RulV~1CQhaxUHNTBZ8X`3Vz?-RV#x5!CjvS^YLgE7 z{KhTGy5_)GuQ45y)Bk$d6r-5HK2NMe4z1K`u6!}vJj#_e)}6_L-vb_lisK4Ld+yzw zh=yxTom<2xBBM2>n5@cK7Bd0h$`&f=f-fFyWI;{)I!}M}QT@J8xuO>oLjc-jNlwJ;^0d(t?+^+M=LseO+^_~005k5BB?y>2OQoZ#1t;Rh zd#6xgkYkm2_I=%%^K7hoH!U9B3$^DbS}iP)?1TJ=a?xTm!3SI#3)wkh>|OvSIH8$$ z>bx0|OpEc1%VS5nhhm5ijS=9n6;+E62x2tA861F%kMG&BZKzmtsld*f6Za!$$lV-? zNQw0Ci$h!LtR}z_w3@R|NMp+-`mJz3I(AF5g-c>HA z0?MU(5l4xcJih)fUd`?LTRZ9{jQiE!1qvwL{)502I>F(~a z%x=^gmGAd5Y)BL31AobcA7z;-RqnPlmNxkz?KPXt6A)HS91a7DoTp8lyQ~_)UoG7I6BcFGovcgm__R-0Nju`Oi_Tl( z2(O0Nbci&!*+QS15~G!6aE7Z)cq8b8BZ~UauF;N|pIF%pL&KN(1N&&g%=YXvUuwT; z6iD6>ppR7gvx=murX=s^O~S)WxtlC#$jiVVm!uxY+y{a-;mu+L-jmw$T0r8839g1TO2TR#arqORh$5qRvOZ@(bWiLYot-+MdYm z=$?uYDH$YCnv3j*lIAv;&AXA^CSBn4+?I{F$&O`%cCtPNy=tBNUbwFZvrcF#K#cDR zHNlP*yPSG{#IN=p*a>CG*p~_mtPd`6tS*hrY7Ei2l@M2!So^N6p5 zxU8-eS&Ib9<-o31D$YzqxKkDk!x$Kb-@`1>-9SAzeAAOPwi=$0b_$b5Cu zpHBJLKx>MyHGrT@6bn9%0zkY*vxK4hOa8!wKI!Ex)^~u)d!`-m=YAVR<@B5Y7|Wgl z`>{?5M5$RK?|v6Ap2I(Bn3`polE(Gvbfy0BQ)PX*?Cf5r*DuldX#Fr`V>2`p5sSnj zDy%hnrM~t*45mpPpS^XoPbyat)`))98B^cdP*QJJA}dR;fw#u#3KE{wYVr;I73%&y z`St7O2c9N9?x2U@mBOAr)Qh@M@wXKZGCdppSBWJx;Uwq~QT-*=$Q{TAE!e0_iZjCO zc?qds_4;eMg|<-_!%+}$9emoUZiy&wz^{K$%(zRP0?+PW80>~-qx9D+_5LaEc7Ryqeq+zv+ zLql94PdPKBui{p;QkX(S-fA{aC|#*NE7?OMDB7tIA8HO~>y(vXGazHZJ|7ZRce^TO zymGuFS?gbj_Rc#l7C){2_DrGbG|I8W4D9-W=mX}232;w_w=k<#(RQJUWurw|jtV%R zLo|s3<{q_`Hd{2}h?w@5Y1!O3^ufAqkmh zH1l9N(Z)T%i>p`w7#vaz2ZN)uz^jrz9b8bZF*D*Lj2OgJX?m(4($aj&XGu7*?qOap zSdL~4@SD@y+M|U!@D@vl^vH2WbLQ2JAeDTn( z*TNygbYCUXFZgoOGR@xgRR(U++{-+u>=Daw{Ku^0!t&8V!=u7VRd)i;2^Y(vIWR|j z)v4o6Xd9;#<;;@Jgww^Z!xRrcHa(CL(e~j%G;rm&T1}*MqL4#1>=nCAeZ~)l0V#;_ z)Kqhzj`dnM(nR=s{wM2Fbt=CBB{&)wqGLDzj(h$s*aePa&-}=X2!*B`O{ojAu5{jc zRAm2NnuO0yp%$^y4l$ulCG3vb@aoMEA?Y)vme``|*4p}uyHL>pBIqHYe10PYsH_xx z+Yi`iY-rMO`!|Y_XLV5+R~yIZM3rz^ghhL@tnV1r>}GqAp6bPo&&nfzFK6SpK;qMa{ob|^jTXu9$T!B21n(}WI*Pc~AMPL_si(%S~a zoX?s{sJ5TWDHZdmlB98OdhJF|T7-WO(J;BnFm=eRXvmcd`EL8lIZkf)z-~=;#Y2vn z(}_orvxhDA;k>viFXDU92?CsgPyzElt7F4Yki>GhTXB|{H(6G$KFd(^TY_F@4uN!) z!anQWOefZ33A}6@o6l-SI82oL2Z{sZJ!E3` zXbY)v-SboW3nQOG>jrj}U|nr-06KzuBh2f{{100V`ja!rviuK*=||`9b_uZdf4j(( zF?ZaKSI-r9DsxD}CJsCY=QRCLhpQwXD4gNXhE6{27jgl8oX{E|rpp5zNEAR_)}&7@ zpFC84?)T$rEQ@|`aEP`=a>8Cf-H)0v7sHpsAECdn1n-0A=RZb{fe8CgJL5H^lg6hY1H%v9vwq!lYFtSdpw^lIex^(5D!OZnau@6;vesd#~?2Ey3 zKjdK_M9pVQu1X2|j3pH7Z@bY*rEi>zZ(Bi{;2_hwzJdZI$xrVfgWnFsQ*6Ps)WkPX=LqW zzj3{CC3)xaI`+c0-M9SD^DGIwFZB2HDy_wloHG;SefSV9VG@~lq87L@7kreP#~zF8 zk*KH>U7zhoyD>W#WT!ex*|24u$rzu<>?(9W@Js84tsnaHD3+utUXwkqxVGm2e=$FUn91iGbdo{*GpF^Ds+a2VmEKP;1VSySKXsgbpbhM`X}Um5aN>zY8}(ATvXPdsd$z`My|d$#d-fl5AXVn~LK z!_KE-b39KxrHlz~k&5oZLDBl0+TA>Mt#6zd!=0!Uyy6rSY9q*dN9=BeK97mKc zOMi&Hh^%ehscw3;|B!}^mOXT!K60LGySP_${QF5yLU;Jjv#R*vpk1URyZ2|Au(xOu zKV=*NZA)uE81=47xVUx@ffBNTo$W72tj~I>bzsfK+r&M4KHh#dFR!*OnH5A~=x->Ezw zAr;BJfl(@+zQB{$@?`ZWt7c{*VU-^NnP69P=L@&Zd6%~3{vJbeHP{*9{IkMC^lmEe zcM;M0!rLW`CRopXwz`dWNyj~(3seY;&C_VEEPM0uc9iynru3oVv9j!KRsza|ofXBSP)2K^FP4nD&!9< zyOiM{tY?q`=bsf7$bj=Vkez9t^#*R*_|_ejQ?dcbvSwQ&CMKKufRevlNpBD^Ix^{> zovr4AcK^wy0A9*=8 zI8Gf)yi|~!3DZN$N_<)aI1AS|JnR|nQ5G$NdK8R8-^$i6w9RY~HsYDs+rF%d zYWLunlagj;8ztYKJk@$ebmV&)8?uWXm$SLrDcgNIe)Lo^wb(gN)&;CSE#HxSAEbit zv0(T+h(?$a7yg*Fr-(te8ud<=MBshcm^NLYYcHObSl`Op({ijP`MFX~%N5Uyib#j} zl4Fv}HwFDti3v86!Ht#^D~7M8)cGsX*PmL$%A-h&YTisHxH?W{j1LkAji!VqxRb`L zar6N5WvJfOoOv*O zyE&s$m=*iuL|lCN!^meK4cRF@4z;Hgi;ntoaA(MdlVY#If8^bawv@$KZGyL;OD`V0 zUE&>ibC#7v)rgQYKcbke!Cmvrb(a&85W}aZhRawKXfES9xBKoDyF8Kb!F^xB#s5Id;swyQw;p6 z&XGSZ2m>CXBi~p(w#JHkm|WSprIa0HO8;99bR#!&B_epDU@M<3+P3(&m$F6`WE{yd zxXj~&6e#dH;;&jHJ&0uV&2Hd)aD4M1T7a!Ku_Ea1PZcLyCx&;#(3uV;wRaq2wg_0- z5UK9mOsqIEY&US>W=#*H0qkjwHUlJ#)2AV33+$W)J*pn@t*DgWX0ee?Du7{j-JFOd zFLr-DV|O-)UWWl`3E7>;&iPWxC_>1U8OAin2qu;a1K z{_^}(*YjskiAV7TP2e8l4tD=h(PhUP-hb@Ys&0lo=uBv0z5KlDk`Rp`_!>wx$3;OW zqLRU`|76s$(;X@Y|BVWqnIR~RXogh)1;Hzj%KB!(9G9 zP#Wf6Hf8?H*2{lG)L&;S)H^IYLIpa-E26$7LDrgo?sZM7_mYD2Pck0LcY@&bQgYV7nmYCG=V>&R`4av)Avel?%)CWR?kas&t&3X=y<$I z9jotR^a3hZ?AzI-Z205R-yDgQ$9ly>9n0kmKavUF8lb+o=k1-!Z53jfXX?rrpzqyL zr2Z|YBAH?Y>4Qgqtp89Au>M`a`VV;OCE=vi+h}zKNWGZ|Su?)rk8`7M7}Ura`lBp? zb)eyzT-g)f!gZhcp#(uCaoi;yMp`trHR^k0yE|rxEe@K2{Yn!@UXrshZQ0f8i5by{ zOK*Q*YD>HubMm#_4dcY#Sn=7l?kD5WW%TJh5nV^J=@a6cBm$Xz&@9V(Ru%R zr_!sI30fueT@&6aW6b^15xk15-&p$lmbq_^eWS1VV4sp)ap_Npi8lB$=&}O|!^kWz zpSmaFY876em+N(3r7dL6b>1JwM=0~qtjC7K#LbU>uL|>r2(5cV{H9r=Q=5aR9&e+O z17wd&6?xh)uii^xQKjLK+vwoKSAqK7X7sdM4-N6KmwvLBF-Na!AICYX;`2;2%>pfruTR}`~~pRX>ML-zN~$HoU{_EJpe;yrms>UNb zd!0*VrWs{_GK_UU6I_uspdW^Y`i^dO(qaCVhKe`GOgC&k3aPO>5sO(B@GY(oU^FX` zJ9^pJ)o-cFQjE2N+0$DspEYr2Q-LQcs)=uj>q~UwNv!Uweh54jBL|jboMYBXH(CTviLOqSHy6Ff}6MSiA5qSrqrj^Hmi}XS9vum z(I>MywbCqUW2QW-I)A#L@@r`pE&>fezd8Jm4AK6(IX1v8(O(Z z09_%`<4Jz##clxPu9(FYFd0870mr7YxGhf_vti!|DSo=spQYwYy{OSIZ9LPnigF6j z>fXm4=BspZWD)$(8i~5*6K}x6HFFagprht)o?In`Wr#Ptu_)*?ByHj zsb_X%!;7J8Hxouwa2jnR9-g$;m5p zB!6#Ou;Hev@O1^|lF`dfUW}{Rt%q_1V`si&N`vUMa};AZ1B};|Nm5QzPQ9pSx%9s_ znceW_iDAp{4v?MeYSR$I;gGk~b;wX~GCtGC!(RICv5vQ(0n4ox+Eb>2t1x5(zkx7O zmL}}Wk5p~TfnvoaB-*#7BI($o4h5g`%q3SYv6aki<^8n=yOQ01cZS{n#+v_Mu%>Xl zvg?8VME;4Y8p9qBq&$4uo^10jRb&e0jN;6-6#U=*?vnUNgD<~33&8Js5t;X`ziyps z!#-G{_`IuPK{Yp@{bcg#D3M9>AunmTB*!;ctjl|b8n+Harl}7#CEHq4)HQ$3ksF&R z>z)#Ne=A;w7`4Ll^tTXiI;Dp6sdY_BpYYR>R00WlY{lm~(bQxU?*uP214{OkzgQPy z1dT{r2|2AtbF%)H5HXumLq5@nfe0gl$MW;u*c}Ox(sn)A*Qj+SN>iOltpp!NWVjDx z+JsK_2ha;XONfq>zjW&u+?k9gSyMFHC0zPK?{zDJ&EE73Lq&BzVP91Y{TJG8my^d) z*3Qe?wVREu%oEf!4S8B6LmrtWOs3`J4zHiX!U7o>=o#uCQTOZ=7_Jj>}&gmfTj=Znpc%FdakL8m zn!n}o1WoE!)Jv=2!-RspLw}*cd#pf42fkF&?XtAcI>#&v>#y4Vl@FWXsa(>kbBlhP zXYQTi&f4>ig->ye21wcNo19$h%XJzMITW`|u7@C+ADq$Ch(AtUZiuvOFuS)yXBEc# z=7AE~?^i?fg1Q|Cb9*0vt@HyDcTwLHKb;Bw3mF)0z^15ZaZQyhn+Sv90E#)9CwxNn zkTszAQ#c>}=-(3kg@{c0*ILwM&@gazoZoY0$XqcVW<1mTVvF!#u8l6g3W)mCAh{L5 zQT{o6%Ft}*0|iPR#IX#|6Pn=NC`hxdmP4#BE41IL5WfQc%uk{a?lf+@);cQa8Nt`f z_bTqzSh2h$m7Sjkx&3h_Lw<9wzr0!f8*`TZ!@K-9oc$k#GrbUQo0yQ*$>VVvM%Ox! z1l77RAAYhDo?a+7F8*a#M_;*wP^lkFl+M`ytKV<-{GY#N2=mEcS#~Y7SeReb*PUkk zWKfA(G0y#wEIVlNXdpnhJB|zKiL(b`*q0EBM3ORlHe?U?+f~RE zgK%=xO0*lCX3~t02EnDp>DO6D#k!(73ocG@oXFsjus)Nnv>Zj@L)OIl+*3KR)-q zM6A_@#z9h0F+1?41nXb$c4PDE)nw=@8V||u+i+0#9mIeYC2U$67iCR=#>J5fif=L`r*(dS97sp^j;*CrLnA=NkY-z0bB4mR^(D{rO zPH?%g^ZG9Cd)6*`rT=ta7*z;xg#6*PV!(I2@>*41pUg^N<{qE}A*=75&VF3x@Vpa< zmU^lA_8UG09~Ot(pR0P(*3S$2&F(xy`1EL>rd`^ zn(t6GRbyOdb*craty7tmanorrx{vswQ)HCpoI{+a>Y?tFLOecs|Hrth4tiyI+T-g( z*U|F80hXlQI0LWTm4&<Y35qrUNu!qAoI5{gE@jdIwYwK zH;Ti8;M1kX)=x;*UNo{PzB^p6KYi;Z!pD-LIKH59$SlIJK44gmYmj$2z!x0M?MF6n zo<4%jx=z+1{Xug5WiaZyXSVu#^B{)nOQqH6X$S1E8~BG84?dJHzXbsl&3f(o`_I89 zX+&NfDB_R^?S6vlepCuuT?N!=*V7Q0umTQm z-Do@#-RK_8*Oh^Kz7t)CBVin)69!`wD{t}$cxm1@H69JK$LHe&8|fqwi+upYIDppR-zF93K}!!h;#kk^kGc3$K&}Y!?8zP#lC;I;w8+}zqEfcd!yJY= zNM&QnBTSHvR#6=6T&->>O2%Wac+$;2 zr%xd!p8S2-u0}5a?ffQ~04P%WymWLpeR94trzHwM@IlARSiLeSe>2Q`dd`CqaMpXN zdn(qH4!0GB33(rD11tzj_$x74cZwNTk&m9z=~7d*OA_T>80@+ErC-~xRSWrtU+O_! zXZjunIA{>XXIOEJKK=gjD^|}z&TplKY4JE;2z$RrrsRw@$vGC52VM4MCdrLs#p$Fz zwKA@ixky!O<@SoT3}jSecBB67K=H$vj%+8*{i-xFo4T;-LOepQxxL zwTMwdZCy2|@P3B?o~ygkMF9QrAtKLHB(7u?H{&{ z8;R1T-N^g!Mh5!*N(($ zzX|P8oS3{+ijX|U4G~#;uicC}oZ%`f928n!QsL~ z>gvmzt)Ua;F>W9bWVXt047m_(b%uOE9DalDtl6Fd@)kFQl)T@sd79R~o!hgYe-g%j z7AtB^Hk)?4-YzLGNj+mNoMUs<*2&Is(fF<*%RBa}sRsVPShy-y@eIpEr*0_nT;h)J zn`theP8anK1MG*HZ}obZ)0?VRRDL#XX{qHhXSxxMX2kJ4^``1_>slNTccU6WGFkz4 z%>QR^6&*(>!nN!Q0?{CTQnj4i40V31AK2=Pope=m;mM6UV&{wU(_(b3D#wNF;Ya^N zWw}3@?tieR-{k3!+vJz%l}QO+8e`+O9?OMse>8fQ{nTnPmhtc`eo8I!*ntfAAw8q> z`ibr%W{r*Fb7<6(*38W^ErNTt0vYt>Wy$kU4GyPGQJJC|_>~E-1NNMVy1(3R95oYy zI35;8L^M)dX*)Fscz)Volk3W0C2*7o0KX4SGLDFYeXcexX2VkpRJ~2t4XuJmx8ymc zBi*At=^+b5QP>Rs$&EZ|j*m*D?#eH_3U$%YZgt?J(UJ#I;-V27*iLJkR#_07!iAgP>e>Uh#+~Ez=E<&p+1T$wbP&$+UhbO(}F6bFjE7%-KVwHeUjsI0N7kGr**B zJUSNE9}i=MxZ${IcPivG-GQcJN;4NK0p$P9jQ;iZ@605jJA{N>QaQ9+Sk9SW7Lt@F^QO@N$-=+)Lng@XA$_@^z4NTVwrspuS`i#> z8%|klVe&0afr!*6NsbmIaa|=vTD9MN&rB}C>tNZSuUXTW;W6h_)Q)Tq%i{L;HUnnr z%%=A}DXnQ_6}j)mCx@*l0^R6nh= zNoFK2{Q3aP1{xhuDRqenE9(gH5_&j{FtFGQRxrzpg`eu|fmK@MheJ-*6KLQov@#xg z$5j2yuxuV9DK@(^U^u`MKKF-hW3+rq=Ld2Py1Mnm-1fYnEIZ42o6atoPlt!Ds2J9w z{_+EkxO!Ic^*aQ6gU|@jT(t_gfK@h~jv}MkSiLNiK`XaB_{yH2;iqUWsP!SJq7AsS@g2c4=QNfeJ?>H-KoB0d1+#vL=Bqf11yhkb8U147w1|evIP@L#v zFy_4;7%=n#irM&Bg-u+cHon}2n_CmNAra*K0WHV zMit^^3t4zpIz@4o6HYJ3Y-qHkMm-wI{X4h7xmw;&f7>*=0#dVeSl1ocy1Lgp)QJx^ zO3wf^Fkb~m?9&B`44e*fK;Jw;k<2k5TUWsQHL3nFvHt+ZbCot^N&}Uw*|%!C>T$i%qMWL3`}vM8tS%@sgWmKVPKlRqf7#TZeB^bsmWK%w8hF$C zi1L7Jj!o5;2{*+ooAvG;R;|FHc)wOThChVp3UOXp6}O)^2ByK53PGgoXDA4<1CK(S zzW5G3i>&&Ew#x=PR|Ntf1pe9N)+e&ZG4 zQC`fh<{In5|M~nN)h;UN*srZ7S8Vf>$|javXN%c{rVt(t|Jpf7Gb0>BF_gI_vN1 zpMFx{0+-X5(Z#c`z7Q~(#wf#HFey7)z>QQ#)3;~-x4IewCRhv+)_WTIhKBlFN_d1l z;cE6-COE3Aap?B;j2zdJ&_F-6Aj=cNU5!|Aa6=^cULX z7O;jtUC9yfmWTpl{|DnaFmx-YfjOL;15z%PuLz!opJRLiAFrD?*lTR1L+fK7P<)VV ziw_Tukz8sb6EzfGI@XnkLa!ORvNQ}aU%xA7lJOiJtW@K{esFpF_eD+ZrfiwuH@5X^ zk`H^cyHEro8vEdurPH^0&I%`H=I<_BENEx$awWviUXjINs&14|Al1AB>vs{*AA7tj$lAR%yb!`S|tkO%2K%}X5gmVlz|7|ClDMZ79U#l9=TK)mM z_RHe=Usizt5e=bU%TU`7Yu?W^uJYw4so`y2+Z5I& z6M5H~XYR#Xx7RFVSUuO$T1AQz8Es8lU?UFc#N^&G>h6T4weMfZ|3cHh5|6)t;5Mhl zP{<0XC=wB|q7-HCKV+_={p1s;e#mz(qxWi1R@?9dQgSi2aN~u0<{74Ty56*485bKg z8B9%%O^>O*QWUD{XA-cpv-YyHF1+T9vkS)QRXat3ypPoi^M<#mr&JbYykjwRoyw+2 zgMgc&SjCaV7u&}0wx1J0u)67tE||P;`7ze_$}po1mVCa7Gq^r2` zPH@bCTnZCBd>elwWIRy$NPg3d&CmJ+x@9u=@_x6j9wE2k^astUsai;{uHFc1yC-3> z5_q?Ww!&jfcF*G54`j3v^tZ{$p06Rea#RSbb6@vg7Zq_88Grkei&J(Hy$(!gV)KgI zR-Hs3Q{Kx%NCmR@`^0i8F)l4eupYl+sJ<|+)H4jZ#2_d8dd80|p^kF&b-L*Pq7Z;m z`3LpzFMpYAI>R~O#PHPN$I6eY?}Q9tQbxm*NEyeT4}Sg`L8(;IqZ(ZGx`jL6IUjTH zqOOxh{I(A_rrb~r(*kGDvtAs3(ITl6&{ZJV;8shlMyqO@8VpCMiD7R3E=NrM-LhnS zoLld{Se9j<{!fYxK;@Bbc_eq9V%_*T8Wn?4Z5&}JW+z)r+Jt3eBQLwX2n`8B2L3{G zqqau~7{5+2pB;&!AL$BLEsOT)85Qufb{~9%b4t`b>OdrKgR4nBFl6Wgv{x&b4RA{| ziI848rEU~_Sy4gl>-db}IQu0Ea*N{KptZjWHWR#&*K++kZfd_=Ax(Ir z5*XJ>DlheV>yDLT?1d~HR}?%Ly8eh{fs6P$j?lNks0~sQyV!o+w0eTkP-5gJ7J^k?!mX3uZ)YEHk5M5$fmyI=K?+4nb}uXz*o2X@Z| zRQr&5jc;brnZ2YW6x$ccKufk>?P+X_zfOvg7SCn3L(O&BHBZCn1Y+ZwK*TRw$^M}8 z)eu}mZ z^9yrhf-1Ni(;U;R+Xd*y?vJh+*l1?f`ZpO}J@mboP?Pg3h8J-j*PO!_t|j%K#$W%D zS{Y~{{!%$@z2qZy4sS91PoerPTmNwTcjcmZ23YAP!(PIzgr(C29ly^r@N-Y+ZKe>k zj*2p-?fRLIhQ_9Fe?BM9-5s#bTCXT_J~onTYdXP5*cgtyGp0=z1yxKEswIiA@k$|5 z{g67F`)G68ub||HQ62%Owb3xKSO>b&?Ef0G;1Jw>2()=oS~InU~? zB`;}^X^_MOFLxa^=zZ))`(eZ17`Pq3`+@UiVKV32$Jm=tlB31QrCBV@sPDLv?(=3s z)*e$z#-gIXr<`-jU}9v=$iB*}_Y6== zcxgM%VviXP@~+>r$>9MFUD|Owyt54V7MV~>O>)z4cn3o3J$>fz{cQ7|1ps>0Cl!(; z++G^DR|AAcNTV!*zfgN+WrHBPIWzO^knFGyy^FHz5~d3FY93 z4!f^7@V*`aBuPjI6CAr<{R{2K|*DRjA z>wp|2l>=E?Ge7og^IqJmz@n0eIpE2GsK}ANc4RD{@AYuY?m56hwRnlt6<*t3ms@{ zdfdb|43P!?W|qDZC3I!Z@B)#f4_}8SM_|Kvf zOG*;Y@-6dHU5yvzIZ)kJfWw2;*g9_J0g)1P>u=uo?tu{nz5@yO7m}vc7*rpN8dQeD z({y8uDg-rRHM5?TkgFTI>M@z`_*$q`P1WtnSQcs&wjZ_}z%GC6)gh~Z4T|7B=IXK$`lTI@9zXI&;Ag`f4xnlqe${kJ*@MhFUgiKtv@wl$QB-(=4Dg% z*rxLGve7V|WmwX_dv4pv!=9;%{qaSy`Q5Z*zt<#JLUOWNe~Q%Fh#`j`rEe0eoBpT} z!I^F27h+YC#dyW&Qxs3$q7^Accwvhrky7cEnC*PtY6U?xczvlx6&{wV==yDCd1!u( zH%Q|*iJtyw$cWebTG`HqIhN!{5n8l}A(|QBhd`X+gRM6Mc`e_-2y%MW^v7ry^$pW+ zd!5DB$EdN|6G(frzR-KOkePLI$T`Q!?@~_^{w`5(h%&W(a6-z!}8fQDB_W% zvg5a2F7C?pqOLi^FEWOhOi^$)T^Rdtyg;nt>&t8gu!%NaTMKW4n<6$XoxVR!Qe(0C z^rLTkxL>aap*9I-%M6@R>4LN)en`~-yEF`fGG77%adZP*%Gi-SW+*RX1z;w+X0{(> zMF3fj1S<@vlq=2H7R9g!QU+g$mjUX~6%`4VS^_rsUDFjzXMr3E{isiA`KN443`be7 zz)zcUj{V-%%e+M#Kfd?Pt)b)+6HMXQ5Q>EPH0dM4dx9nJMN5sT?Fb!DS+o>(Zo8UQ z`VXd3k7wlmGSL%|24QVAo?$TYuGpaYb>N7@^+Db#Kl+L|=Lac)x-*3$jOLuP>aRHr zmG&2*%@pFk)-^3LsASu{UGX{!{N!F)K`pfTWUi$&R_R6*Us=OlI$_b$Rr(iiZg1xyg<3QH@DP(&Dismaz z53fmdjTmn(Yq-{fQask~m*ve5?x5>)%@f9;P1#w4W|b`e*L?7Q#TC`xvrB!arMv46 z{jLmo4D|`HWJX9EMNGzp$ILe=zEf#P^Sm^v)OBtVeXyg3i@M9O+PiYJPij!z=q@VQ z(saFbki5MQK#j-&FiH_86IMg8wv)id5#uRyQ$ubQD6R3njXA3G%p=v3W1F;6@-f!9 z)@=_>7-gU7X`G@er282MI08)V{`L{)GcB6egQgdE^&wQHqJY#wC5-O(2^h7}-C;Vd z^Ma`7N*vJYKf`D9NO^a1qjZPeuwJ5rtZ{5;--r|G?cOUMO`}9o#iur_dNNUgn38EK z1QyfK80gym?x*8to8kwHyRC~g;*-ZVO+ln7j`_$4XBArudCXKEMJGA%#1+?}r1no2nsNxbXr z^C%fQ;9LA`l&hO})3_yg+l8jCzY6~m;gVq4`i~(rTqW1|OnFnWoxh*qCg$4*XT#y<&@TBjv?wcSexefnrsm z>~YfBDrDVX!fx*e!W9a7gXO?E_;?HK$%6SyTlcr#`D65R;x2rs%p+0~8=u#=V-2XQ zm5oy0^UwODZDZd^X?VpJ>gdWFsT%!g)$jyV*W<1zMtO?U zz2$ceiPyBSErz;=OM36XaRbdo=Y0~fNy1|vb!G_i8|H;$FlC(34~>sJri4z4Z6>Nv|qYFEI_1AGZd^@G13woWtZec?UeDV zo+as#n#_PS`xga9rhkaLJ^_&TTV?-w`!^m0HQIvg#n+ZwH)b~122fY!UbhQiGwrLq z{bpfoT*FM`-jd~-A>65RLZR!jANY9J&k)Bh45xdjYv4@u3Tv$HdG^tim4@QvFjug& zR2_a@@<2)N=6werjLCb^ejl0}sw57+S-^MbI{?l%-aMw+*qY)mG4K0qHd(9C_t;DI zMK5he7)g!DHrDDvQ)Nq9sq4k)hgHQ|pdxf#fDQl%@f+9_+>}~0a-@!@w(H0E^5qXH z7hHQe+@neBnMbN~8DrZPBuIXlg~yh* zy5LzFW^-uO`Yk2+$FnpWZ9w)6eF7ydYhbfmdt(T`XI+RkV}@aa_NiFXs?g+h!TXat zEO&pQwdm`e9p7|2t=hU$X!%Ii@~QN*NTj0e#v@7HZ%eIaU2EN9xuiCp=v(fal#6fW zb#W8OGq1Q}37{l3*pOvVXI5Ez%Sh$;T^i?8T?cA6#1FBJ0*)R>!XbH{@EIWk-AA$B z4dd|(vUNte=6@*1Ml_{y91I=7uAP(C=FMMI2JNyDbG{Db@ zUugV3&ER$QB>(XNSC0!IYG3A%jfZ_CD1P6G7!uYy^XWI(CtwT8Ash->hO!Wm;k&j4 zJTMS^9UDbrzUgLMq|2A}*>$g9QDu9vEeno7RQzqU>dqu}(ryh2HlTHj> zNF{ty$yMwl3q;uEvXTa9Nh`es&48yvvgNIJzUAwDmI#V);GrStEz+3palUWD67&mg zCR}0c<(?gIi0kqo?Jj8(%pnnOI)0EaJ>)0r!V=W8=V%23$PqkzT|8a#Ygq7DH7(fQ zuovAK7`0^IzrR~R)+HSn!Q!J=ztFDZHG#mT!9c%OWpOZcJqZHE1DP(xvE=rZ86XKh zo8oa1 zFp}H=p|k@7U1E&n%-@eDVq-pi=;zXVYaLy9fm4IsPR#mwFH_z(lPccj*Ez|)qrgCw z{R21tD_Afcv^#mC22#au5x{+P64}GneaL20kU$i>xX`@>%j%JP-F&86+T4E?cW^<_ zc}--rR8)uu)TTBxK!X?DL&JKGJ_|nLS!ldev9?TaFD)oH2xMC-SAOK}J9y?U6~Z1?n@rL#eBITEInr?0jpbNWsBuEQf9^-m zX89{^;9;0hHZ6 z)q8F9L85#`1vw(zfAf=28BW0vrcsZUu$bQg1UqP9pnH=ij<95V)d0vE!(07>brAn%-+4T@tIzb!ihPDT5V`HU3E<>C& zk=QoakHsq2)Nb@l?1+@dr$mV!Y;92(HT*)W2&`EBYKy3`;N#AC!u#;}cuKF|-q+r> z2fqsu$y3qXP=U*XwrYX!bMmeCg>*UcJq#J=(X`}n`r-c|r}82M^+~dVTD6Qku2QyJ zD^&*jLsLv!yxPiXbz#JJg8j8hPNLUIO$#~8j5*>YjHfr47uESgrB)RJ&3aZ@lyRt(qWCA@&RA%pzHdj~@` zYE(V3ev}BT>@CsFiAe|LR^y{*SX=G#f**vjO#7{gCnh`Qip`E*K&eg&E5ceoGr3by z5(33v_zqN0qqiNBbdy+WqH+_rKANqiGb&{-tqzu4N`!hPaT+E@bc6sSKKjxU1`5yO zfs$#YsSdV-CX&6?OZ|=`i~~-vP0iE6Q@f>7Ds)U7^XG@w)nGs2_#;CMId$J zK)%OR%$=jP(tE|jr<%lE`sueHXzInq4BY+HH~8gSXcC-_?+XwTJ;2_sh~-=|ia2p& z6?xz{(syb2-C2(ClPh-?LJ>zvCK1kh&L#1Ut|#Tl<5m2^4m2U6YN|3{%`ZFaoO?mG~K{9X(+F#jj+4*{+q*Mr~@rC zK_BNMuX|gJYCUe-J}0jCz3^}t!3kC--!}XSd=>#%n49BZst6qDc`5^s9uiIu@(t&5vopo!o#G7fnbgm^p;6A zNWKpm4@mN(8AIl8o)k+O7v{=FeCQO4CzB7=$=! zV)R@HDejj%WzJVJzB(I+-XWl@j3FOs(_9jLui4QuODw5q;X`}`;CFRNb>LD6T&^rL zQyGZ#;)fqMjTtHlqK>-BMM(vd+>lk2PFxRe!kX4*ShcqTUhD^7?*z1V1~k$Z50#g>MzHtI!fR;6Q=C;6~ptvK3d zn)fqGMVJZff#wq;Kzyeb^2z0s^YSCeCIjI|%XA@n*4UX94=t!+_H9-Bq9G4~Sr_(l zW9RT}tI>BooQc)rA5;kk0QGASG&d&guUF4`LR*>k`t@rQ_KJ!;yTrnkmaM5(|&rCayOWF_2TbFXn>D)(f zRZF$x4<|vyvfymNKFAN5wp#o3G>IWM=LsKz_taNGzKS>%1aUIiK-V<`BC-ye0il^W zkj@0u{35F9EjXbh3p7N*O8#C>U1wfCUPVPjahlIcVhTv>FKiUb$+#7BIE6TK0+&Cf zx(1x}TNaR%4G~81W|~pf$nB=@Ne@IS!oDU}m`1ptZdYD;pEifU#S(gI!1t(f2o_pv z+gV^!nIVX5mS@CnDZO6@7DT5$=lNSON*}=G+25^)-+IUB>o2sugx&LZ@l_y%1dKoD!>KTJm5p`v3a8OpkJ}kb2jaPJN!bP4VS2r8uWESk5bAk`KVm|-Z!hg ziX>=t+{ZqRAZf>s@5C);llM+1xfW`cM)5PeM3T+wT^P^bo;u2vpJyyr z^A@%&ocEq-bHSnJ2y^nG{3o&m4O#Y7mWlwzT3pL!0wXCdgQCo03$#>Q>h~b!oj0ok zI3FhHuhDPrJH6#kH0lTEE^iMr))Hc0X$PSxM9~trZ^SMM@UqgSA4%-!`aRTZ6KxvH zi)Wz$9(B!cF?7L{69Z!mvsRC%pLbJyjoAUT?6L>6iTRFyWH%kPyQKs347 z!u1(i>7I%|AhVOdT|J=MTppuE^Xb0&2MOvo@4}oYdv8{g^LFYIL%W*VlW!X)9C+a@ zswZAbE1ngODd_r3;map8uX6sBZa$}9?71buN9?rk7c!v>d+|M*-;c7?%%|@JtRpBr zL3vru?wsYgjFMV{po1n~$D{!xJu!v~GKaU<>FHBy$1g&=*Uke|&Qzw%N1&xDsS>}? zOvLifL=u*JZ?ECXy)P|&n7DO1{yL(XG|sN4;;T%V82*`PdjdVdQ8b68w#r=LR>Ufl zAns^Fq&zi>2k+yuE}=e%+9YxtNr=#M%p7i=K82=<B-V!tFW)*UZNg$-$bVu5 zEr^HQ{YJV|kWRNj?$X_vj-AH!jUMgH?L_bk2$Xr^Ws2>gNkZAle&%nPrS9-kHO%eE zO7R$=!asXF{#PE||64w@`Zp&Np@S(cMtWDZcmG0@RLhYOus_cs;`&Ve4ljjQnZ}&F z1Gj~$j^y>Cr)r^{j&{EpK$9;K;}J;ik#e6VUOx0KXxWsHjKjvb`2#NXCWG33mDIht z&&*lZu+3Nwo8H}w0XrrS>cH`&=)JEifMu1

`s6D!#@e-##ohqC$Po%?=H6n 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 z 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 literal 809407 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

e_d>FAIMU4aDC7x0 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%0DBluk}R?fhAK6q3U9T-$G5!wMBKMPcro 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­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

`mJBko1p7<%B)ABI*88ns z|2g*hB4KBW2$KC#^IO+yOd%8XfD9o#i`&6McJaVkGQr8SmJMEt118e;&p`qD@sBNP zj4$>+bG4@&1S-5cik@A85SDH3e+@VJ?V-ob15JV*zM#!;p@>O0f_Xvsx&Bb`C+hgn zwTa-{ge0E~0PNdL-366J=XPorgD7l4Itm(Bt{UQ0_K6{+5o4w7Yk zt#^mWRXzNl1`Gm_7{4nBx3UCE=LJFt-k#|OGC>)ectQqnlC(v^8u8cy zJ~UxD_Ez8k(Af$BGw#U=L9x*lll-r=QH0f9c9C*s|=zapjNwZv?` zUCZ;J%I5tzP(Us!y)i8!={nRUpd5SSaNj$VMa3fhlBJ&zYEq5EghhlAo{!Jkm^LRI zi9he(e-XlpZ1dsB&7E#=9KGFO9i%Q;YBDPvgG?@vSs|*bh;%io5?2cjDq6L8JM=Nd zfQ?_lUz@MG(LZB$hP60=IdS!Z3O4*;m978IkVDtIQlH)@g7?lz)54Klp5L;Xzbh6y zo2a)kv;<{5u{W>L7@uquv0Jd&#n{=J?-@Bc&D-246@EbW>U}I-RCXX;CY3xb(^TTq zE<}BR=pg$Dd;3ipkS|ftC=Z{LqZeJ57ul$Qy2R%gWRyfgFLGIhtzhTB^0H0vlo-mR zU!b2-m{Nk$GZEJ=in%?N!K5{O^{b&=IgecXCB6M35i%xQ-SgWNk-lk0_Hojdqbq}} zM9Sl_py=g#?i)Ic?=IXVF6~HnRg|ZV;Q%VPWGb71KM%2n zB9cC4EJU6L^z-eWdZ=+wi>cUZhsUH_HYAzL7Tp+%HlvVXE3D>!UCbJ7V4ydt#)V{L zS4VBUN|N+<&wxQabu=j-^#+R>5EYW%U|i^723OHrU||{%B>V}czxi-B)nv>Tbfyrx z<8o3DV2x+9KrNoqA5VtLEQc1)Ma9VioJDP(XW)^JRd^!W)cLJZ9|oegt-J|tX(kZv${&;L?yV)**0^R4U(kvi(1Q7|ar zz$~;r8N$V)oi~u!L4Ur*#Hf4agEjS}dWih?mYVuuCm(d~F6HO5mk7vYEL1xG3+*R? zDwjI~RZ({t0;w70KcR#KNH6o!#F063LgUsRD$}8F?Ote-EYe^TW6HK7kIcHm;UQ!% zub#Q0mH#l}eb270%*TzqgBPQEp{Z~`r;y^d$8}ndQuYhqmv>%chrN3I?!m|+q{meVIiX>NKw}$1`!>R6=Zue3FEx|r7J5W?6m(8UjC#K^ z3Z7jt3>&=t@xw<>jw4ujK{l%uisB;08kP+egg28>CyLshe(>*)%Dl+ce}j(wh=TcM8TM{S*H)jTHq1Rf#>|I4~IB^#ng5evCT@ZGi3E z*ae@7>_c!dK#Bo?)BiI?)(?Mf19Bz}!6lUAKD+O|0vZ+HK=}{eTR6$Q^gcoVVe&1F zWsl~K%K%A->x;{aQj}?JmeVdkXCYDfEeVn zFZj9QE@PTtn6+T_a&*nl)kNKgzd%=!TQ>_#8-chd63d%vRlYlH< zvbejJtIW?W*Gx+>WEpLak4zy_wfiSccuIaXiU2N=(DK}F3xUryD^22hkE>I*{GMS z0^e6jsXnh#p1t3aN)>Egn)2)bHFVw_H4HFRQZk-9cBo!*)Nfg4ou07-s?J=UJWo;3 z^W+T*^TKz`JcPCpHQW3AH20bGWv3a$n4rmnH=AR}DZOxyGQ(QG zVF!Z>Me@ayu#Qr}TMc`H_Dt8pctcef$xP?AR$e-MuzIZs)-P{|iu*DY#M=0$hfGZu zZ@8p7+dXh#k5zu9Qd~$|Ex;-|m;`as`wp2?pv$|e)Gi5PoPu?j0r^lz5}O%kz*gFi zfy@AzHKzHnlj|RWKL9uVNELB3Fe(PzBk_Lh(@Aci_9!o@(`YD4y-UHQjyQG|5gUN7 zhhTGKj}d1y<*?Sw=Rmp)Kn<@1QebLgQX3`kH-RWoa^%Z^>)JqZj#QbWb^r=eJ1JfQ=Ds;*m_bUa5 z7F^vzV-#&)nkwe#>G7@>%d*u-EKdi>Jv=kUQ-8rW#V$fMYeg|){qGX5n{0b#2^H_v z)k<^A=w5C)7yl;mHWksmz{CMyC7e(uzvXc`Ul(L0v95uvrO_;ZtwnUsOOoEMSpcI= z-v)X9#Nt^G@(oatsM6B%3yPa{r`N)Z1gCXG*}R-m)tG>ip_655C^G9&+2A~dY){(t zNmf|)LK~Yd?6byo@U)#DUMlG@#C@hGh8$8!13bDWomSW?A!qGKVvFz3lH(HOZ|U-Y z{>hFwhT`V4NPr?tcqIULoBemMCTY?Ve*>zDu8?oSbLwg@Wa+GQI>b!JG}L6#GkxV2 ze&VEqd|--AKM6&E6XxAsm$KAmpGv{vweSq_Gq|BeD(cDFPyLIPhRNCij#U-l9g5-b zL&}4g6P+24&$l>nbE#{YLuO1mL-P`TV70(@ly23>r#8n~!Z}q}B70jE?d&)*5mX__ zswfSsN#56T9rvW6K^R$Ppk23CpLDBVRF775ujfXcOd6kU``8t+h-B9G2l=-sO-qHd zahw+z}vAgna?GgAZMI1quR*_v2$@0gYAybYP0J<2Q;s5gUM=0c}5_{Q?1bIq$_wa6_e$aTyyV>kdTVs&%CkB}I5K0>08gF|MZ&vnAcQ`MoeD zpHKQ3yGwIWO~p`UXq@iI9EGsqLnF%DQIxEw4XmdoPJK1kuCgh{u7r^sJ;y)z!R3c3 zcgug{z}x5H6hL~eM3$K{&bNcIP$2d*+ND`K^RPb0bK2U>88{vs7u2o`3;hG6NT zDYG6t5tjKgI}(Uu1zO7E6dOvcT|s6BuwfZye^}Xsn`0d&ZX3&^a)!IAygKZN;y>iN zmaw3HEUyV@LAfoBK0eyxes?#4*Xr2&v_o^Y6vVvv4ZKWwDh{plM|qBf#tb*9CJ+U2 zbFM?tX*8XaMYWSQL>LF4n2 zw>fPJkV|wA9XP+J>Ds=uG#bjL$Wy4@si6y^>X^6#C=|%^cx-fliOkErO{By=i-Vv4 z#ri5!k*n8Mb8p8hQ!*&uw7z6~L9{fA$Q1-c!vSm2_s9(e9c83m7{-Zlt>GSH-R#@| zZ|EriIAq&32FM)tBYAR#Pq)k3CMQW8nbmHxjHjoFarYVDm(uFk+Prx+rIz`zrIL%9 zJd`_+*nRBUbrX|G1LXX)sKV(fHgF!=c_sGLE4e--!>MDkrzXKf)kZU_hxYy95me_8c1lF{Cw)&=4Tllu-@`aE@q?hqYUIa_WV)E*_@ZL z9Q^a4kXb_kw@t2N>cg|p*OXa;@ZE)#AEP5?yYGQg#CP+zq0wl;w+DBgz1%TyQm(ui z-`gUX{B$CtI6aNt=-5hoJfF+@O?Z7AXUIDR8#fBl;%Znt30~@lmp7)cyCQW7H}zoj zlFKEnM}w6e-)-+hDuud`Bwh$Q>E|zWgq;F;jRaNr$2hE(Ye@}j3 z<=LXEq-{?A<-CJHk7~b%%6Je+%evmck*s*Qjq028(<7iIO?dxD7l0?}R9_okK;=fC zAs6!^p_GBM;crMpN0zBNW}VEM7|BtaQQK7>=iO?k zwNU-CI7w%kUU7p`O{#J$K-X}h(;~n{)5hTv^Kd%6lLXID$@Lsl__@X$LGGTJK~iO&Zgx9C?tXu|gqi9F5h5C8T* ze1dlJ=@P2>tt7?8oO3TEm(S#rYAe6J{nsX&3(T9@R8!X%Nhu%Kvb%N5bFaNZ#23qT z$GEabuI7gX^ew3#n1olIa%EdTjUHb;uxi>Y=buouojt73nR4S6ZbS@gln` zFSR1&u5;m;k5>@~`#adeqRZirU%F=L3a2q1-1hVn<=;Cfxe%>vLgO1ueJXz8HpILo zr-X(UB!$v0lSNGRn~lva5ldfiyDbg($^}U|j!=5pdQW|3xP>wIIRUsT|E?Ld-S3Kh%tg*fkQMt9!>^V{| z#$UdA*&i?@H@y(hhJ0?|A*S0A`ZCW;%VAnQTALn6UE#iUG*QwTVo_dCPex!@*<9BWD{ob}lkA=kP zWWJfAds*K@Cc&?+7p-l@nz^3es3YSjiAUFE)Kmib6b)_TgOuXk*R5Y*j0Qwz^Smae zhHaLWbJ9QOu~6{PR;8~K;Hp99t4`Ay%z2r`ZEXHJ7mJ|lQfO+JPPWdJs6QD|k>%`7 zQFvaF@2NiO!TA0pH$he+*E`6i%+mYW15sPUNOda+-%A%duj2j1Jw)Xv&srb5k9~?s z^5$$T9653m%Qm1mJ8c`G8{|TM(qQPWTu+(W&$6z7{fnZv6@ILlfHi|8AMqC-Fcb2) z``1zgxsh(EHSJxGKdt4HMBXDRpB0?uVP|7E$m*5wKpVR+NTG6 zUaYyr$M@Q1isz0+xJbRM&AmM#u6&u)%%=@Yo=7v&S+;cTa&2K^)~Phl{!WcrPl$;b5!;h7Q?%Mef3fs_ojenc_C7{6EZ!Y`q zbAJLb_fObqgi>+(2s&yj2#`}?%~{imHH&RcH<8HF|1|gCY|le>fSoE~oEvRIaK+%| z;Eyf!wOmh3*k0(?OKATW#E1mYb^a|SZ2|}a`o}pQc(Fzc7YwO=1U6N>f_{frHC|r; zyvY9qW^(!U2W7I4A_O8-)o_PDG`_cx>p3)ow6>j^A1+O^{szT4U@v}yWB^0gKf?gf z|8ge%pT8c6DE-d~aX?z;-;XdrUdKP>!<9bA8bOA%*iQE_{6MeaZW>%42+@b1xvX5% zEolHFJD?fx1!CV&;++l=S=aCe?7!u{36E#lNX)p!wqDl%NABB-YIdYsdtuP*Z_omk z_|z2R7HQbc_;dMEGDfo8lxHBtd+)O%!Kea6Q);RKtUN;ki7dK0#z`$fT=$2%oAg66?S4W6S_HDHYg`E&Pn#>RFfimF4Z+bx9PrxBiXpd z^KN~C?8vsq-OCn4XN^&vCUm;lk% zI@|OJYqjvf7D58m=Edr10p@5Go?0KWFYhv)#>wSKt?<0+B(lh)eepw+DsCr&x2Qn} zcV0qZOpQa0t@If)rm&*iPRt~uzrhGW<7nkSD-izRr-n@ERho7N^F&%kbjRFw*IzW> zAL8rJLY-O4_PAm|=PPwf-093!)4=FjuyHOJ%vCn$ zXHcOwDaE*j`D^W<|Mx%p-*dROo$DIM(CbLR6}@qHr_a5S=Uv^L-Ye>)x)JGVD$dC3 zgnrqgo6uF1KBqKok1SrW^APNjV|vghT^V@aIatiYT%D96 zWHr%8^SpoEl;S*{`>PVi@1G0(igk9)JFA>f5(9bAPlsd@aq?2#BcF zx`*_@H_JKxVoo%QGsdL}?X4+(!=Tg|9`ARxpp08^nTRn3)VbqP(Pt-tr+3tXBC@|_ z#aN@6Nkt}YL=pLhR=I{Qcf`*ISc*b(9IsfO<{gpOJh>Ydy0?oF7cLI!)Y3RuCLx)b z8Z6`c6s(c#_!Url-Xza@h=;@9MZoM5(R@D9MD|b@FEANA%q0_75Mn*u$6QjLJHCxC zrSS^i@1pL~U9NLBEXFtNn%RG7@iC&Z~H;Te-OrikS;cklf zyK84AsA(7*{K74Q`YSRAd}mc+{0*AwTL`+F^je-N(ijM!`@{4tyex=tUk`-czZ_`$ zlqA~6A{zMOvk1E)gNbSaf&Y!pmTJ6v*viBg3Owwe2Q4r#K6I z^|5AGREb*2=;+1#Tx8G46&8U!>(VS#y9>zq{-g5Y?@#UDKGgqTe&P4z{~0gzHwwF4 zrE^7Q$Y`BFb}2^G?GcatJK=OgKll{xJp2dvz!hkF=fN177O~z6 zE*aUZrlP)odusC}2`xvXEvA4oX;Cs3kv773ll{ZI4TBO68eCWv!{$aP5<`I4dtzTf zHa4qVY+JzcR$xsIFXjF4P0{9&%hx=q@KZoqhz;_p1WxaVX3*V)Ewl!(qzGehrzC!Z zzDgiQt~W2R?|XoV<10B>=K6=z&bO~iYN>|XrzHZNhWvmO&u#siLH3lQ%i#c9|*ker$Hq_|#gB`|Nsz6>FfJ z)kdJUo;T6y+wz#=?g4a?PZ5b-{kdEqUO|<&JfZ{88X*_=s z7k6ks|7OD8p5##%8oH2N7N}ez=IXgCksDE7Y`r5SY?d!RW5^yR^8?71JDc&38Pn1F zmM38xH*T%9`Ih{ocZ>Of%r{y6^%V@|T4eOHud`&kx!WTD+#uarBJ-9q)g(_DPO|>( zhgn6M)B(*oMV98@c=xnsZ!QCtZc~M8K-OEuIv+BH?~*ri+WaS(Ybik(Ja1pK1#~Hh z`1Fbc$UMs%pvIrW3a=|OpyWH})=w`UPe?6|ro_Um zT%joTuh=x_rx#Ax^a4OYCtd@Ot;}Lk5txXWs0Xoy52xW~iRIK@oh`JBx`WkjK z1~clP0Ate~97!o>An_R+L<+Ofk0BlkIoYkR4a`&@bAmuzVcY|s*HbKtPei80AOyW-DmXO zos(Q6m$?9z1iKN;Y0MwXKdq{zN9ozH${^C8$>I#;d{2Q08?o}sxl@R z&QI;_84uBP1l&@>^~Jn_J|ydk_0JH*^xI_kNmJvDDQ2rgExr>=k9laJ@s%T^@lVJ`7%HM|;WaWdx7 z;X8GXU7YXepP5AyvJXhHf0?IOsCi`_JyA7Kkx#2lOX^4{E=^~D2c6jAZXX%lO5^AZ zfxUe@FHt&iIqSWM>%74}_HOfhFEnMcFLAK0!m_yh+oZCZl7^ukpmfMRNhl&46bK(Q zs$J}!YJ6Oy5T!;5%`DdP_aQv3)F;09gd7-&a})7MeH|=k?%hK#h{r4pvdLKujVXGg zUGb24{P|d^hiQ+5ISdLU_ih5Cr{&KR0ByWaxd(X2qYs!DJJ(AgS5DI!7~l#QTGs&W z=A7`2ZRX2`Y)x;LI+j1hYO4paVO>h3&+nYb1fiWAEr*G)Z7fN`-Uz4UVlo31IL=^9lcm zJf{-_!j*0V_M`F<>rHOD+QFv}9I5-e0r-%l?bGUUjnODj#zgdix6_;E7wzCn5JmZrM} zg#nYL^XqRV123YDweL@+{08xiYzEDNN%))ElUw#`+CLwkw6tW2kQb{dUwq8~OlHh*@^+YK>BC4M}H3Zg1eP2lJe^`(}K!F_Mc zyX$cO@`By0_;@)=vl}_8_=4KY$}+ob$4|zm_QdXc+o=xC%)N#08!u3<&0Kd8%Vy3c z4^YDE`ps&1Z|&HSzMvTWwd207Cx;j;T4C&RbwDIN?I77fJIm+^u6br^MJ4 z_K@B*x@2dqPclf~Qj0Orj+tJia$gS2ao{Ll!yU?t=jIuD<$|;1v*yz}clf5N>8g^9 zz~Uo|MY*g>q8^jxH+Sw|y%wV=#RhdNp6(e>k&sb#4ZEOJWZ&#F{{?#ndkVU3c!V(B_qEHo&@k%aM+bfAHs3$$y|i{~gD4wOuEM!F z4n%P)`1SaNiiG$bZ3Dj6Li=G&+`;hjC(9g^F)5brES%z6YOL?B+q}x~iwoo~6VZ6y zktZvs;@cNSvzRVGrSN*|yU*(>@afZ*Qvec8&;Jc-y@J2CgQX7 zL@rJaKG&W^s5FKad}@q9NL)eS0wvB-f(3C9IMqXaZ6*wvPI6yLwy{m6;mb)K?P=`^-KhyR z0|Nu`NgWp#EhP;t9Uv4q!80^ugJn^{(O@CuPa)!Um=g2EdPWOxaP*@HUJ7jxh#W)& z0(}9!y3tkodM9$d_SCisnQ-5uf$5ctJ?s0QT`@hH$cD|q*G@I&HrCkHn3_fyqV0`g zP2@nHwSNs&p^^Xbqpe^5a|v$BN!@k&U%yW&3S5JjeLZ%GH%ZzQ8pZI@sqZ}wO&N8< z>#TN)s7dLS81)K|87w7Vddj;so`j&(E6c`*c)}Bb}1-PJ&-*BM@cz zW;o?z!2$P;Z{dndtpzCN#o+jR0&@se3y+6)=;31BPI{896@P1`^G_`u%bnabm3rP^ zJCI;|uJLD~!&JSRvJ!feO3vC^<#A-!G)ge;u~|+^aUaksoFOB~duwP8&URA2#d|<) z`ox@>A)m#S+C0qjGE4@w9P^$8`kbpXNTogC>oQ|`za`^0Gl!<&nr9_fWff2s`lr!P zVj8*PO5bD@6ue5=DvA$_dG^*!Klb3Y+~@~(70|h*xTnO*=`zWiqENVV5SG^aJTsV{R!5prCre8QHQF&HN*b8C_@|u)>{M4E z@2I*Iac<#9;+KX7hDv!cdY`!uxp@ls=Nk$jRU}g4L03)ZW4`(uCZA&)l8|=(`2OS% z#ZBc;J;0xvPg(tPif-cs`-qLU51tS>(&dj~U@g}V#K?NjqwIz(>dD=c-YC)EPD7fR zkn3OO9V=E38)=Y~FOWggCt$=O2g~SK!{C9iJMNX5M^GDyt># z*vyPKQ9)jFXxmv$U17#9;a-WLz&+iS*?>EsSkqF!Z6Llq+sOB3DtTrtqrw=)RAopf zC;Mbog|I|xb#w8MZhEQRys<;X!b#tEqyNBA-__on0ICs|dQXG8US5_frlL^qc&)ft z)8pig;(G8{S&rhm%=d{ZuQ?~VG?784aRZM!tE@lVj$!bT6$dbn5SC-h&MJYE^mFxMkiNd2KTn)l4O zo|}O=-rBqy2qP82zkKvqeo<&mA#xnb`pb z=8}Z032k-Cta1+35?oX%oS**~OiZA9wjnRfa6;m`8q-nApa-h#JAX6voil3uZHcu$ z)!RWGa`W(ov@Wy zn-6uL2$YS#*RkAKt>%Jj^eXMk&}cnQH)?e|NXIlgi4A($)Aj<8Wnjb02&ea3d%KPv z-udtR7&2-a%9H0m={(1q(a<5sSI2W;j(NFhp!CL`c|D^R*#)bfEnAyw_!UFQNUbTL zUXnX{BjTB)MU1MNY@?jF5DIc)`mCfyp}@`cqUS4{ze#uW8WKBh81ERPNxaljnB9{c z7f^9e*TlCh%S+F?VQK01Y`4DWGRdqJBvU%$%>_XWO5&q;UsI1A!${BqbKUt-j$Rb* z`ioH%eI(J+_lvD={m#`}^`g54m0FR@?~%Iq91~0op~LD-Iko!Mzd@XxMaCpL;UpaP zv$pxZ!!n(lSReVpix}kBf}^nM8}P8X2B0Pre%Y5e^AqBZ5 z#IEK>h3keSEIeWGMd;(bFycbm`m`vg@6wEm>*?MOu;jTWbT;W?H38JGHk7%kMhaW0^qx@!uRDf z+qV5yHD1JtozX&z@!Y_Z@2k1%(yL{VJ8VRS?9zKK(2aC-_fe*>E3qqy%h&KZpNpy| zJDFaH*&EDM!~H?=2?>cl2zORa-3$Y@z=kMAgwiv0wNOvTu{Zr~1v|G4p475-iw9#F zPR{8S5OhwBi(u;mpQZ)M)$et4CO;*K!exF|^#{s8NFM^b5tsFR1%A#aQRV2Jb)4xN zN%|C;l6f{a7e_n;TgC}ch~v`|wfxS}3#f0*i({P_S}5SIT{E=>)}qR5Ibe)8A-Jvu zS`+GjgNm8~;IH~j%9k8)7F&7)VxyjvZ5ZY%snU|>Ejq3oIA@i?kB=wv4|p(Qhzo4| zbSE;L2P#|rrMZdO6HZHi>&j6wckkf!UR?WMcG*8N>eE7$9&k95Jpr_B_m|!S^H10J z>mXs03rRSt=r`ziZQ9@&7^~f$iFGfB>~{MYRGyFlLd3aw@Z|^LCG5GE*US>?z6NkmtWpCGR$I*;I$GT}Vxz?Xa)l z-72=p_OP~P%h(YoQzH5h{fk`abkO$cyo}X%pjV+S5=XpOlnR#0y)a~?(%awK%hJ#+ z6Hz$2PU@YkNEG(8443G`)D?tK^7XsQ6bYuFeMAFWHDF@w`Lv%N6K_^f`1ase4w<&* z0)jbCP|D|rX!e8$gOIH0BY{)nf?;x5L-4~2<8G<@A-*G^mCNs}eaxio0 z`P--~-xHDiFuf8j-9;jEOT;3^9=>z~7yw~?we)@_f2=m;RGn#7ji>Y|O2{!Omg{^w zyaFz{`ojXa=@Ux`$nNa03)-S_Awecg zi$zj2niu8`nicIlF}(FO!e94opp*{JD{kN1U!Rj2=E?`2#7y95$Jj`%5~%Z3bu2qB zJ+e0npXU31oZqccVilgpT*@bSrwh+vHw3;C-2p$wq9wvdFlalgYBU28|<~g&3g>U1ObI22fWcAH@lAd(7p+kPqOc>ql z7pQdNbI)yE8f7#t4x9>O44JZ4oQr-AhBGlkb(NK@v z#fazYm|c?quN3X(j|{J>T0=z%40*pIUL)UcTY~#(woskdM@SwVG}3hpoa*4wj4ZsW zYy}iR02FaIVChKV`p>1f|8JLK7XgXQ{05*Dm+yV$?w)>GuFBj2W9|`GqG_C7Uw{{g zb1_@kXBPz2hs!_Fnz7O`v-LumV?H4$C)?IJZ&(S)_U8*ycbm#f1^vCl!zOg)0m(t; z?tCQcn2%mag-}EW&-ivhh2YU_-HP6qox(`gJvz~q zf2%tBi?I36#GFg1!s!vp0B!5M-x)%q#ZpRFhyTFLwWuI*TxhF*v}QlOQ76k^as(`p z%$8%PxLFf@>F$2x)DBzx02a35m5G25q66W4J5x58U(c9!_A8zppu`S1@aq)_C$Mz^ z7_kpj;tGLl0WKBbs<#HPfAVFhQPFWEWW`7IFPG-0DcDHgci&a7E=qhPC+<+PkfQW8 z%;@5#f45hq%eE#7QOip7Slk3>!{Z3|748n-^^3qfds}i^E?L^MyN)yt*0vQ5zJxk zwXKxqs2ER8FQ+`8e`H(#!{iuwR^JhH>gxf}+C~y@)WvZ%tBLv4R|Rhn-c~!f=i0>j zv*^Y-`g%H36(r6X5Zjw3;(@}%ldo+}e5o0V{n`C+?w~m4i8&wHrpO|tOF^(0f7NZ1 z94~muqzhvb9^V;cw-R-wp%87jfUlYly8Xs4bO#ipg8w4Lb1Ekpm!RyVEKr}X`jNrM z^K47<&jpVQlVkfyqkD_uN_Lv)Q=v;uT;g;4r|0K|nR)~|eZhD0Ki);C$rc927HaN3 zQ!VNk%-_ecssgL@KzTFaA#5uKIOLOxoCZ})j5}S#@OczQ1)!cqf=LDi?)@4vP zyr{18>xjkn9f@>~VpQ9%szBw6%aFm$BP?{JVnkpYWS&10BD&uQ;??kh?7GnS>MeCX zFnwHoW7`+bGdR|ns2{@qrFd+xphpq!X=F(aJaRPBYd4;ugjWG2`6EsI>)O*!?k-sh zQDeojF?kpjbn?5*>~{|Ho$-UX?7|mqQiJ79N2qD$_l02gUB=`d(G`K{uj_e{rkR|+ z9?#G5En}z_Szq5T)5U8gr4xL@0_*Si!rJtQIh4?q*Wbg@ji(T_A-jP$`l(liy~VG` z(KaQNpDHfNc%bKXNWf6R^U|gqV7|*-NI&Po;Yu`kWak0puQ0s z)un{1VynKqw&71qMD3@{nc`fU&*gz&#ruVaSgWN!Qd5i1}oFriNGYv-;;F>ikHfZJ5?fyQmCw&VpD6vhf?F z14&e7j7H-GTj@3-a&ZWk-D`FQB!=Y?S%zmbh#%2^F@XQ_f&Tx&sP!-381(nl|1Fjd zvLe%oRHp;PE5U9|z`j!c7P!%AkohL~82IQsEz?B}r7Nt4+J#=#wQB_F!-od;M$nS3 ziP*y(%qL(#oc{>Go}DXn76qu^AZDWxR5lOB$i5?X9i{RMyCPl<*?O2%D_+U;-UFMt zciNxgxehTlcxdO)1(HV7_`M^!gk}QEsI?{fRH$HK4k~K9S0x}&CulTSf~4P@U$UXV zf3s>67Pt`8;yy8B852pTu>J_A+t#+WR=|2#Bgej8T17Dr(+pn5tq8=-9#$Mm)azI1 z1JmR%xywmJbbp?A!1uGWa*^B4GLkeJmM0KyU+%G2uZoHW`%fMeWPD)L$V+}(xG(Oa zFb~R_F(b5eV|u}!U`Bp_Ki7`PjSA=Z_&!!#G*0463>E6Wr%52!nl~Q?pXkBPO0joaI4*!WxX=Xl8zguD z*`H=za3z)8-=L2!F{xYVXc%PkW9PYD%as}%5uV*Eo*ggtoj2@v6u&A{fPj)x z4OH9sG{aSo0Q>gQFm*q9b&a@Vmeoe6TrCIF=ciQ=8BT>xm#P<0gZ`o@;6dS4EuNEL zJr!OD8@(G~_copU21~5`vTDr~LCq;7?6S({<|9ezPZ&)p>d6O1P8_q(Fv+PK&W6dg z8Rb`O3EaE=#OA{`;myo=>uF6)=@O4eS}bZ*lzija51 z)pB8iMDUgGfFAmhGvRgLT$gbD1(~80pPK!S;Y4+;G)_`QqWOZfl=t@Cec$!P=E?u1 zBMCpeAB+WVGjP>c!e=_K`-c&396-sd34E;aQuHQft#fA@0BVQ5;8#te3yJfo)1rUr zfJzHdfp8vd2P%K+p@DTZRU3~NB27*kW81kFZq313Rw%1=cVzxtBc4K}Wg1Gni<=_s zq=Db*!t!`gDA-NpE+}5X79NF;JXsY@kSMuOa6z7WsAvkLS`6WL)5_f?s*dbpBtnks zQyVrh$#HhDvV0g=Kk`Un53@JF6II^5;V|itRC0{sbWzrr*3fxAUl#o$lBW2IRH3OH z@bBBG*$qvSY`l9@1CZ|=8+j0e zT;&(Owd@exyp&EBrRMSD=*$a37k3{tcea0n(({Yu@1NVIunfyEe4qW#9f`1m?6b;XV9^Gwb8it(I+Cxt# z0bWQCn(a0l5$q}F%4)oCIy=+6*s-5p9<=Di@Ay+$I%N(upi`1l>MvL2EUrZKGgX{| zS5$@QgGI>CNqs>9K4baU$+8>{1i8eeuJa8N-?+xDk`V_CqS2$H0azRooIHxx>e6@- zbx^GN(w$u_g|~mjjC-@~Qwj67BB;B|8?)-LdIi|9?!;uP6N|=~=a7z+z)9zB3}s=_ zs{$VmN|@2>IUXY0q&aq9(@fydj8Bqj!iM}jFK}@`KJk4TUVEb?#XuPGD;*1s^_t4} zw73KsN7GrKS1S8o0_X0fXbF$`OKbML-j$l@HQ^5W_~v;pTt0R!`!crR8jkO;02@>u znNVCw=q-|MWTsuaPscI0o}a+$-NIntTc{5|FPe-qZAZil-3G_~=;bEwfX|+d-lv%s zpg`DIYnYxkaQTQ?mM&t~d?PX1_l=oR!C)5O1(7mF2`h0iqJ#i${ zd`B6bEVuOcz4+R%Ty=+bqC4)tN^!fNrjYu6W1A|<1Pl$=PwoQltZuRB-=M{*+F!VT z-{(?+zUu$Yjjnf=kmNm84F@Hj>DP!T!d3?qcI0YtGozVgD*HF+C~K(K5FYO%>zCqT zL=bj~3K=pl4wfXWnfcK-?`)8~2Fvzf=u_wTa7Ng873r`gTuXB~dUVfUa%>oQkZL;* zDq%P$Dwx?1tL>*uU;aKVm zFL39NRt_m1RP26~8&RhvVGwtTW^bWV(F zbZ>a2GH5tNP7>@Nqz~O8pP)>hRC!hil?BOy24p!v(s-oO5Ag0CyxNPW<@NlNV7_NumG8?PQGgm2EQC{h(gf7f zq0^jRZKC;vFJG#hDe}cw9>G)I%SgGwRlQ!s(+QbRY!RU22gD?aTmg=3rob`NrAC&bL(y=GfgIFb*K z{bBK(gU%H0U=e(xtEXMFm*SxWk&~(}VH^}{h%%6Q)M&yE$sD?ho#ypOOm3{OTgYE* zoSC)GV~zaw*_e?BUv8P)*F{i*NOIy#dV)5ChbFa8@Aaw3E^P-ZtFpvhBJJ4X+vCIkEN_k4XaLZ-!JS)4L<5>ijchXsxCZyOyWK2lh+7$|;!6iO=n zorx)@|L1QIA(j&jTlC7U@-!+uq5-=dY`@7Jc*Dlw)Xc`_P|x3|rxP@HHq+9Q3n|?IdonGKlr>QQF|5RFbdWLr#Tl;Y3V|JbU{2z0nwdc?KviyNXhl)I^P1FJ zEgHP?K6fJA$(oQlQEYrY$$}icRK~?yD;vqxdXxTo3j*Hn; z)KJmo9yuRy1l0BlvS*Fib!&>7OpAdJBk$sFOzSn!CG;f<+1h=!GvPJtt zHwZU~OeOQas)^j-KcWoSWVN-t2(r}FhRC~cHqBgck=tp#nA>yFqb%xrmt@PRQ4nt) zzFTuho}=^e>+Mg1fXuh3jqmnvZh2G5 z|B@tbu$|pa#ZGH zj`8jn+*tyGQ1ddbetGfNx@N%4kauUgV04+yPxAUUkq5qkv=RIa%B-T6g=sXRykv6h zq{mHw&csERc#zzw*9_R|UIj#Xnlv031&;X8Z`2PVFG$ms82NNHB?vJe-!o?U-=eb` zM-Tem??1}zguX*uR95WTVWcJZcIOM^Va;U$baaXZ!*=5Frtv3}W`!2AEEQ+tngd(? zOrJixwO0bY;~$4$@sJgQO~@AolgCJ}`PT8!x?Wab0*3^xUO1*F%?wjf3_Z zTX~5f1LcxC@`1H;=ErrvqThqbEC`b^wRjFWKpTZ!vXREvrfADb#mSM3SIHr{)HZ1`f zp7m1fPx#=pl;r->83Mk`4Zqy?A?5i5K^j2G;Ky%TXaMbg0mqE|3@`z4z)_`Ow7U!2 zHGpfWUi$!|ZJ$F-uJZKgc4`c~QXE8Z4U!m=aE$#cd+HZ{u*SI=X#K8I)nD;lTWTkj z*nyz+NG$6Q;LNcU6n$~m9((|3<4!dI)}gPW#u0)pHiDp!4dIi)SG8*EEC6fJ*aSJo zfpubbkr%ZcngAS~3Lh2%&}bg@twk?v`Vi!Fjs#-9nF;*1hX8r7jZy&u znm)Vy1|29~kinoQI+>Vfdqr4aSsJgKfo~;1PR^A5pUFotQB6ia{FZ+EXxDfA*TUSim~4H^+%vVN2Gxr$tv5#oFB~aSZdKCB zQMf!4%__}Lw#YBfj~SB|ca(jkCy%4on88t`yNX+^uMIx;h`{rpZ;tMG*P6&}Kfa&Z zCQp<4_Q|e~0uISdU2==pftplggxk-J9My!@c_d5er5jhL(UCA`ohjg+{@}+hikE!)*K)8kj7vD{Cb=9%(lbixG3r_OK6=<@X~=%{*R$VOnl6E zXnerthY}?7s(M-%X_7PeYSym7q~2NDmby1!wI+C9`!zkZu8^EE%$F(y$x5WBY^Ul#V2j#Ex0%yDqt z#+~VeugIO5cjDXORxs6mjPadakiJR#bjx222F5p>%oJBVs@i+{@f<}S!5gs|fS2U& zk@){o`aYY9y63Hj-ErAOptM@5JH!tysng@9HVZhEm8y=CN!=dXq*XON6wkY(TJYs# z)}cC=#mO*#<)Je1LC5x&hq!;a(s15gh8f*H;|%q=3^Q0%&`JaK{-1{^Shfl@`b@yh$-u zN3L67L!lqW+o$}}^|Z%a$oP`o+q*ojG_)b3Kv(e-XJA>4&-h?bEk$^|&I=#yJ99Dd z2#c8Qm&3DcYQ`SEi$LyGkGbVwczw@3{_k}9qt>D&g7*z$$o=p|B2@#_MVcBXPUH}I z#&BrmWu$Ej!Ci{IMms|fp^duvsJtt80yJ=@MS`t?Sv2rHn6+Tu}u zr?6-9`d2e7BJRt)S3mFxImYj{DJ5F`-;KL&PHCsm#M6`|2q3==+ALR_~X}L%2ezGc$ky z_p(UUL@`loxgvZicrcCvu{ML^*cWYTYa8_Qvq|=NrimY6g7QP01`x%jW{@AJ)Z=bL zwAXv*kRB51fG_CI$$(f@Iv{r}^{-G1_deC*ue^n!ti>PuH8rw5sB@CE1O#szP%l3Z z8=H{{my~xERlk{rBVR!Peq8~9k<6%uaj{s2e|&ML+~@;Kq7o6W?d+|0Ahx1v0d?wI z&8#0BRs5^P2f8_V{{|k6q9s7M?>{VmI=g`5-iL-;;Y2kok0BfsCdvxbCqwtBK7qCRs`5C=bX=DSAywzGMrmXwc8!#y8|$HR zP|g}seq=@?z`+JJUEcT&lGp^G&Yza8kiC&+o~w~MI1Ril^VDSodP$2J1N@tc@ckT~ zt2#_Cc-k&L!2GK8-u4)akLP6S zdXO&?JZOKuu+Y=~`t%_EA|b!92Qeg4UYh-Nb!YA6oB3HI*90x*f+vjntLnMN-W4Yx3GZqUy`LVcQ*>=>)BPnY)kY1nIW4=40=F;n!ygd9<=Ph5k0{8VooSj>C zyfsTkCQU?9uxcff_D~enE3!MyPUZ;@Z?3otM2V3fkE}{2EsLP8Tt3rdz4{yN8#Emu z>;sR(lYDf(QRm_bl6KG2+T0#tjL8F)=u;N-yEyavt*3Vfl74E}s(w;k=Y|)> z4a-|Z(t1&!2M3iicK&Cy-ts@tdbSIPKWM$(-_ZIRI5+H?JM%N-GO3{wyPo;qc~fp- zbFp&ptP8I5!H9anKRy&bdmx|7_O!6@>21l(Jec9&U5P&Q3U;Fl8wEThW$V`^@PXPL>_MR5!bac&87OWjxYY#0>+61(aNop%?G# zs)GC9FME=7`%1x!JkNMH?#gYNy3K3BewC+pkuMymQe9 zd)s=^+{)j4A;6)Wv>(2bvT)x#H`#`vYfNwQe(0C+iL&=7v#dN)4LJ_^vK_M8@aLHh zTnmE>^X-Gjj%qX85Qc~r_#h2_C5PjACXYe7#_aRx!2CN=Vrii%5A0Os6!DArR4esR zAn*6X8(t4;eAPCscG-jg?R>n6Wldqq1AV8~+V|RH>uy|fM1CfLLV;9SvEeMGegxX@ zK&5f-(uOXE#I&uU$pWlMlB4r*u&1P5!)Ke?^;TD6@v%E0eyh=0mSJGEd%vnq^L)cZ zXXE^D0q9mWfj{AuzeJ?xH{T98kn)Bd%-s;iKIWPRPW?>tO6~A|UHI8nMea4AYf>Gz z{0+(g>>q=*PJV9<#EAOwr5FxzG*_Qf7HzVqv-@F5-W>Xt36?Hq86zfSFU z{KXag(MkO=9$HnS7d*dxF2#CXm`Jk60G?h{Cd}22h*TQ6J!v$Ki|@KwY^kI3CiQ7E zTl&=1XR>E*Q{U+K@5bFquPeV^vxUd2Sp{CmaK22eWWNJ z`Ae(SzI?UtC<;ymj&;pk_k>(PZB4f3d^Y>r$}Qr&XXo8_AwprdQ4V|Cs6Lo;nzP$q?Yv6d_{JCEKbf*5!2fKU4k%b2{RSo5 z9s|cWl8i5a_4_8|mv9=qDgeU|_j5^rx`0t!Fz~4i_^JtG)4Y#AW#@9{etBVWdHvJD z&wfE{@}%}@L9S1@jE?T~qdSercD)Cn4+n4S02c<}xl!y~rgKzCfp5H>hi{bt99wx^=ff0Y7xHVUuNgaEbBf|Fg_2w%gHoU? zzJFtw={~8~?bCNS%6J*c;k~{upTN^a`(Z7}bCyK0`Te23uJ#7=f~Kc31K$--8K~98 zdn;d@7qK?&l_B?lqwWT@b5{(!4DQ_@Hovd;ab|K3N1KN^oC0EGZ-@vA(L8~dWcQ6L zmJrABYS`*4(-8IA<=m81F2t*Rn9nn%UlIEVi}_ zR^PW?V17>ZYnoG=shYVqR)&Wr3!cf-YyT~gF7RNuxV42tSd$?%SAhHw;~!51ECUJO zWbbN%x=SM;KkB36WX|X>|0I+)Z7xW;@ls)W6*|k0?v$)S zF+N>%E?g?XRSSH?_QLywa3AA#x8ol`RA4By|(r3 zwa_|T?i(3~Dw}xEoe72e8sd6~nMVmE%n(RM!qqYFrg2xx5PixWjK0VJc_eiTzUhn% z*h@782tm)95gGm|VNpZUjsYp7jJ7{Og&@yzy3C70P^@5$kyWcC^?wYaQ%_-{%GFs* z?fA#2z|}Ap6m&k-i~-*yGTkI->HOk}!m~(OO+jKHfi13?8_(O#P&eM^f$JMgr5dHJ zCs;RBLlYE=Pw@_PfNM+jabhea8GqJ(bud?r2rkhj&VSY-p3UlQv#B5GMZ0 zEJyUy!e|9IJ=%QU=xaCWm#Bnuj8Ltic*hvw0LO+Bppo9r!2TI?tbhS+P`@@R>o+K= zq!dvN`_HW7HWC1!{BN+1Gh7MYHQh9$4;*v8! z6X+7zKO#rEX9&(#00R8@2*5j!wCh{bPLq)F3uv2qq?dQ7=-JBop$TyG&%$;mQI7IX zS2N%Y7uh%Hy)6{72e#iNst)^Qx&%YSigc&WAQP;B!{2)sQUg0b;5WM2fFoO}oi<*a zXqzCr_uwfaBCVaHlbR0bj3N8dIcg3!UHq6 zFezxDSzsfnnYJ{(6nwH*BjBWR8A7${5%r^ANeYAnzu$7Sh50(KK!x@-iV{&28_j|3Y;ox0VRsS#jMNa9r1D-rD3&*!@ zOW0hV%YEhdK;{70seL=C50Q6p&6xwZ_s8InjIEtrE6B8MC?&+LoB>Ia{Xw6#q>bf0gB?Z?cxRV%W6T9jf_ zGF#w#gH@sRP$gx?7(LVwhPQm_`7keHy;}>##|D`WVu02jAG3d;r^m&1b&7$KfNy z-mw+M<%IGZ5UvHjT+cS3ONH0J`S~~Wli-}Q{opZIsS4g=M&@n0gAehY?>GEwb5|-! z29I2z!m(Zg!WsPlO%q6EK(nGDaG)*w!y^BenZMKsAUh-tth5g7XM9YBGJE?9%w9qr zaCKo?YDH;DCI_~|*hH&&v2SXnxNz&!gTyW2v*f8xyZ!Yto*s~eYuWvNgsg7bmC-s7 zlb-I|rr59VqX+LK*pm~T#$;k(P|Ob8(;G1^t3v$CN9PNmK53{f;jObtn}M7s5<))8 z%R{{gYZV_DY|T81*Cp@SFzo9BxVHK1{GzeOV_|fOdb-&<1z8UCytXzM62!8TUlVFW z#?`f&`*VOiro}sb7e%YKa4Za)YLQ7o#8%^uJmuvx4%fc|Iw8HM_vB=@q_;x z1mM0d$k?NY^*c&MjBtjwD{3yma@tDDIGlidtv*MC@2V8Hz2fljywXH;IZfcMF>W!t zc59P@&UIt{tnkP>n>YTM2aoP)DCg1{>7m*@Pluh7qmS7q-=%@_fm(pA`@mi#rtWsa z{6MsIZg%@bNZb5iQD$agc6(p?DZ_n<&$kpy-|npRQt-$v=ern`b!WIf;ASM^;0|X4 z;mitdglsh$Iz9P>DEoQ4)fkf#)2cgLiMXST1A5LqUcHb;_ZQIgFVN1$`PQj8kcaDe z3;z@9tH1guI^000i+adfHj0~%T{(()ROw)qC${Dul0oG4wC=D_V)-?b4Xw(X8|C@j`1oNS@Am3$=Lo9E^46iFwd_9NtpM1yU?vMg z08G4a#@N43y#SsmHUqLX9{9nf2grGq8Wi@#50oYGgbCA$)tDM924r6#4|y z>6VLZW%YXl+1j3q-?iITDTZh~5DvtbMfBEyVbJ*b$_h@EnHNnGSxa0WfAik2ZoQo!u3X}A7(k{IU zqGDq>Ac4lY9O3JD5IG-oySZBFaGde;%r9OQBKFLCceVkceD|sI(40P}#nG=Cbn^pHz zv~otlodneMSuG&U)5uc&El{y-Y9k82kCUsgN+Z(sO5`ac4May@&ZpWMGzaXst5#gI^z(=)1mLmzs` zZdXK>mu z_5&2hFF;#xgj&l-e_sy!4cgC`Te)xo>IX0&_pAYa=uIDC!0|N?y%mDZl^&t0&u5a* zszyW5`;t#3>eu|0t?5y!zl87#>hCz8C!ty_ao%c7wu8_g;rw7VjAEC=)2zHlH$33= zC;6FIYx6!~*s6!S_t7j|(Xduw-0U+E5`)4P#K^C9YYpv#mXNcZw&{8Get4hS)Y?|3 z;{(N!$9?Cod}$oP*$fOHi);p>H3Mja_uF1MUf%3}$clOE`w;;Y$!C(y2hE9IYZf|3 z#GIWy&V_Qu+sm4=l6$I}#&)XH14e_PHzVdkKAu0D7&h#MV3f^Q!jlvRALg(1*nDZ& zT@G2-kJW#A)UiBLlGZoeFwHLRW#k=K?8j1<(xnbb72v5I${I%prs8&iG9+GN`hNWG zEIv(#sJ-%yNV?qdN{UtJce?kCuR6_%%8cokt*yCulJ?|oW?kJ^ER*BCqE!hzqHm{o z0y(i&aedgcCuV(>TeH0-s(bPoTfUO8=bYCAI)$@oh9tu2+L{L0m_;vk^k?z#9^MIU z=uLsFm#pk)BdOnM9|F%qxoHJ$w+Nf+8Z$<}S-CU-lfh279iXUw(J}k>Q_W!1%WaFp zEtHVb9@+~Km@ylPfn8SKH0-HE{q&T^a`{+U8^sW_Vl;1)I@-1Y8VPQqp8R>vNhmDL`9f~BORel0g zbyWZY5n*6sV=hkjE-KtCEGkTS9iEaBMq{(foIDf<6&@%4aoAn`f>@q04(32CU%1^* z#2`*(%vJ!%+d&i{jBf;VQ(e9wwCcjzM(tg!+WN!G1q4GDTTxhSzU|gqp=OvPk^J-5 zL(*gdj2T5)Pfu-K$?U*9jNx6$8Z(etp-Vo{_D^=RlW%E@CnnV!?Es?y< zWOGhTJ?q&hA{rL6g09#OJ=M)Nuo=O*`1ES4#gxyZTz9g$J>{we9aLduU7o10BM#B0 z$i#bDlI!VNSzh;Jy$f?JQ_Au@yet!z|`&BcY7v zj;>Bmx`#!E4hokV;?_YwNu@ChFf_$LARGx|&yB~_@9m3xopL1zzh)>lN%xysJOI+v zMB(prXGYd;A~D+q+thK@m+-dzuRbX9c zOu_BYrK))yk;-u0JJ>Vi4_jQmCJL@wHBy*nlaz{s-usI8YFV`D_?q>MN=;}^`v&gx znMgmdhnlQK$Zd#^f6Q!wpKIQMgAl9>;tri4$O=%7XsG3pD2}%-{Aav*;dKH4a`J`P z?0^~M^~LQRs)F|fFGQCEbFXq#&PcQEi;HN6p?~@&SEKke1jsx3D>iJpapgFR;3lyh z7=n*)_Q9x8GgPowFRJ(7!kNu~gEQT)&!3%~Wj$Uwll30wdB+w}Hu%+PGH>6$b3G8< zyEpE9gw)<&ZFjA#*)4U7JOr#R zdL9vn$PrGOv+ivzx^zac7r!EDxJ~BmrYy+~-`2Kx=&EpMUI*MgfOAlOjj`*4s9R8g zG>#5Z6m>bhJ?O>NkbU#OL-5n6WkMln$LR3IUY3XxH8T06_8oYzJ%^@@f#8G#Zewnv zCf3iO+CQKCnJziuTSZ*=F!(l?tt*6PgG(RVeoT8r8|l}yxw4F_vvFnQLX@(tZvnBc zdmnS76*@ZU1_fNf|6<~Tjjp@qXke#qCAU5g{|;T|kNkrvTzd&*)52{F4Hg1$OpkMP z=QP0ak$~kH9YJzbBJMF!}w->pH8#;nU=>Lc-cBQ1;wOc8<^3|+GdD?ExypXIv`+Si~7Z%Vblj5FN z8ip}J7{aH6dN1%$Jw%IVRv-kr3L#kcgwOgLSU?#3Hm{VbC@v4*)eDSe;$09CR*$M^ zjuphF_;adOzB28Oo1*m4J{`HK9s~PnH#afA+u}z(NdJ`pE&g1o(XfDl;NuiCv<7Z0*{<{j*p-wj6n&-Cm{^IXiT<*v}h(F-qvZ_6d zJBSNoVEigFxGsBx0?hoQP`#IH=2rw3K0=ck&C(F4v+{LDc#v4Jl>VBeia3+?&*w`| z)_Yv>7uvCX5U%!m+5iAl-wrQJtL882H|_ zc2m2_mKMNPg`Y%)WS62997t@PW4oRFf*ZEPNs2Wlh2U65pK`2{Lqb2vhks_vCX>)< zz?7O4S>L*Qf9>o+{$&uPfBKQ%Ude{imQ_377~=~W9@ZiOnU}xtne^g^3T*PQB-=`2TF$e6a7&*`A z?oA_C2Ejs-4QOm%<36)>dD{HFk8jm@UdOSN&CjK@R>ys6@x`p=bt!%}CC9cHe5g0O zC^~^*w3@%q2#Aq4u3EIJ=6%vyrCGiT+__e-lh!4_EXE}G+}A5xKUc1r|92^zI_yU; zs{$Nt|tSBilvVsQI=JUvyaZP5d>#K7&USP$-o)? z7zdfA4Drx0G;-rul3@tnfcH@tKsvih8AW=i-#L{o!D9We&y5DRN{aF|i8eg6ZM0QS zSe{`z&F`youX+(r44U3IEz^U*C$gu?dG}`q87?4S2 zQki!&h~-7=7^wx$=g4=^0?bHG08lCM2iQ+H6UbtKqxkiP0pKVeHNdV`*ZsOuLlGGe z=fTRBr|UmKi! zb(Icmr(Fq_`fpgF3Iv0HgWQE8W<%rrh2Vf}?=TbA3P*%i z0-psm22l$a7LZ}cjqjSj$%XUs!lNfp=;@=96HQn9+lE9I#uXudo~hbauf*i=P@Y#_ zl{^CucNBOqV>Ffh(vB7+4plrW38uy2I|Qqc^>c{LF8ES?Nxl)w8@1B-N=S1JgXPt+ zQ9zm>3pWdx806B|YQt$HZV+Bp`hA0?=%i8D!9eM*EVYpmAGO;ZH>zQ%wzR&yN-?9l zG<}VWETA&_R#E9Yp11GId7Jqe(V_2uVzx4Kl2=TITY)buIx43wi`?O?1PH~2kv=#GF_f-@0;qD zJuHXDS73|_aHqf7;G|ifHXizTdBTq*7MN7IdT-VEoaj}Lxq-)vi?iw)CcA114(1Qf zMmuL*o~*>cZsgjpx)0#yRKP66RMd*8#<$2cMq(#7h@~ zT7vBwWznypz-A7P^b^GK+iBlf0UqRd=QD&%^WIw@qc|T>(&p7qZ%L3C|1H+}75zmZ zwKM36cff{tgCh{D8tniRSfNhuU#5 zVMC(hZ$e-QMoakGGn@+tw>f`VzVm|P9|E?Q_?#eChCVHI=8*>4{+v};~wxYjR?Wcv6eBJ zLA5!e*W0HUyrk?O?-ph!@JS6~Jal{eCpP$vt%cX_#_PS5K$gH=31Q{Dr>~ti^4Pcv zK7ON>vdd8Ap8!fqnc7ZXmbXzukamimV1780JgH_u_vE;&HtFFuQzGlCpCCnqt%D)N z?JzYd*!is(BVorq+Bbx~x8UBb?N@)6@BqfQ^e-6ae_WUzH|q2GRe$AUc?3>;QvM{L zy-mrmV|PuRyez}|mtDv5laM4G`l%ZhhRm?j4e*}CDaQ~j#}~@=IZ=3awS}2I;Dt;QZhnJF_y-^gnF9b4 zAx(OiIzslRa1ooecHc<>r0)i6;*gv8X_y{yF>mRWgBC*+3oWp8vSQ#9Q!(ab{LT>b zBZ2Auy3vA~72j%Sx>q@YFv0P;m3c9xrbVXNqFw;o&ZtkJ2Gf^-UQhxveXGQvS~8g!Y)+}v8ec2iTJf)&qPUi`=j zrnP7wpi(lu2gu+8Jgoq3QYGieIT-3~NNs3}H|Z6KCa8sYO+OQu`kro;1N4| z4I{CvIW-n5)eg*yR>l`IT@T1EBPEs>?Bp`WqjHuS=KcCOt?UD3?pug`<72a^?iT`h zb_Zglz>Pz0TcH{W%hG!8qaYreoL|Y1kQP!K8MDqb@xpsdO+1xuve9huL_>pKW_Vf8 zIc8S1y0-6QZtivEOx&5#6t;#Y5PbgGLJ&Kl0=IhxhdFJ<%T~&%b?>|DXGd z-(&w1c;bJ@H^A@@qwTM+xl|j;qFv1Y?&ZUvL)+Jn*{Cnn8w9|Hmh7yBHuNQnKAykS zMZ(eS8ktJZjvF)6Sj8D>OX9&r1jc1=>)}!Yw5q!r$nR|ImtKDPC2fczC!fIPrB`0F zwH1-fD)RKN_^cfm0#2|nm#w;tGW5FDrZh$;C#J_Vpt1mx$)}O7fknlbWw{G*!y?{(!H8|tVl zFEfIU2d6#lrg8(2=sjidRv?t`*%{W#=%kEUjJtqNY);{7T|6V9Ml@^|?A*=^>o;g0 zFTWPtCRpl4r@6HwXMoyK9GcaIZ1@HdM6-uH5I+26F{%pKh9M;z!Keef z|BAdA?!B9s(p=%rN$Ma>8t;}mXs_^&8NvH<9IJWzaU6Ob?qIK0mYvgo1mfu zL^(HhYUth>)JnNHv_;kgCdR>2Nug0E&qyB{g<=0Z0SJW}_$8>?%8gALYK|@Xvi00Mbpg#PA zasBFHO|r*X!et|i#GQ0-_x(3r!V7lJoy9x&9JZsAS&OpjBqx}@9^C#LW>1&gr$?fB z@~5C!Xjq!}_7<&cLXxOUYy0!LfRZ&upoMPB)AJ0w#FS(`lTqIqy+YQAI&QR4UQKf~ zzltjtVQt^FzPz$Gq1RY>abc=@(<%P!AK2!fUI5yH-l<{7PeW3l(qB zajU)8PY%F83${6r$m$YIEanT@+=i{CnU1zcVNlKzQ>83&^&k!r19VNvGeEGD91dIn*tOde?n_nvX zu}iTBG)EIG=-(4si{sD+2N7UR-k56At(q}%a6MkmX@u0X05l`W0=VE7v7&Twc7ry9 zTpB=F+fTS1%TfI_zW{{zbu};j6#X!;-SgR|S>KJqlBN-To0JER+hoa!=e`m2sh5?ULb>u;UD8(uizOUP zG+lTegf?kWP}oq;ngwnnVN9Z*`rT)S(@I;ENTr&MvXAHdJBFu9*e2V1*a~tM^aUu+ z8mp9;+>DmHGs(jS_wGpD=H5#48u`pos}4jrM9je=r9 zjiVIu>=>Jv1(NFzA}*=tCS6@pv?s6ZNXWHl=?BCFFuxJY*HfJwD($7E83XB5w|^2T z%7OFhpIMk4e`jGXdO{`yu5olNu2g|~F?|9|A9O%ffln8=eKW9}F_LwHTfpZ0w`(-( z(lhoXdUm_(uXsMK5)#}~5Ml&;Z(Nepg^r|1RC^I(yN(kK~k!Y2=<#Gh-|% zN`DGHb31;rq5|S_Xw_wm`i}q3LttLk8X;DQ?A5KlGT6Y2;B344Qm&FeHIC7YkvFt0{plm`h3<8iK}`4$E{Ho!NydziFx{aEpq8 zhhKIs+*15o_FBv9s@(1|+}Hh*^}QGA-$+KUaepr*%NR=l|2B2kQivUvTNAZ#O+xS^ z8SUrdntIhW)H2rKw60<>!Wb?>^!blDGp-Fd31hkj$X|S$4WwrT4mY{dxw=|HR`j~u zurYCN(H+A7n?T<2PXzL&F0`e#A#C-mru_;A-RE>duQT5QW&hqDlfcLg{W5I$K5Tcv z?L6g*_oC1LA9kPFD_a7`)dlsvy&{NHqx*HvLRO7}UDq-$%`U4ygUJwUPSabgZyQ?- zk6!Eam8P)R-ciEWDBRSgC%k#jBbW1RsM(^W&dIxoOlRAcyFdezpu9jB##IG5yg8&n z)VW%ev%FE9aJXuUmg%k!TBRJzD%{5|OH!TJthM1N@}6j3be{IVXtg>m=Sy9=aeQ~@ z?qP_PV&cd?exd4vVqrCfx&j|^k=oo+Y%2B| z94URTv0e6*!qa^z$0)2 zvW=zPmCtogL@r0+4-Y9eNci$Wm@KNGG3qrAMy%SBI3NawEv;eBm`_bO~y&n zP{;YrmxeScS>-Oj_|N@4VJob#(i_EQtq_Rx6 z+LhCXHv%6QMKoJQIkxP+<}dkqCHEaIMJn+|ch5yeG_et#oyQZ|&MuG>DN?`^(ggcEJVK=LT6_+@yj}KdIt9R=H0P z;_~dC`XxAPHrrLA@fX0l&S@%lcbbg()ae=_10_-oX)!?sxE#!Sj|X;l);R1FNJ>p$#)gh3ml zvAtv}EG{W7jF(hWP*A#O6)R7jKt!bQf)K=rxD%i=YMx%TE6)(ZpQwc!e&3Q21R`aO z3jp;qgWS#~uHh4>NZ9q@IJJmI7775>nr-mn;-iLYpcO|&;9#4qiRFZFrTN* zc&8KP^@Z<&La;*3`XMhUZya<}TG-?g?`5l6P?bT=r~K-$3HC#$v?m|S^_{c_c2a+| z|ELlyhT0Oky&K_gC?yaGw|>(QZHgZo-IA8AK}&v2;aui8>~nz@sDjJaPaUfR|@ za0K6vDjnRY;)AZ`X7-rbq*S%lXkFI_6R@W4-r6pB)_4*z*f%gp}GjJ&=mR?tT0fl5AKO3=j;fc_@wI zTZ`K?VT=4DJGe+rjquwymi19*-TfawL}%ZPEUZi>vlFZ^SFpVkKz#aMx_mvUUo9iq z1iDV3b8(foXc`>5@7{&V{r=M#9m|sY(DP&pAmXT`Q|vhrPdBK6up>(M9h>AEHvtgn za(P{}LULnfocyE8`^em^e-wFP!jT(0IKdH^brpL2`Vn> z)>op|_Mtb30`QR;dUYSRbJ)IChjMu2e-HE9GBQ@~X8?>!4?q*m$||}_0Xl3N?J|+)OVx z*cb?as^K??%iD|35D%EUFMq_svPoP`itg*)K*$MY*y+Zry8yirD?O+j+Blt790 zp7%`FL-mbRQRv*Gq|0AQHP2p1-5X$>5T3tZIQ2EhLaT#~giwc!A3*u<1E_x0z;9&4`YbEsvh1bFt$5>u_PzO) zJs*Hp3;%aE0)P6gukf`a1xKK;j?IWXt08KSbV3bEhag5CLCL@*&vY{u`J9=;f~mzx zmF&Mgi4O~Cr1ep^vSZp9(3kx_EZVUNL_!23r-t}_^9N^qyI`oa5{n%@a>skY08vZ) z%w%TNwM-CTbh;^b%(dE8ZIe^h@^dY8JJ{bL95>`}d>qDNvz#T3)z2F!95>LV5&J^O z;4?raCG2&=?I6s`1o;J*X&)kTH*cEWI6aB5OW%}<89DzFYqNdFIy(S_5jZ}v&`SQ7 zbnRarsS6eG!USUW?&|2@F0Y(E8)dryZGZ|)>$52;t(-n zF}XM$yBPB+)6y>~z<7W%dbTxwPG@Ev#z#t-3kbaAvGF-Oity#KW`hnj-l)SUd#{8-&XW&2aVC&Pw}ngzNUnh}C?>wH2Rq^L@oj$#=Gc-Ch9uE(Y1Z ze_3))ZYW}@#U=-G2}ooj;ItHQ$q$k)ffZt-{Pv22CB+fWr)T-M4c-?$JUHZyaijM4 z*&gr0@&SLuX(V{($g1=y_a6KLP$>}fU5qcv7^PEuU^w3Frt$xR)wg+h>wL8A=c&O} zexZ#rOtUo0GrGXhe^FS?VOw6%X4jjnQ3@ zU`=v=X78wcuH_&d!E(g7Sw?397leG4h~jE-zs; zbY>+KK#k2p6IlbJutsLKwkA+~>=R=O92R+Aq?VrNU7Y;kCQNEK^jcn2)EK?7d=g|5 zZftL=hA0c1(Nv?bV!qX}oaek+`+Hi98lOnBrm8SFJTck6T2rJH%@mN@ck)^*4q|5; z@lZ2Xh;=EUQ?d;mj0ZD~dJ7s_TzgSQTYw(LTj3R*c+^*YVdo8_fg(}O$I|b#3lhjI zoj#k}=v3X*B0GrrUFN(($}**QAxROF zi0BIkHI2c-xdhFed%=kw8nPBSsfQDfcV84D*r5BM^SZ7%xa$>OpcC1M;G^r@31Wg| z8%i104fS;lMyz7;Ghwtb!xq@`VAef^fv&#EwK!{N%wA8ik;uw}O!5^YxaZ2ex(xlx?9L%>&;S(PG3bw}1Hk!+@gG{ejC0yMn zGw8e$|6HcVr=&+OA;u{&WKco*5u}R2=#wSr(03X`^In zYW*~+KY5Wa8Yk*}c9$=`DJPk1OltY-ei4v|IT&9T5s~5-JcJ4`zhHEQJfA%+>u?)- z5{AjHGqU@N=8LY~`CniLhw~W!>?03k?_W!xI+|&58;N z=hF!)+KJ)=xRUw-IE@J;^w@?V420nN@YGO!lA58i?P;?RSj$u=W8QoG?By9~D(!wpxIXR^qI1r!@_mIG$Ji?E#;;8-8_fdloB9X1$G zb~ABe{^x{O6w;XWvuC^if8?8*e}*fRyclnoT zHMlvV1N=!#5#5FCszo2yoS1@(N#$SEFK_WikUy2pw=1SGQ*CHM zeQXaV<+dOXYA7f8-DogX)$CZP?fIx89}&09s71Ah%An+c!^-SorFMD7 z(b*GP()Q9q^u?Im7mq|0bS6zh#4QIOzxI&^v{r#Eur% zXY~^Y`XuYM^m+2wftK{fFY8e@N3@tyFf$zzUaO;nz23BJl8Czlyo9z|5OPnRnA7Z8T5$2_M zBfN4r4-&M!?vehg1Oka^qiUDY#<1atDp7MYwx)K=pLx4K!foemi$1jsHn%kc%H9iX@JpOfl=JXobCV81&)jmIB z<_>q-G}v>FH#{^ZEYDX|*(GthudG1$I{ESYLRz-T>0J{dSfM#7&8&$7+KRL}Sy?#&} z7KzqpcXb{Sb5|s0y-tbOke?#cm~-!_)3dgv1atZOOYVbL3(xja{EvJhm>XI~XA*B? zJ7*~lVj6bJeM?^ABz%zjMMzKlZgSRbxYX5Y$N%N6GrAi{*kj#W`C};cm+M@a2a+Wi zZBgC>LyG#~08v;Jzd`?H93$dS9HY~taoAAT&$7jx^z4PXk(h;N)G6OCqB^@Qxz9!q zJ%sX&T69-O+Gg#)}N>Gj7SOQd_ zS_r8Ts-#DP72Ko6a^K~Sev0GdUTWWz0yL`<8PR+O)l+pljV{Fky$4#$E6HEp=gE_i zyta3I_K5eCTJc#H4~fBI5&Ltudem2$?3c)3@cJEZ&fq!lyF(_zyb%`8%@JId?=Hi% zsyAQ0;Kt%;CIQ6MO9xy;+f64Fh?C&4ABjif#&2+wg&?F)OlC3~RIME=GV0-y` zA+o({VZ~<^ZRFlYIDUcdFHU#N?A$2Gl^2M*$QNbtXi=Rkkm)ta&{rM6IGrgH8&RJw|41DK7S4hJ)qtQv~Rl3QG3mu8&S3K9NiA58dYhs`*?UAOnvpu zS%|7)*{$?pAWe*aZTy--e>nbmQQt&g{Ln6KL6jJj+bK@+WmTmglRrO|-Fhan1?_n% zVu@3+JnZnKZD?`f2Y=Qomym<0ONZ}sIh$l@K5~)D$!HStuaoZ?(T@QW2xJs7oU#IK zW?bw+Dts4LP;_ffXBW06uUN^Uz;!b>`gyJR?Tn4xiOefp9luZGy9o;mUL^%YjuPz? zsu`DqTbJ;;@!z1g{`-}0F6SLa4i&(O@k5`78f4X~6 ziYH~?h)BXdG4Q6ttT18E#X&=SP6~Zpn)xzBE zE`z#CpqQPw-lLe_4)vbSPTvs;VqiI=BvpKZJkH#X91Si&WrVeZ636Oruj)+tn5svx(cqP$T zBv|#aEKz1LH;6#(EwD}V@Nh7(;k;P;xccF-tZRoVhNao5CU!>j-geSSae>LUXuu9S zHQc($Vy`u@o`3Uo6^Yzx`46?Vv8#tNjGsVcS&h5Q`ipZY>6Cq7k>qS+LtXLX1J>CY zD!&Zs>VLa9GE9#A)+N`RE>8mP{1ElwH^??cb|c`WTlv=6@U=j4;gICni;`ovS8T5; zXOZMSDrUx~!;$vVJZA2_rv2Ej4GM?Zq(Ai!`bf}a$0;YJOWiwDp8nqMCB_~u4^6s@ z`Yaj`7OhG;oHGRY~Y(C6}b5{vtQZWYJttYOxjiY;H998|OG`#Hr zKxoxlZeH-@D!#D^U3}7u|2`;=DBxhJl7y0aPBuJ0jMaMQ>c_(}9c4N7f(!FPOGMr$ z0wxiS+g#_wso|$2WQYKcyI7x6U~*olYzVz~_lQf|L4VWIVCyMI7G6d(6Xd3bcL4BB8(t|V4=(-y#u?t(^c<(Rs-YalS|EPSc z`UGNfaGP;XcX#W^VO3c9bKfJZj6Kv>dG1f!1wSmVBB}ZOKiGjupYljDg(uTDz^u7$ zik-qB-|g7Fj0=#1AegZTzL(kI*oaWtELD%$0%vcxLe6Q-cG#mT?agr(D(gkTJjc&y z*A));(7t;Jq1t)}sL4gy&;E}BN$P52qd=P93r7v*qOG#+9~qUUnV)QPylnLk@3HDK zhR7sKx-Skl$a<_T^4Bo4e#1b4C4{RpAcj?VpZ$-7BVhx*ma{*}l43Yb8##s!XmJp( z*JC5!r60fSxx<_G-MhNF&dBv*dGtWJc@XwwPJateA$?))g_5c)$GN6}I<*qDUQWd@ zN%--|gK$blE%m-Dh7Muv=OxD}#o@uzm`?iqtAmKrp24R4P-puMM!jGen)e!n()Vab zg9!8-VEpjFIj8WYiJ1M;q>t{~hReg+mi$Xfq}+Pckhu)MI}RyM7>LRZvkceAXm!I3uSC}tDd`jyRI5|{jobcl!3YYY8_;}^1T(y9` zOW?}0$XG2aw;VZkGfj2x)C38ks?9+g63)|jCrwiI8)vYk|8hnB7twv;E-RNAs$@Y! z{mO%IEh6QB>0nh%RuBmgvnda`!{#jl3>*SZ8w=2oh?o8{Yk9dTR{0^ZspZX<_7g*a zKa!s_V6xN6oK)x*(M8d}M*o~niG)7iUv<0g%-At(uU7JoRG)>rEw9g`w~fDP#EDeA zHVeL#kfaRzCekH1X7}jiNO_jh1!!JeBTVKcldt^*d;S5O=?V7CZ2fI`ED8#pJ6;88g z%NF$G%*QZM#Zf#C?C}({6 z=MimveeG8qhllcv8OLNG0%IFSuQ&a49DkY%sBV&l1<;?$nwUHW`conz5YV53KyR^% z8_^2wSS})i^Asa-sJP&Qh(f_^E_QcDN?ZdHfNEXM?Gh|5!lUu*$n031pgG0*M`Bsj zJ-(MwWqc$qKgMTXMBMc5^!UJXx{wjQ2brfLX7J@Fi`$JbOMivA^}Dw>tgqBRb1r=3 z{dx9q^r4h=O0XJD9olIU#=mxP_ZIJMR}dt>@lgrhZWiQAQun8fe(0ycmO5p&H|3V+ ze1=eO6YleOJ^9S(b5k{8WT=IP?JSu@`61r(Mhd#91NO0xs*mN>XMK|SFIy{5=t(_` zx+dY>IHwRn^}-bZOBi{33VVsV`|FnD>Ul-;BWZc@r<4@2AW*eT_o~!~Fc3}6*DH#P zF~jni8@Jz}S2EX#s=D7GDI~metKLZOAl#E%h``U`^Uhwq=wK(qe1?$nbu=yxj{+% zq%q7UGdhN<%*B8TD@z7KX!12|su17K6^h)1B&n@MS|7c6pFj_0>1I~_Ig;0yF8b)jZ6wSV2FDnXnsZwt}W?OcU+ zH<7THGdJYbuB`+o3wY`Cent)1deld4SL6}Y%>s4Y%$5ZUpG!oZ3~*l;r4c<&W-8z# zRVB-hW%E=ld}-yi2`%|FuNzcZ-`#sBG?PZ`<|dtl#tZL&xdJ?;)X8PFIY;a>t6X3V zZ+fZsIp#}Xne>lnatT7J7ks#*uV!J3Z)s78jzu@;A#lH z+};;|)Y&A})(=bk#_mLYm(Qlgwq@(nHh1M{EAHi(B=R|lmWD;{JGqFpPe#j9#i9IL zd?337ds~SAJEs}_5hSzW$}%jWee$Ou>{v9T+k*&DBEfqr37#T7z^1T~C9&TiAMh0c zAO?W~Zwht&o;B>xp00#cqh@MyibS*KSW9(UoR8Ch2}sTX;=MIwOw-e|?u*MtjwZYV zh>t<{M9;Js0IE*#^Y19}~)mW4~qSyU4XVGejFEz=(xdE{*hmSvbKBk`mG? zOp}>nxtf+-LrY3#YY*M5b7LeYO*Y&wBpCAryiHGgd^@Vs+F&#ow_MJsM@R^e%yhb! zMbqn3^5vq&e{-1;(5Tee^36ncO9Uv2=$Kg0$iO?cYw`DmCNHJ{+_%%eeZQ*t*>4bO zk{9e~axVS*8-Ulx#0)t80GK{lxw815{91y1V!Xda{aMTum7w1RqrYz~;zYebY2J`; zRaY8`?}sMC*5S|QGfpo4bu$L+NdLAP!}!~3%mDIdr8Wty)KU|!SWah?C9t*plZ8})DFFKIm za7n#(lk}>Q>TTA;%zpQ7M-pc=-#@~({i_rls82}Ko7UlS{Dx0MAsv-hF)$=6ZT@Wk zvZ?XZ@Y%`q{ZoqL?9moK;wNiO46+nTFg#rN`b1ohR()2kx~yu=N1ajxUvKo;Dba$N z%2U@=*7Aty>DE+MSf2QpH zs)ERjFSwjpq!*4Mjb<@WAY@#+hlMR|va*NWyn}nNnCE2KK+)pS5lgt+KAF+lmys%F zzd6E9CZHP1|9y(TRJnkEYRy2bH!&^r&Gk)uYNBpW!8Rb*gYs^T?maVg;tXXgP<<`e z$I#1kvVmf?AaBbwiq`8yIfe^5liQ*HyF?qGsGLgR#Ho$QrV#m5wVWu_|d; z8S5V_(fj8``n?dQs;d(N<_$uH5nsQJ<963vSu=vx0+dvzyG`p;Cf+gQWlO4Xa}BN5 z%8}&jVr{e>^Fi)BGHVX8I2sDniCteLM>vsX`{X&*q}aaM8hX@OH<-A3`m|YSZd;Wd z4c%brVY=&4U+Bq?NkqoK61#5seHEd%si%jPA+Zs{&_#=>6`sS=90kLr0=?up4cO_n z;S-xDB1%<=O4(c&P{EC459LcakcaBtp^#X73y@GvU*oPUQps@8q(S;$J4IYRZkt`6 zJ^4JS$MHx3eT+o*tIDlgjIUf7r`PL@H7O79;2ERnb#HH_6p)0Ay5}5tNxt^iT+4PO z6AhOava?N2(Y9smvMSDsx&P{MJpWG+6_hHQd^#et%2NHxQ@uSPaG z^b@kWsY=6ge0{Hg1*0;)gd>&-=*r05CFu%bKzw@9Nb`n$;{;lJ;=1)dEIn}9DyE5( zI}xgdr9Rju+*vp}H4hvFP6H-aj1A<7fWxM4fBKf-^N(!fANlPNjWp_$lm_Effrq%Y zXwo6k2o4ShfFlJe{nCk}QjXA@ac)g1JFRCuuC^jeJSCE=LrAg3OM-!^f?}PR@(Vi7 z%E{Vp3cZ$eB$_xG#%K^V^2WKIuR;12S($;7&g`JwF$T3B1x(XzdAMj2t4iJ^B z#K{Ve($OH_Dt(uFN}GI~JN|7L-nIF_4wKw57+0h*5n9c>c%|Tb$ZxUUO$LFFRu={M zd-CCPmE{fBi2*E<*i873pcnL=(-6x5I*FseYg9y_>|-~`GQ?r?Yal?Kx0WIq#USD9 zdK6ZspOa{87o|wx;cCT2mt?E3=I4OCsX6x&5LMbpe=3uSE;~DNa=ZqM-Kk1hrq%sP z5&JU1Ns~02qOg1REM?k~_t>YnnZ%>)dtOZIzmj653&SjBjYhKtHO&C!7Y~cr?2PoZ zwEJ^BCf)~g78tT%l7hf)U|g7QUa^}ahot&ovu9$#x>{|m0fmGXgA8W4wOeoeF|_>T zsOPKDgKx5zk2FgUS%kEn$S8vcPCGLG`NsZ!aej%L|7$<^G4X#Q9_P5XeRE3hXE0OD z>VVp8-O9SA`F*(MO^(5Xv6XLS?&L7AvQ5f(MxyzkK4vX`*V<)Wx%hZs(&6a6Q8yPy z>WuX%A;!oTw?_$JI7*IyMosM;;kg>t9d~k$UAVL+1`no~_PnlbX!mR=dGO$g;i+Hh z?MIv>-UKgNO-dDj7!Ts{L&rMjr%rdDmmEJRfJ$Z$GUR93oQBMG7=;e`wJ1miT5;Ol z7#tW+gq1+Q<};liHxwV9AWfv)_MwD^Eu7_>ZTN3a*(9}`dAeYcrS^Q@Ezh3lIXNZD zi$5&RchEM;D>K#87btZjdz7FEnK@AwY2eXlF^%>mRybmmzIE;yDh>DRZo5o$ZG2ud z;S|=O1QO<%L05U+BSlEA^a$NSM~zna8d9|eT1-#S zJ`W_nJG4Vbxw6-`zTu6-xy)ItW@F!OGSyo96?Sq5Kyw&ncTImUwx9$(3;gEKwuOwS z6KO_kUTnzL$DBiS0zdhyQQ2P_ay~WsTSLw!&7+=w;B<9CoL(ul6j#X~1Jz3Y4%Jsn zNVJVs+7@@j(x*0gcEfS{LaBiDcOAr(hK4a)PJ2Q-`Yrt8klK(Q*|>_Qm)q9L%h+X6 zp1Ad!Ria;|a(8&tKeZ;2r3+CRe8(AMU;)bOJup6NdN}!{rLC@4?k*d;*WuGR-lti) z&Q8m%ZgICL5=8!1T8&1}#ppzFe-qg~gUGEB7+)4m?f)>gONXt>SrgtGZXQ7M!Wpyv zJ6kiyeV2Q+cly7SlWgC?JT$%c`Yq6j9r9^&>k#g}*y!K+LZm!4%|nxPaUDv-8|e%p7wQ`#NCYs^oi26!#e?>^MiA(7?c z(u`m_9PB~cZ?;{L7o)K-4o=qdcKQQZF}n*aT80pu{yKP(*0Xk^f%iVL`E7scST7| zt7(RqnN~Hpl+rPFq>Lg402a#|->uur`BTW-9dsu$rE8ac+vnyM%}a%aY1NAy*hS3B zW`-kGIp5&fvxI#}I3{6a)%SM(Ml&htrL@D&yF3&hBP2 zZU^#eIJ!gB2qG$KjFkR`-Sb}E%IppR%yMN#pJ}VkMbM1QnD?2>IS90ux9}g;9}AI4 z21rx{^maw0xE{SGy<4CztXI}&SLz+y9}-(_(=dnKu$ORPjl^P*KgrgeqVY@GqZGJ| zzBN9grrxfuGb;k7TQ2g&;3Vea;3bz)VH@PWv?xF9?H9*Lbf7)@euQz150&5EDoLt< z?scwYq2Sw_p;k=<`rk*nRv^v*7ccf76fSE)_$ z=efiPvw#Nk6Z9>SpAfv@<3|IVN2TBRk5Yt>Ke-o>)qA+A>FetXW`c!|_OYK@dE@0I z?cJvk($?`UfDI7X8NXmIDFp|Dj=^7uVny|?sLuTsJdci~d=9o=A8Jolkg`4GZ!8xH zpEfE}J+n#j+&z$wMN0_@yR1~;=_1Fxv~+237G+C#I{+kCU8>`J|8}GOGHuYTjU>r) z!D%uo_8L>FY8(sDwk?}CJf+XKlQDd0Fao2#&RZQQk~wv^ep;j$ZX_VIApUx-^p2cF z39G|{XkX`yu&8Glrb=myN}8Aq3hb=v=63}6nw-|}#)i&Crl)n`_0w6fuNFMWn*obyi?Ay zl`8a-skGJr@Kw^;fw{5Lyp&+aiIWt`>s`DUu5<2ets+g*cA}#m|9%cv6|KAaF|tg2 zm_ge&Jew?yVLVp0#jiLZi(yLNxA=UXW{X2IRt-OkQaMw1j`f5k`+TLGV7i)$5UN&~ zp5+N^$EVl)DD`f!rTOGk(=+aP+bmyCUR?ml`wLKwrUS58W|ClPtn_#|?>kLnN$ryL zmqh+0P4z8pP81(sXD3ujPjm(NleoMYeK-Hw*}3WRvlD4(Jjo~H(yMFc&H>65nh9Hv z04WoDRhYC&WXiaR0K9#Su1_CntF~#{TbmFREy5?vZ^-LgvYA;(TusRJ6AO!x9px5Y zSJCIirTgyUpWy|0hl!JwvjgRY?E>Yc0ErkTqx7p_pUNV&=%@TcKr7mUa0Bd|U;$ilOdE!V z7w1D)F?ENwug%l;hfQdr=t94*xzm%s8kCJmqUJ0gkvRUkF|1s_hasIvA$W_+g0m^E za$*1-T~x!ss^7mUXf)gnd8m0tymPq4FV%QC)K#PO+Aow!095W9WvbQ1Ao)mGk4Qj~ z)_{}#(SRkx=%&WvrdjU`hS@Fky=mjSS*jkz3v(`

dU~G|{V$oYqDh`J8YShk``v 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�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%0Dminh$5a{KuPtr1c&!*>e>#9;~u1+ad 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 From b7dae5adddc9a33f4ee0bea7bd91748418e1f699 Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 3 Oct 2017 12:18:27 -0400 Subject: [PATCH 476/633] Bump with statements in one level This is needed because the previous commit changed the context management style from nested with statements to stacked with statements. --- tests/unit/modules/test_status.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/test_status.py b/tests/unit/modules/test_status.py index 24efc397c5..fadd448aaa 100644 --- a/tests/unit/modules/test_status.py +++ b/tests/unit/modules/test_status.py @@ -136,9 +136,9 @@ class StatusTestCase(TestCase, LoaderModuleMockMixin): ret = status.uptime() self.assertDictEqual(ret, m.ret) - with patch.dict(status.__salt__, {'sysctl.get': MagicMock(return_value='')}): - with self.assertRaises(CommandExecutionError): - status.uptime() + with patch.dict(status.__salt__, {'sysctl.get': MagicMock(return_value='')}): + with self.assertRaises(CommandExecutionError): + status.uptime() def test_uptime_return_success_not_supported(self): ''' From 7d5a121265ef7c3765fb266911bf2490428357e4 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Tue, 3 Oct 2017 10:22:18 -0600 Subject: [PATCH 477/633] Skip unit tests under Python3 until loader is fixed --- tests/unit/modules/test_vagrant.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index aaa69bb078..708e923139 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -21,6 +21,7 @@ from salt.ext import six TEMP_DATABASE_FILE = '/tmp/salt-tests-tmpdir/test_vagrant.sqlite' +@skipIf(six.PY3, 'TODO: Python3 loader raises KeyError, issue #43815') @skipIf(NO_MOCK, NO_MOCK_REASON) class VagrantTestCase(TestCase, LoaderModuleMockMixin): ''' From aebe76b6f8ba52caa570bd9a54b1b274e100bbaf Mon Sep 17 00:00:00 2001 From: Corvin Mcpherson Date: Tue, 3 Oct 2017 12:56:44 -0400 Subject: [PATCH 478/633] Fix issue with using roster_defaults with flat or cloud rosters. fixes #43449 fixes #43643 --- salt/roster/cloud.py | 2 +- salt/roster/flat.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/roster/cloud.py b/salt/roster/cloud.py index 1bd0009156..2042a03200 100644 --- a/salt/roster/cloud.py +++ b/salt/roster/cloud.py @@ -63,7 +63,7 @@ def targets(tgt, tgt_type='glob', **kwargs): # pylint: disable=W0613 )) preferred_ip = extract_ipv4(roster_order, ip_list) - ret[minion_id] = __opts__.get('roster_defaults', {}) + ret[minion_id] = __opts__.get('roster_defaults', {}).copy() ret[minion_id].update({'host': preferred_ip}) ssh_username = salt.utils.cloud.ssh_usernames(vm_, cloud_opts) diff --git a/salt/roster/flat.py b/salt/roster/flat.py index 9d5af1c333..89f93549ed 100644 --- a/salt/roster/flat.py +++ b/salt/roster/flat.py @@ -142,7 +142,7 @@ class RosterMatcher(object): ''' Return the configured ip ''' - ret = __opts__.get('roster_defaults', {}) + ret = __opts__.get('roster_defaults', {}).copy() if isinstance(self.raw[minion], string_types): ret.update({'host': self.raw[minion]}) return ret From 223a1eea83e7b8c8fc3dca96aa45af52235ae3a4 Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Tue, 3 Oct 2017 11:11:42 -0600 Subject: [PATCH 479/633] Fix object_to_dict in azure --- salt/utils/msazure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/utils/msazure.py b/salt/utils/msazure.py index 7488f3077c..7e1981b0d9 100644 --- a/salt/utils/msazure.py +++ b/salt/utils/msazure.py @@ -186,7 +186,7 @@ def object_to_dict(obj): ret = obj else: ret = {} - for item in dir(obj): + for item in obj.__dict__: if item.startswith('_'): continue # This is ugly, but inspect.isclass() doesn't seem to work From 88f2c0ef97ca4a30276b494e88e89f54e732c89e Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Tue, 3 Oct 2017 11:18:24 -0600 Subject: [PATCH 480/633] Add doc for new requirement --- salt/cloud/clouds/azurearm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/cloud/clouds/azurearm.py b/salt/cloud/clouds/azurearm.py index 0397aaed44..2b79255f52 100644 --- a/salt/cloud/clouds/azurearm.py +++ b/salt/cloud/clouds/azurearm.py @@ -10,6 +10,7 @@ The Azure cloud module is used to control access to Microsoft Azure :depends: * `Microsoft Azure SDK for Python `_ >= 2.0rc5 * `Microsoft Azure Storage SDK for Python `_ >= 0.32 + * `Microsoft Azure CLI ` >= 2.0.12 :configuration: Required provider parameters: From 87d676f08a5b0d0aed303dbef6d6e2f98ebc69ee Mon Sep 17 00:00:00 2001 From: apapp Date: Fri, 29 Sep 2017 15:03:56 -0400 Subject: [PATCH 481/633] add -n with netstat so we don't resolve --- pkg/rpm/salt-minion | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/rpm/salt-minion b/pkg/rpm/salt-minion index fb9f044651..6669e75cb0 100755 --- a/pkg/rpm/salt-minion +++ b/pkg/rpm/salt-minion @@ -67,7 +67,7 @@ _su_cmd() { _get_pid() { - netstat $NS_NOTRIM -ap --protocol=unix 2>$ERROR_TO_DEVNULL \ + netstat -n $NS_NOTRIM -ap --protocol=unix 2>$ERROR_TO_DEVNULL \ | sed -r -e "\|\s${SOCK_DIR}/minion_event_${MINION_ID_HASH}_pub\.ipc$|"'!d; s|/.*||; s/.*\s//;' \ | uniq } @@ -155,7 +155,7 @@ start() { printf "\nPROCESSES:\n" >&2 ps wwwaxu | grep '[s]alt-minion' >&2 printf "\nSOCKETS:\n" >&2 - netstat $NS_NOTRIM -ap --protocol=unix | grep 'salt.*minion' >&2 + netstat -n $NS_NOTRIM -ap --protocol=unix | grep 'salt.*minion' >&2 printf "\nLOG_FILE:\n" >&2 tail -n 20 "$LOG_FILE" >&2 printf "\nENVIRONMENT:\n" >&2 From 916ce3d006d1f7395b647e8cb8e745b84c889f7d Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Tue, 3 Oct 2017 13:37:26 -0600 Subject: [PATCH 482/633] Show all nodes if no resource_group specified --- salt/cloud/clouds/azurearm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/salt/cloud/clouds/azurearm.py b/salt/cloud/clouds/azurearm.py index 0397aaed44..ec78384713 100644 --- a/salt/cloud/clouds/azurearm.py +++ b/salt/cloud/clouds/azurearm.py @@ -401,8 +401,9 @@ def list_nodes(conn=None, call=None): # pylint: disable=unused-argument pass for node in nodes: - if not nodes[node]['resource_group'] == active_resource_group: - continue + if active_resource_group is not None: + if nodes[node]['resource_group'] != active_resource_group: + continue ret[node] = {'name': node} for prop in ('id', 'image', 'size', 'state', 'private_ips', 'public_ips'): ret[node][prop] = nodes[node].get(prop) From 51eca1a6bdc3d374538feb174160c2f8a6e9ee67 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 3 Oct 2017 16:13:54 -0600 Subject: [PATCH 483/633] enable tox for tests --- .gitignore | 3 +++ tox.ini | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index d676e374e3..0eb3831877 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,6 @@ tests/integration/cloud/providers/logs # Private keys from the integration tests tests/integration/cloud/providers/pki/minions + +# Ignore tox virtualenvs +/.tox/ diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000..52b026a299 --- /dev/null +++ b/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist = py27,py34,py35,py36 + +[testenv] +sitepackages = True +deps = + py27,pylint: -r{toxinidir}/requirements/dev_python27.txt + py34,py35,py36: -r{toxinidir}/requirements/dev_python34.txt +commands = + py27: python2 {toxinidir}/tests/runtests.py {posargs:-v --run-destructive} + py34,py35,py36: python3 {toxinidir}/tests/runtests.py {posargs:-v --run-destructive} + +[testenv:pylint] +basepython = python2.7 +commands = pylint --rcfile={toxinidir}/.testing.pylintrc --disable=W1307 {posargs:setup.py salt/} + pylint --rcfile={toxinidir}/.testing.pylintrc --disable=W0232,E1002,W1307 {posargs:tests/} From 83a3678cef854b2fa0ee0d790b5b984cbe942218 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Tue, 3 Oct 2017 17:24:05 -0600 Subject: [PATCH 484/633] provide mocked unit tests --- salt/modules/vagrant.py | 4 +- tests/unit/modules/test_vagrant.py | 174 +++++++++++++++++------------ 2 files changed, 105 insertions(+), 73 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 1a10fcdb80..3b9bbe3453 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -518,8 +518,8 @@ def destroy(name): output_loglevel='info') if ret['retcode'] == 0: _erase_vm_info(name) - return True - return ret + return u'Destroyed VM {0}'.format(name) + return False def get_ssh_config(name, network_mask='', get_private_key=False): diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 708e923139..0d745a6bf2 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -3,126 +3,158 @@ # Import python libs from __future__ import absolute_import -import os # Import Salt Testing libs from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import skipIf, TestCase -from tests.support.mock import NO_MOCK, NO_MOCK_REASON +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch # Import salt libs import salt.modules.vagrant as vagrant -import salt.modules.cmdmod as cmd -import salt.utils.sdb as sdb import salt.exceptions -# Import third party libs -from salt.ext import six - TEMP_DATABASE_FILE = '/tmp/salt-tests-tmpdir/test_vagrant.sqlite' -@skipIf(six.PY3, 'TODO: Python3 loader raises KeyError, issue #43815') @skipIf(NO_MOCK, NO_MOCK_REASON) class VagrantTestCase(TestCase, LoaderModuleMockMixin): ''' Unit TestCase for the salt.modules.vagrant module. ''' - @classmethod - def tearDownClass(cls): - try: - os.unlink(TEMP_DATABASE_FILE) - except OSError: - pass - - def setup_loader_modules(self): - vagrant_globals = { - '__opts__': { + LOCAL_OPTS = { 'extension_modules': '', 'vagrant_sdb_data': { 'driver': 'sqlite3', 'database': TEMP_DATABASE_FILE, 'table': 'sdb', 'create_table': True + } } - }, - '__salt__': { - 'cmd.shell': cmd.shell, - 'cmd.retcode': cmd.retcode, - 'cmd.run_all': cmd.run_all - }, - '__utils__': { - 'sdb.sdb_set': sdb.sdb_set, - 'sdb.sdb_get': sdb.sdb_get, - 'sdb.sdb_delete': sdb.sdb_delete + def setup_loader_modules(self): + vagrant_globals = { + '__opts__': self.LOCAL_OPTS, } - } return {vagrant: vagrant_globals} - def test_vagrant_get_vm_info(self): - with self.assertRaises(salt.exceptions.SaltInvocationError): - vagrant.get_vm_info('thisNameDoesNotExist') + def test_vagrant_get_vm_info_not_found(self): + mock_sdb = MagicMock(return_value=None) + with patch.dict(vagrant.__utils__, {'sdb.sdb_get': mock_sdb}): + with self.assertRaises(salt.exceptions.SaltInvocationError): + vagrant.get_vm_info('thisNameDoesNotExist') def test_vagrant_init_positional(self): - resp = vagrant.init( - 'test1', - '/tmp/nowhere', - 'onetest', - 'nobody', - False, - 'french', - {'different': 'very'} - ) - self.assertIsInstance(resp, six.string_types) - resp = vagrant.get_vm_info('test1') - expected = dict(name='test1', - cwd='/tmp/nowhere', - machine='onetest', - runas='nobody', - vagrant_provider='french', - different='very' - ) - self.assertEqual(resp, expected) + mock_sdb = MagicMock(return_value=None) + with patch.dict(vagrant.__utils__, {'sdb.sdb_set': mock_sdb}): + resp = vagrant.init( + 'test1', + '/tmp/nowhere', + 'onetest', + 'nobody', + False, + 'french', + {'different': 'very'} + ) + self.assertTrue(resp.startswith('Name test1 defined')) + expected = dict(name='test1', + cwd='/tmp/nowhere', + machine='onetest', + runas='nobody', + vagrant_provider='french', + different='very' + ) + mock_sdb.assert_called_with( + 'sdb://vagrant_sdb_data/onetest?/tmp/nowhere', + 'test1', + self.LOCAL_OPTS) + mock_sdb.assert_any_call( + 'sdb://vagrant_sdb_data/test1', + expected, + self.LOCAL_OPTS) + + def test_vagrant_get_vm_info(self): + testdict = {'testone': 'one', 'machine': 'two'} + mock_sdb = MagicMock(return_value=testdict) + with patch.dict(vagrant.__utils__, {'sdb.sdb_get': mock_sdb}): + resp = vagrant.get_vm_info('test1') + self.assertEqual(resp, testdict) def test_vagrant_init_dict(self): testdict = dict(cwd='/tmp/anywhere', machine='twotest', runas='somebody', vagrant_provider='english') - vagrant.init('test2', vm=testdict) - resp = vagrant.get_vm_info('test2') - testdict['name'] = 'test2' - self.assertEqual(resp, testdict) + expected = testdict.copy() + expected['name'] = 'test2' + mock_sdb = MagicMock(return_value=None) + with patch.dict(vagrant.__utils__, {'sdb.sdb_set': mock_sdb}): + vagrant.init('test2', vm=testdict) + mock_sdb.assert_any_call( + 'sdb://vagrant_sdb_data/test2', + expected, + self.LOCAL_OPTS) def test_vagrant_init_arg_override(self): testdict = dict(cwd='/tmp/there', machine='treetest', runas='anybody', vagrant_provider='spansh') - vagrant.init('test3', + mock_sdb = MagicMock(return_value=None) + with patch.dict(vagrant.__utils__, {'sdb.sdb_set': mock_sdb}): + vagrant.init('test3', cwd='/tmp', machine='threetest', runas='him', vagrant_provider='polish', vm=testdict) - resp = vagrant.get_vm_info('test3') - expected = dict(name='test3', + expected = dict(name='test3', cwd='/tmp', machine='threetest', runas='him', vagrant_provider='polish') - self.assertEqual(resp, expected) + mock_sdb.assert_any_call( + 'sdb://vagrant_sdb_data/test3', + expected, + self.LOCAL_OPTS) def test_vagrant_get_ssh_config_fails(self): - vagrant.init('test3', cwd='/tmp') - with self.assertRaises(salt.exceptions.CommandExecutionError): - vagrant.get_ssh_config('test3') # has not been started + mock_sdb = MagicMock(return_value=None) + with patch.dict(vagrant.__utils__, {'sdb.sdb_set': mock_sdb}): + mock_sdb = MagicMock(return_value={}) + with patch.dict(vagrant.__utils__, {'sdb.sdb_get': mock_sdb}): + vagrant.init('test3', cwd='/tmp') + with self.assertRaises(salt.exceptions.SaltInvocationError): + vagrant.get_ssh_config('test3') # has not been started - def test_vagrant_destroy_removes_cached_entry(self): - vagrant.init('test3', cwd='/tmp') - # VM has a stored value - self.assertEqual(vagrant.get_vm_info('test3')['name'], 'test3') - # clean up (an error is expected -- machine never started) - self.assertFalse(vagrant.destroy('test3')['retcode'] == 0) - # VM no longer exists - with self.assertRaises(salt.exceptions.CommandExecutionError): - vagrant.get_ssh_config('test3') + def test_vagrant_destroy(self): + mock_cmd = MagicMock(return_value={'retcode': 0}) + with patch.dict(vagrant.__salt__, {'cmd.run_all': mock_cmd}): + mock_sdb = MagicMock(return_value=None) + with patch.dict(vagrant.__utils__, {'sdb.sdb_delete': mock_sdb}): + mock_sdb_get = MagicMock(return_value={ + 'machine': 'macfour', 'cwd': '/my/dir'}) + with patch.dict(vagrant.__utils__, {'sdb.sdb_get': mock_sdb_get}): + self.assertTrue(vagrant.destroy('test4')) + mock_sdb.assert_any_call( + 'sdb://vagrant_sdb_data/macfour?/my/dir', + self.LOCAL_OPTS) + mock_sdb.assert_any_call( + 'sdb://vagrant_sdb_data/test4', + self.LOCAL_OPTS) + cmd = 'vagrant destroy -f macfour' + mock_cmd.assert_called_with(cmd, + runas=None, + cwd='/my/dir', + output_loglevel='info') + + def test_vagrant_start(self): + mock_cmd = MagicMock(return_value={'retcode': 0}) + with patch.dict(vagrant.__salt__, {'cmd.run_all': mock_cmd}): + mock_sdb_get = MagicMock(return_value={ + 'machine': 'five', 'cwd': '/the/dir', 'runas': 'me', + 'vagrant_provider': 'him'}) + with patch.dict(vagrant.__utils__, {'sdb.sdb_get': mock_sdb_get}): + self.assertTrue(vagrant.start('test5')) + cmd = 'vagrant up five --provider=him' + mock_cmd.assert_called_with(cmd, + runas='me', + cwd='/the/dir', + output_loglevel='info') From 67a701de7c4ee9ee98162ca378d70423a6971e7c Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Tue, 3 Oct 2017 23:23:17 -0600 Subject: [PATCH 485/633] lint PEP-8 --- tests/unit/modules/test_vagrant.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 0d745a6bf2..0abbe874d4 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -29,6 +29,7 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): 'create_table': True } } + def setup_loader_modules(self): vagrant_globals = { '__opts__': self.LOCAL_OPTS, From 0a9acba6257a079ffce0ccc43b7d657cd18dfbde Mon Sep 17 00:00:00 2001 From: Nasenbaer Date: Thu, 24 Nov 2016 15:37:23 +0100 Subject: [PATCH 486/633] Add missing delete_on_termination passthrough. Adapt docs. --- salt/states/boto_lc.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/salt/states/boto_lc.py b/salt/states/boto_lc.py index e21180fd15..cfa7bbbb07 100644 --- a/salt/states/boto_lc.py +++ b/salt/states/boto_lc.py @@ -122,6 +122,7 @@ def present( kernel_id=None, ramdisk_id=None, block_device_mappings=None, + delete_on_termination=None, instance_monitoring=False, spot_price=None, instance_profile_name=None, @@ -185,9 +186,10 @@ def present( Indicates what volume type to use. Valid values are standard, io1, gp2. Default is standard. - delete_on_termination - Indicates whether to delete the volume on instance termination (true) or - not (false). + delete_on_termination + Indicates whether to delete the volume on instance termination (true) or + not (false). Default is "None" which corresponds to not specified. + Amazon has different defaults for root and additional volumes. iops For Provisioned IOPS (SSD) volumes only. The number of I/O operations per @@ -268,6 +270,7 @@ def present( kernel_id=kernel_id, ramdisk_id=ramdisk_id, block_device_mappings=block_device_mappings, + delete_on_termination=delete_on_termination, instance_monitoring=instance_monitoring, spot_price=spot_price, instance_profile_name=instance_profile_name, From 73947e310309dd09a07cfe0e1aeebe936c33b76d Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Wed, 4 Oct 2017 09:10:47 -0600 Subject: [PATCH 487/633] lint (again!) blank lines can have no spaces --- tests/unit/modules/test_vagrant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/test_vagrant.py b/tests/unit/modules/test_vagrant.py index 0abbe874d4..d337566611 100644 --- a/tests/unit/modules/test_vagrant.py +++ b/tests/unit/modules/test_vagrant.py @@ -29,7 +29,7 @@ class VagrantTestCase(TestCase, LoaderModuleMockMixin): 'create_table': True } } - + def setup_loader_modules(self): vagrant_globals = { '__opts__': self.LOCAL_OPTS, From 9730c5da1793ab876aa6e9f201f0b71a0135969a Mon Sep 17 00:00:00 2001 From: Richard Simko Date: Wed, 4 Oct 2017 18:00:54 +0200 Subject: [PATCH 488/633] Make sure volume exists before querying --- salt/cloud/clouds/ec2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cloud/clouds/ec2.py b/salt/cloud/clouds/ec2.py index ac45a7a62b..b7a9061c00 100644 --- a/salt/cloud/clouds/ec2.py +++ b/salt/cloud/clouds/ec2.py @@ -1988,7 +1988,7 @@ def request_instance(vm_=None, call=None): params[termination_key] = str(set_del_root_vol_on_destroy).lower() # Use default volume type if not specified - if ex_blockdevicemappings and 'Ebs.VolumeType' not in ex_blockdevicemappings[dev_index]: + if ex_blockdevicemappings and dev_index < len(ex_blockdevicemappings) and 'Ebs.VolumeType' not in ex_blockdevicemappings[dev_index]: type_key = '{0}BlockDeviceMapping.{1}.Ebs.VolumeType'.format(spot_prefix, dev_index) params[type_key] = rd_type From 042e092ac85bc3e66bbab9fc8abb13fdb74e9f03 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 4 Oct 2017 12:01:14 -0500 Subject: [PATCH 489/633] Don't put unserializable dict.keys() into state return Running ``list()`` on the dictionary produces the same list, and has the benefit of not catastrophically failing on Python 3. --- salt/states/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/states/module.py b/salt/states/module.py index 2e907959f7..058a2e5ddc 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -253,7 +253,7 @@ def run(**kwargs): if 'name' in kwargs: kwargs.pop('name') ret = { - 'name': kwargs.keys(), + 'name': list(kwargs), 'changes': {}, 'comment': '', 'result': None, From 0dc3c4ef1cab6b9b4cfd0f990d3139ce1b8b2a8c Mon Sep 17 00:00:00 2001 From: Eric Radman Date: Wed, 4 Oct 2017 12:57:55 -0400 Subject: [PATCH 490/633] Skip some vsphere tests if pyvmomi library is not installed Also fix up 'E127: continuation line over-indented' lint errors --- tests/unit/modules/test_vsphere.py | 34 +++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index 004149ebc0..c3fc740b69 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -25,6 +25,13 @@ from tests.support.mock import ( call ) +# Import Third Party Libs +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + # Globals HOST = '1.2.3.4' USER = 'root' @@ -919,7 +926,7 @@ class GetServiceInstanceViaProxyTestCase(TestCase, LoaderModuleMockMixin): mock_connection_details = [MagicMock(), MagicMock(), MagicMock()] mock_get_service_instance = MagicMock() with patch('salt.modules.vsphere._get_proxy_connection_details', - MagicMock(return_value=mock_connection_details)): + MagicMock(return_value=mock_connection_details)): with patch('salt.utils.vmware.get_service_instance', mock_get_service_instance): vsphere.get_service_instance_via_proxy() @@ -929,7 +936,7 @@ class GetServiceInstanceViaProxyTestCase(TestCase, LoaderModuleMockMixin): def test_output(self): mock_si = MagicMock() with patch('salt.utils.vmware.get_service_instance', - MagicMock(return_value=mock_si)): + MagicMock(return_value=mock_si)): res = vsphere.get_service_instance_via_proxy() self.assertEqual(res, mock_si) @@ -1002,7 +1009,7 @@ class TestVcenterConnectionTestCase(TestCase, LoaderModuleMockMixin): def test_is_connection_to_a_vcenter_call_default_service_instance(self): mock_is_connection_to_a_vcenter = MagicMock() with patch('salt.utils.vmware.is_connection_to_a_vcenter', - mock_is_connection_to_a_vcenter): + mock_is_connection_to_a_vcenter): vsphere.test_vcenter_connection() mock_is_connection_to_a_vcenter.assert_called_once_with(self.mock_si) @@ -1010,39 +1017,40 @@ class TestVcenterConnectionTestCase(TestCase, LoaderModuleMockMixin): expl_mock_si = MagicMock() mock_is_connection_to_a_vcenter = MagicMock() with patch('salt.utils.vmware.is_connection_to_a_vcenter', - mock_is_connection_to_a_vcenter): + mock_is_connection_to_a_vcenter): vsphere.test_vcenter_connection(expl_mock_si) mock_is_connection_to_a_vcenter.assert_called_once_with(expl_mock_si) def test_is_connection_to_a_vcenter_raises_vmware_salt_error(self): exc = VMwareSaltError('VMwareSaltError') with patch('salt.utils.vmware.is_connection_to_a_vcenter', - MagicMock(side_effect=exc)): + MagicMock(side_effect=exc)): res = vsphere.test_vcenter_connection() self.assertEqual(res, False) def test_is_connection_to_a_vcenter_raises_non_vmware_salt_error(self): exc = Exception('NonVMwareSaltError') with patch('salt.utils.vmware.is_connection_to_a_vcenter', - MagicMock(side_effect=exc)): + MagicMock(side_effect=exc)): with self.assertRaises(Exception) as excinfo: res = vsphere.test_vcenter_connection() self.assertEqual('NonVMwareSaltError', str(excinfo.exception)) def test_output_true(self): with patch('salt.utils.vmware.is_connection_to_a_vcenter', - MagicMock(return_value=True)): + MagicMock(return_value=True)): res = vsphere.test_vcenter_connection() self.assertEqual(res, True) def test_output_false(self): with patch('salt.utils.vmware.is_connection_to_a_vcenter', - MagicMock(return_value=False)): + MagicMock(return_value=False)): res = vsphere.test_vcenter_connection() self.assertEqual(res, False) @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class ListDatacentersViaProxyTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.list_datacenters_via_proxy''' def setup_loader_modules(self): @@ -1126,6 +1134,7 @@ class ListDatacentersViaProxyTestCase(TestCase, LoaderModuleMockMixin): @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class CreateDatacenterTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.create_datacenter''' def setup_loader_modules(self): @@ -1174,6 +1183,7 @@ class CreateDatacenterTestCase(TestCase, LoaderModuleMockMixin): @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class EraseDiskPartitionsTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.erase_disk_partitions''' def setup_loader_modules(self): @@ -1244,7 +1254,7 @@ class EraseDiskPartitionsTestCase(TestCase, LoaderModuleMockMixin): def test_scsi_address_to_disk_id_map(self): mock_disk_id = MagicMock(canonicalName='fake_scsi_disk_id') mock_get_scsi_addr_to_lun = \ - MagicMock(return_value={'fake_scsi_address': mock_disk_id}) + MagicMock(return_value={'fake_scsi_address': mock_disk_id}) with patch('salt.utils.vmware.get_scsi_address_to_lun_map', mock_get_scsi_addr_to_lun): vsphere.erase_disk_partitions(scsi_address='fake_scsi_address') @@ -1260,6 +1270,7 @@ class EraseDiskPartitionsTestCase(TestCase, LoaderModuleMockMixin): @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class RemoveDatastoreTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.remove_datastore''' def setup_loader_modules(self): @@ -1347,6 +1358,7 @@ class RemoveDatastoreTestCase(TestCase, LoaderModuleMockMixin): @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class RemoveDiskgroupTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.remove_diskgroup''' def setup_loader_modules(self): @@ -1605,6 +1617,7 @@ class RemoveCapacityFromDiskgroupTestCase(TestCase, LoaderModuleMockMixin): @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class ListClusterTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.list_cluster''' def setup_loader_modules(self): @@ -1694,6 +1707,7 @@ class ListClusterTestCase(TestCase, LoaderModuleMockMixin): @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class RenameDatastoreTestCase(TestCase, LoaderModuleMockMixin): '''Tests for salt.modules.vsphere.rename_datastore''' def setup_loader_modules(self): @@ -1837,7 +1851,7 @@ class _GetProxyTargetTestCase(TestCase, LoaderModuleMockMixin): 'connected via the ESXi host') def test_get_cluster_call(self): - #with patch('salt.modules.vsphere.get_proxy_type', + # with patch('salt.modules.vsphere.get_proxy_type', # MagicMock(return_value='esxcluster')): vsphere._get_proxy_target(self.mock_si) self.mock_get_datacenter.assert_called_once_with(self.mock_si, From 2dbee2b0fc27fc9ad85ec8f3a3a233d86124db71 Mon Sep 17 00:00:00 2001 From: Bike Dude Date: Wed, 4 Oct 2017 20:50:03 +0200 Subject: [PATCH 491/633] survey runner bugfix --- salt/runners/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/runners/survey.py b/salt/runners/survey.py index 591840060c..6607d8951c 100644 --- a/salt/runners/survey.py +++ b/salt/runners/survey.py @@ -171,7 +171,7 @@ def _get_pool_results(*args, **kwargs): # hash minion return values as a string for minion in sorted(minions): - digest = hashlib.sha256(str(minions[minion]).encode('utf-8')).hexdigest() + digest = hashlib.sha256(str(minions[minion]).encode(__salt_system_encoding__)).hexdigest() if digest not in ret: ret[digest] = {} ret[digest]['pool'] = [] From ae03b70b19317252df4167f26fd0fc2fca9ced46 Mon Sep 17 00:00:00 2001 From: Bike Dude Date: Wed, 4 Oct 2017 20:55:38 +0200 Subject: [PATCH 492/633] survey runner bugfix --- salt/runners/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/runners/survey.py b/salt/runners/survey.py index 591840060c..6607d8951c 100644 --- a/salt/runners/survey.py +++ b/salt/runners/survey.py @@ -171,7 +171,7 @@ def _get_pool_results(*args, **kwargs): # hash minion return values as a string for minion in sorted(minions): - digest = hashlib.sha256(str(minions[minion]).encode('utf-8')).hexdigest() + digest = hashlib.sha256(str(minions[minion]).encode(__salt_system_encoding__)).hexdigest() if digest not in ret: ret[digest] = {} ret[digest]['pool'] = [] From 15b8b8a9f46fbb1940ef6ca204584244dab0fbca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Santoro?= Date: Thu, 5 Oct 2017 00:49:17 +0200 Subject: [PATCH 493/633] Fix typo in salt-cloud scaleway documentation s/scalewa/scaleway --- doc/topics/cloud/scaleway.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/topics/cloud/scaleway.rst b/doc/topics/cloud/scaleway.rst index a8ab00057b..009e29515f 100644 --- a/doc/topics/cloud/scaleway.rst +++ b/doc/topics/cloud/scaleway.rst @@ -40,7 +40,7 @@ Set up an initial profile at /etc/salt/cloud.profiles or in the /etc/salt/cloud. .. code-block:: yaml - scalewa-ubuntu: + scaleway-ubuntu: provider: my-scaleway-config image: Ubuntu Trusty (14.04 LTS) From 3e41cb3a9bc066aa8f82c4d42f521db3f200070b Mon Sep 17 00:00:00 2001 From: JerzyX Drozdz Date: Thu, 5 Oct 2017 09:08:08 +0200 Subject: [PATCH 494/633] Added missing requisites to stateconf renderer --- salt/renderers/stateconf.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/salt/renderers/stateconf.py b/salt/renderers/stateconf.py index 7165b0cfbd..f5cdf68e88 100644 --- a/salt/renderers/stateconf.py +++ b/salt/renderers/stateconf.py @@ -363,7 +363,8 @@ def statelist(states_dict, sid_excludes=frozenset(['include', 'exclude'])): REQUISITES = set([ - 'require', 'require_in', 'watch', 'watch_in', 'use', 'use_in', 'listen', 'listen_in' + 'require', 'require_in', 'watch', 'watch_in', 'use', 'use_in', 'listen', 'listen_in', + 'onchanges', 'onchanges_in', 'onfail', 'onfail_in' ]) @@ -405,8 +406,8 @@ def rename_state_ids(data, sls, is_extend=False): del data[sid] -REQUIRE = set(['require', 'watch', 'listen']) -REQUIRE_IN = set(['require_in', 'watch_in', 'listen_in']) +REQUIRE = set(['require', 'watch', 'listen', 'onchanges', 'onfail']) +REQUIRE_IN = set(['require_in', 'watch_in', 'listen_in', 'onchanges_in', 'onfail_in']) EXTENDED_REQUIRE = {} EXTENDED_REQUIRE_IN = {} @@ -414,8 +415,8 @@ from itertools import chain # To avoid cycles among states when each state requires the one before it: -# explicit require/watch/listen can only contain states before it -# explicit require_in/watch_in/listen_in can only contain states after it +# explicit require/watch/listen/onchanges/onfail can only contain states before it +# explicit require_in/watch_in/listen_in/onchanges_in/onfail_in can only contain states after it def add_implicit_requires(data): def T(sid, state): # pylint: disable=C0103 @@ -449,7 +450,7 @@ def add_implicit_requires(data): for _, rstate, rsid in reqs: if T(rsid, rstate) in states_after: raise SaltRenderError( - 'State({0}) can\'t require/watch/listen a state({1}) defined ' + 'State({0}) can\'t require/watch/listen/onchanges/onfail a state({1}) defined ' 'after it!'.format(tag, T(rsid, rstate)) ) @@ -459,7 +460,7 @@ def add_implicit_requires(data): for _, rstate, rsid in reqs: if T(rsid, rstate) in states_before: raise SaltRenderError( - 'State({0}) can\'t require_in/watch_in/listen_in a state({1}) ' + 'State({0}) can\'t require_in/watch_in/listen_in/onchanges_in/onfail_in a state({1}) ' 'defined before it!'.format(tag, T(rsid, rstate)) ) @@ -571,7 +572,7 @@ def extract_state_confs(data, is_extend=False): if not is_extend and state_id in STATE_CONF_EXT: extend = STATE_CONF_EXT[state_id] - for requisite in 'require', 'watch', 'listen': + for requisite in 'require', 'watch', 'listen', 'onchanges', 'onfail': if requisite in extend: extend[requisite] += to_dict[state_id].get(requisite, []) to_dict[state_id].update(STATE_CONF_EXT[state_id]) From 6bac5dcf7ae72087de2ecc3a966b840cc4d39a8b Mon Sep 17 00:00:00 2001 From: JerzyX Drozdz Date: Thu, 5 Oct 2017 11:08:33 +0200 Subject: [PATCH 495/633] Fixed lint error (additional space after coma) --- salt/renderers/stateconf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/renderers/stateconf.py b/salt/renderers/stateconf.py index f5cdf68e88..18057d9360 100644 --- a/salt/renderers/stateconf.py +++ b/salt/renderers/stateconf.py @@ -364,7 +364,7 @@ def statelist(states_dict, sid_excludes=frozenset(['include', 'exclude'])): REQUISITES = set([ 'require', 'require_in', 'watch', 'watch_in', 'use', 'use_in', 'listen', 'listen_in', - 'onchanges', 'onchanges_in', 'onfail', 'onfail_in' + 'onchanges', 'onchanges_in', 'onfail', 'onfail_in' ]) From b99fd0ad64663c4b54c77723a4f05fa319162172 Mon Sep 17 00:00:00 2001 From: Kunal Ajay Bajpai Date: Thu, 5 Oct 2017 16:28:27 +0530 Subject: [PATCH 496/633] Create dirs if not exists --- salt/config/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/salt/config/__init__.py b/salt/config/__init__.py index e94e387885..8448fdee4d 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -3321,6 +3321,15 @@ def _cache_id(minion_id, cache_file): ''' Helper function, writes minion id to a cache file. ''' + path = os.path.dirname(cache_file) + try: + os.makedirs(path) + except OSError: + if os.path.isdir(path): + pass + else: + raise + try: with salt.utils.files.fopen(cache_file, 'w') as idf: idf.write(minion_id) From a9dc04fb7f136a0f5af7d72012743bdb66a5c135 Mon Sep 17 00:00:00 2001 From: Nicole Thomas Date: Thu, 5 Oct 2017 08:51:16 -0400 Subject: [PATCH 497/633] Lint: disable the unused-import check --- tests/unit/modules/test_vsphere.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index c3fc740b69..1d841896c2 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -27,7 +27,7 @@ from tests.support.mock import ( # Import Third Party Libs try: - from pyVmomi import vim, vmodl + from pyVmomi import vim, vmodl # pylint: disable=unused-import HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False From f62e8ca87f6fa46432b933a98d77d6593c083226 Mon Sep 17 00:00:00 2001 From: Richard Simko Date: Wed, 4 Oct 2017 18:00:54 +0200 Subject: [PATCH 498/633] Make sure volume exists before querying --- salt/cloud/clouds/ec2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cloud/clouds/ec2.py b/salt/cloud/clouds/ec2.py index 2c442d6059..dccfe2590f 100644 --- a/salt/cloud/clouds/ec2.py +++ b/salt/cloud/clouds/ec2.py @@ -1990,7 +1990,7 @@ def request_instance(vm_=None, call=None): params[termination_key] = str(set_del_root_vol_on_destroy).lower() # Use default volume type if not specified - if ex_blockdevicemappings and 'Ebs.VolumeType' not in ex_blockdevicemappings[dev_index]: + if ex_blockdevicemappings and dev_index < len(ex_blockdevicemappings) and 'Ebs.VolumeType' not in ex_blockdevicemappings[dev_index]: type_key = '{0}BlockDeviceMapping.{1}.Ebs.VolumeType'.format(spot_prefix, dev_index) params[type_key] = rd_type From 4a77560646b50d48270d59e7078d301e6eee5f84 Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Thu, 5 Oct 2017 09:51:38 -0600 Subject: [PATCH 499/633] Don't try to modify dict while looping through it --- salt/cloud/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cloud/__init__.py b/salt/cloud/__init__.py index 73327724ef..c70d9aff2e 100644 --- a/salt/cloud/__init__.py +++ b/salt/cloud/__init__.py @@ -234,7 +234,7 @@ class CloudClient(object): if a.get('provider', '')] if providers: _providers = opts.get('providers', {}) - for provider in list(_providers): + for provider in list(_providers).copy(): if provider not in providers: _providers.pop(provider) return opts From 7f004adca10e66cb1686cd2987b4b16d9d5a14aa Mon Sep 17 00:00:00 2001 From: Bike Dude Date: Thu, 5 Oct 2017 19:02:55 +0200 Subject: [PATCH 500/633] survey runner bugfix --- salt/runners/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/runners/survey.py b/salt/runners/survey.py index 6607d8951c..d7337352f9 100644 --- a/salt/runners/survey.py +++ b/salt/runners/survey.py @@ -171,7 +171,7 @@ def _get_pool_results(*args, **kwargs): # hash minion return values as a string for minion in sorted(minions): - digest = hashlib.sha256(str(minions[minion]).encode(__salt_system_encoding__)).hexdigest() + digest = hashlib.sha256(str(minions[minion]).encode(__salt_system_encoding__)).hexdigest() if digest not in ret: ret[digest] = {} ret[digest]['pool'] = [] From 29d8cf4f26bfaa8b646e3654858f1576aae429bd Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 5 Oct 2017 14:54:04 -0500 Subject: [PATCH 501/633] Fix typo in log message ``UNCH`` should have been ``UNC``. --- salt/fileserver/roots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/fileserver/roots.py b/salt/fileserver/roots.py index b3fe8ad074..379dec6e3f 100644 --- a/salt/fileserver/roots.py +++ b/salt/fileserver/roots.py @@ -373,7 +373,7 @@ def _file_lists(load, form): # join UNC and non-UNC paths, just assume the original # path. log.trace( - 'roots: %s is a UNCH path, using %s instead', + 'roots: %s is a UNC path, using %s instead', link_dest, abs_path ) link_dest = abs_path From 7d3ece7a3267972a2c06a41a05048cd88062e273 Mon Sep 17 00:00:00 2001 From: Nasenbaer Date: Thu, 5 Oct 2017 22:42:22 +0200 Subject: [PATCH 502/633] Adapted documentation of delete_on_termination parameter --- salt/states/boto_lc.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/salt/states/boto_lc.py b/salt/states/boto_lc.py index cfa7bbbb07..29925501db 100644 --- a/salt/states/boto_lc.py +++ b/salt/states/boto_lc.py @@ -186,10 +186,11 @@ def present( Indicates what volume type to use. Valid values are standard, io1, gp2. Default is standard. - delete_on_termination - Indicates whether to delete the volume on instance termination (true) or - not (false). Default is "None" which corresponds to not specified. - Amazon has different defaults for root and additional volumes. + delete_on_termination + Whether the volume should be explicitly marked for deletion when its instance is + terminated (True), or left around (False). If not provided, or None is explicitly passed, + the default AWS behaviour is used, which is True for ROOT volumes of instances, and + False for all others. iops For Provisioned IOPS (SSD) volumes only. The number of I/O operations per From 254dac7723d7a2d63d34f7f3b774626ec39a007c Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 5 Oct 2017 16:47:32 -0600 Subject: [PATCH 503/633] Fix `unit.utils.test_utils` for Windows Use os agnostic path seps Mock sys.platform to not return win --- tests/unit/utils/test_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 6e5958e112..245af7f45f 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -99,7 +99,7 @@ class UtilsTestCase(TestCase): def test_path_join(self): with patch('salt.utils.is_windows', return_value=False) as is_windows_mock: self.assertFalse(is_windows_mock.return_value) - expected_path = '/a/b/c/d' + expected_path = os.path.join(os.sep + 'a', 'b', 'c', 'd') ret = utils.path_join('/a/b/c', 'd') self.assertEqual(ret, expected_path) @@ -985,7 +985,8 @@ class UtilsTestCase(TestCase): ret = utils.daemonize_if({}) self.assertEqual(None, ret) - with patch('salt.utils.daemonize'): + with patch('salt.utils.daemonize'), \ + patch('sys.platform', 'not windows'): utils.daemonize_if({}) self.assertTrue(utils.daemonize.called) # pylint: enable=assignment-from-none From 9b717bdd88294c7d926aeb803fa0a8ca082b9836 Mon Sep 17 00:00:00 2001 From: "pierre.bellicini" Date: Fri, 6 Oct 2017 00:47:33 +0200 Subject: [PATCH 504/633] added cores,cpulimit and rootfs lxc params --- salt/cloud/clouds/proxmox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cloud/clouds/proxmox.py b/salt/cloud/clouds/proxmox.py index f208e88d1a..54ec4cea4d 100644 --- a/salt/cloud/clouds/proxmox.py +++ b/salt/cloud/clouds/proxmox.py @@ -723,7 +723,7 @@ def create_node(vm_, newid): newnode['hostname'] = vm_['name'] newnode['ostemplate'] = vm_['image'] - static_props = ('cpuunits', 'description', 'memory', 'onboot', 'net0', + static_props = ('cpuunits', 'cpulimit', 'rootfs', 'cores','description', 'memory', 'onboot', 'net0', 'password', 'nameserver', 'swap', 'storage', 'rootfs') for prop in _get_properties('/nodes/{node}/lxc', 'POST', From d4d639189bebc4c20cc7cc2131b1ad134cf73788 Mon Sep 17 00:00:00 2001 From: "pierre.bellicini" Date: Fri, 6 Oct 2017 00:53:44 +0200 Subject: [PATCH 505/633] fix missing space after cores in lxc section --- salt/cloud/clouds/proxmox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cloud/clouds/proxmox.py b/salt/cloud/clouds/proxmox.py index 54ec4cea4d..b5952c8958 100644 --- a/salt/cloud/clouds/proxmox.py +++ b/salt/cloud/clouds/proxmox.py @@ -723,7 +723,7 @@ def create_node(vm_, newid): newnode['hostname'] = vm_['name'] newnode['ostemplate'] = vm_['image'] - static_props = ('cpuunits', 'cpulimit', 'rootfs', 'cores','description', 'memory', 'onboot', 'net0', + static_props = ('cpuunits', 'cpulimit', 'rootfs', 'cores', 'description', 'memory', 'onboot', 'net0', 'password', 'nameserver', 'swap', 'storage', 'rootfs') for prop in _get_properties('/nodes/{node}/lxc', 'POST', From fefd28d896457ee12bc00247e84702c9170c8e15 Mon Sep 17 00:00:00 2001 From: Corvin Mcpherson Date: Thu, 5 Oct 2017 19:04:05 -0400 Subject: [PATCH 506/633] Add futureproofing to roster_defaults to support roster dictionary options --- salt/roster/cache.py | 3 ++- salt/roster/cloud.py | 3 ++- salt/roster/clustershell.py | 3 ++- salt/roster/flat.py | 3 ++- salt/roster/range.py | 5 +++-- salt/roster/scan.py | 3 ++- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/salt/roster/cache.py b/salt/roster/cache.py index cf4244ff3a..c0245de36a 100644 --- a/salt/roster/cache.py +++ b/salt/roster/cache.py @@ -98,6 +98,7 @@ from __future__ import absolute_import # Python import logging import re +import copy # Salt libs import salt.utils.minions @@ -151,7 +152,7 @@ def targets(tgt, tgt_type='glob', **kwargs): # pylint: disable=W0613 except LookupError: continue - minion_res = __opts__.get('roster_defaults', {}).copy() + minion_res = copy.deepcopy(__opts__.get('roster_defaults', {})) for param, order in roster_order.items(): if not isinstance(order, (list, tuple)): order = [order] diff --git a/salt/roster/cloud.py b/salt/roster/cloud.py index 2042a03200..f165846799 100644 --- a/salt/roster/cloud.py +++ b/salt/roster/cloud.py @@ -21,6 +21,7 @@ usually located at /etc/salt/cloud. For example, add the following: # Import python libs from __future__ import absolute_import import os +import copy # Import Salt libs import salt.loader @@ -63,7 +64,7 @@ def targets(tgt, tgt_type='glob', **kwargs): # pylint: disable=W0613 )) preferred_ip = extract_ipv4(roster_order, ip_list) - ret[minion_id] = __opts__.get('roster_defaults', {}).copy() + ret[minion_id] = copy.deepcopy(__opts__.get('roster_defaults', {})) ret[minion_id].update({'host': preferred_ip}) ssh_username = salt.utils.cloud.ssh_usernames(vm_, cloud_opts) diff --git a/salt/roster/clustershell.py b/salt/roster/clustershell.py index 1603c6dfba..e415d10bfc 100644 --- a/salt/roster/clustershell.py +++ b/salt/roster/clustershell.py @@ -15,6 +15,7 @@ When you want to use host globs for target matching, use ``--roster clustershell # Import python libs from __future__ import absolute_import import socket +import copy from salt.ext.six.moves import map # pylint: disable=import-error,redefined-builtin REQ_ERROR = None @@ -43,7 +44,7 @@ def targets(tgt, tgt_type='glob', **kwargs): for host, addr in host_addrs.items(): addr = str(addr) - ret[addr] = __opts__.get('roster_defaults', {}).copy() + ret[addr] = copy.deepcopy(__opts__.get('roster_defaults', {})) for port in ports: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/salt/roster/flat.py b/salt/roster/flat.py index 89f93549ed..ca1d2e9ff5 100644 --- a/salt/roster/flat.py +++ b/salt/roster/flat.py @@ -7,6 +7,7 @@ from __future__ import absolute_import # Import python libs import fnmatch import re +import copy # Try to import range from https://github.com/ytoolshed/range HAS_RANGE = False @@ -142,7 +143,7 @@ class RosterMatcher(object): ''' Return the configured ip ''' - ret = __opts__.get('roster_defaults', {}).copy() + ret = copy.deepcopy(__opts__.get('roster_defaults', {})) if isinstance(self.raw[minion], string_types): ret.update({'host': self.raw[minion]}) return ret diff --git a/salt/roster/range.py b/salt/roster/range.py index 14d6f14057..8e8922205b 100644 --- a/salt/roster/range.py +++ b/salt/roster/range.py @@ -13,6 +13,7 @@ When you want to use a range query for target matching, use ``--roster range``. ''' from __future__ import absolute_import import fnmatch +import copy import logging log = logging.getLogger(__name__) @@ -68,7 +69,7 @@ def targets(tgt, tgt_type='range', **kwargs): def target_range(tgt, hosts): ret = {} for host in hosts: - ret[host] = __opts__.get('roster_defaults', {}).copy() + ret[host] = copy.deepcopy(__opts__.get('roster_defaults', {})) ret[host].update({'host': host}) if __opts__.get('ssh_user'): ret[host].update({'user': __opts__['ssh_user']}) @@ -79,7 +80,7 @@ def target_glob(tgt, hosts): ret = {} for host in hosts: if fnmatch.fnmatch(tgt, host): - ret[host] = __opts__.get('roster_defaults', {}).copy() + ret[host] = copy.deepcopy(__opts__.get('roster_defaults', {})) ret[host].update({'host': host}) if __opts__.get('ssh_user'): ret[host].update({'user': __opts__['ssh_user']}) diff --git a/salt/roster/scan.py b/salt/roster/scan.py index 645cfcd9e0..fbb1d4bee2 100644 --- a/salt/roster/scan.py +++ b/salt/roster/scan.py @@ -7,6 +7,7 @@ Scan a netmask or ipaddr for open ssh ports from __future__ import absolute_import import socket import logging +import copy # Import salt libs import salt.utils.network @@ -55,7 +56,7 @@ class RosterMatcher(object): pass for addr in addrs: addr = str(addr) - ret[addr] = __opts__.get('roster_defaults', {}).copy() + ret[addr] = copy.deepcopy(__opts__.get('roster_defaults', {})) log.trace('Scanning host: {0}'.format(addr)) for port in ports: log.trace('Scanning port: {0}'.format(port)) From aa3309ef5920db2c5ed8b26b8e142da1c84c49e0 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 5 Oct 2017 13:37:27 -0500 Subject: [PATCH 507/633] Move several functions from salt.utils to salt.utils.user These functions are as follows: - salt.utils.get_user - salt.utils.get_uid - salt.utils.get_specific_user - salt.utils.chugid - salt.utils.chugid_and_umask - salt.utils.get_default_group - salt.utils.get_group_list - salt.utils.get_group_dict - salt.utils.get_gid_list - salt.utils.get_gid --- salt/auth/__init__.py | 7 +- salt/auth/pam.py | 4 +- salt/client/__init__.py | 5 +- salt/client/mixins.py | 5 +- salt/cloud/cli.py | 6 +- salt/config/__init__.py | 7 +- salt/crypt.py | 5 +- salt/daemons/masterapi.py | 8 +- salt/key.py | 9 +- salt/master.py | 7 +- salt/minion.py | 11 +- salt/modules/file.py | 3 +- salt/modules/mac_user.py | 3 +- salt/modules/npm.py | 13 +- salt/modules/pw_user.py | 5 +- salt/modules/rabbitmq.py | 68 +-- salt/modules/solaris_user.py | 5 +- salt/modules/useradd.py | 4 +- salt/modules/win_file.py | 4 +- salt/runner.py | 4 +- salt/states/user.py | 5 +- salt/utils/__init__.py | 521 ++++++++------------ salt/utils/gitfs.py | 6 +- salt/utils/parsers.py | 3 +- salt/utils/user.py | 341 +++++++++++++ salt/utils/verify.py | 10 +- tests/integration/modules/test_linux_acl.py | 7 +- tests/unit/modules/test_pw_user.py | 2 +- tests/unit/modules/test_useradd.py | 2 +- 29 files changed, 651 insertions(+), 429 deletions(-) create mode 100644 salt/utils/user.py diff --git a/salt/auth/__init__.py b/salt/auth/__init__.py index 2ab9ad9967..81a979dd24 100644 --- a/salt/auth/__init__.py +++ b/salt/auth/__init__.py @@ -33,6 +33,7 @@ import salt.utils.args import salt.utils.files import salt.utils.minions import salt.utils.versions +import salt.utils.user import salt.payload log = logging.getLogger(__name__) @@ -333,7 +334,7 @@ class LoadAuth(object): log.warning(error_msg) return False else: - if auth_key != key[salt.utils.get_user()]: + if auth_key != key[salt.utils.user.get_user()]: log.warning(error_msg) return False return True @@ -695,7 +696,7 @@ class Resolver(object): # Use current user if empty if 'username' in ret and not ret['username']: - ret['username'] = salt.utils.get_user() + ret['username'] = salt.utils.user.get_user() return ret @@ -766,7 +767,7 @@ class AuthUser(object): Returns True if the user is the same user as the one running this process and False if not. ''' - return self.user == salt.utils.get_user() + return self.user == salt.utils.user.get_user() def sudo_name(self): ''' diff --git a/salt/auth/pam.py b/salt/auth/pam.py index 8387e910f8..4a65cd1adb 100644 --- a/salt/auth/pam.py +++ b/salt/auth/pam.py @@ -42,7 +42,7 @@ from ctypes import c_void_p, c_uint, c_char_p, c_char, c_int from ctypes.util import find_library # Import Salt libs -import salt.utils # Can be removed once get_group_list is moved +import salt.utils.user from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin # Import 3rd-party libs @@ -214,4 +214,4 @@ def groups(username, *args, **kwargs): Uses system groups ''' - return salt.utils.get_group_list(username) + return salt.utils.user.get_group_list(username) diff --git a/salt/client/__init__.py b/salt/client/__init__.py index da32b4181d..834ee7577f 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -32,12 +32,13 @@ import salt.cache import salt.payload import salt.transport import salt.loader -import salt.utils +import salt.utils # Can be removed once ip_bracket is moved import salt.utils.args import salt.utils.event import salt.utils.files import salt.utils.minions import salt.utils.platform +import salt.utils.user import salt.utils.verify import salt.utils.versions import salt.utils.jid @@ -157,7 +158,7 @@ class LocalClient(object): ) self.opts = salt.config.client_config(c_path) self.serial = salt.payload.Serial(self.opts) - self.salt_user = salt.utils.get_specific_user() + self.salt_user = salt.utils.user.get_specific_user() self.skip_perm_errors = skip_perm_errors self.key = self.__read_master_key() self.auto_reconnect = auto_reconnect diff --git a/salt/client/mixins.py b/salt/client/mixins.py index f5fc1d22f7..c16ec72f53 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -16,7 +16,7 @@ import copy as pycopy # Import Salt libs import salt.exceptions import salt.minion -import salt.utils # Can be removed once daemonize, get_specific_user, format_call are moved +import salt.utils # Can be removed once daemonize, format_call are moved import salt.utils.args import salt.utils.doc import salt.utils.error @@ -27,6 +27,7 @@ import salt.utils.lazy import salt.utils.platform import salt.utils.process import salt.utils.state +import salt.utils.user import salt.utils.versions import salt.transport import salt.log.setup @@ -96,7 +97,7 @@ class ClientFuncsDict(collections.MutableMapping): async_pub = self.client._gen_async_pub(pub_data.get(u'__pub_jid')) - user = salt.utils.get_specific_user() + user = salt.utils.user.get_specific_user() return self.client._proc_function( key, low, diff --git a/salt/cloud/cli.py b/salt/cloud/cli.py index 7c85edce41..cd9b35fbbf 100644 --- a/salt/cloud/cli.py +++ b/salt/cloud/cli.py @@ -21,13 +21,13 @@ from salt.ext.six.moves import input # Import salt libs import salt.cloud -import salt.utils.cloud import salt.config import salt.defaults.exitcodes import salt.output import salt.syspaths as syspaths -import salt.utils +import salt.utils.cloud import salt.utils.parsers +import salt.utils.user from salt.exceptions import SaltCloudException, SaltCloudSystemExit from salt.utils.verify import check_user, verify_env, verify_files, verify_log @@ -48,7 +48,7 @@ class SaltCloud(salt.utils.parsers.SaltCloudParser): salt_master_user = self.config.get('user') if salt_master_user is None: - salt_master_user = salt.utils.get_user() + salt_master_user = salt.utils.user.get_user() if not check_user(salt_master_user): self.error( diff --git a/salt/config/__init__.py b/salt/config/__init__.py index f47b0ef373..2caf21a403 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -24,13 +24,14 @@ from salt.ext.six.moves.urllib.parse import urlparse # pylint: enable=import-error,no-name-in-module # Import salt libs -import salt.utils +import salt.utils # Can be removed once is_dictlist, ip_bracket are moved import salt.utils.dictupdate import salt.utils.files import salt.utils.network import salt.utils.path import salt.utils.platform import salt.utils.stringutils +import salt.utils.user import salt.utils.validate.path import salt.utils.xdg import salt.utils.yamlloader as yamlloader @@ -69,7 +70,7 @@ if salt.utils.platform.is_windows(): else: _DFLT_IPC_MODE = 'ipc' _MASTER_TRIES = 1 - _MASTER_USER = salt.utils.get_user() + _MASTER_USER = salt.utils.user.get_user() def _gather_buffer_space(): @@ -1145,7 +1146,7 @@ DEFAULT_MINION_OPTS = { 'always_verify_signature': False, 'master_sign_key_name': 'master_sign', 'syndic_finger': '', - 'user': salt.utils.get_user(), + 'user': salt.utils.user.get_user(), 'root_dir': salt.syspaths.ROOT_DIR, 'pki_dir': os.path.join(salt.syspaths.CONFIG_DIR, 'pki', 'minion'), 'id': '', diff --git a/salt/crypt.py b/salt/crypt.py index f5f945a7da..3d002a3847 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -50,13 +50,14 @@ import salt.defaults.exitcodes import salt.payload import salt.transport.client import salt.transport.frame -import salt.utils +import salt.utils # Can be removed when pem_finger, reinit_crypto are moved import salt.utils.decorators import salt.utils.event import salt.utils.files import salt.utils.rsax931 import salt.utils.sdb import salt.utils.stringutils +import salt.utils.user import salt.utils.verify import salt.version from salt.exceptions import ( @@ -858,7 +859,7 @@ class AsyncAuth(object): self.opts[u'master'] ) m_pub_fn = os.path.join(self.opts[u'pki_dir'], self.mpub) - uid = salt.utils.get_uid(self.opts.get(u'user', None)) + uid = salt.utils.user.get_uid(self.opts.get(u'user', None)) with salt.utils.files.fpopen(m_pub_fn, u'wb+', uid=uid) as wfh: wfh.write(salt.utils.stringutils.to_bytes(payload[u'pub_key'])) return True diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index 07b2738b79..56fb0c3368 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -15,7 +15,7 @@ import stat # Import salt libs import salt.crypt -import salt.utils +import salt.utils # Can be removed once check_whitelist_blacklist, expr_match, get_values_of_matching_keys are moved import salt.cache import salt.client import salt.payload @@ -29,6 +29,7 @@ import salt.key import salt.fileserver import salt.utils.args import salt.utils.atomicfile +import salt.utils.dictupdate import salt.utils.event import salt.utils.files import salt.utils.gitfs @@ -38,6 +39,7 @@ import salt.utils.gzip_util import salt.utils.jid import salt.utils.minions import salt.utils.platform +import salt.utils.user import salt.utils.verify from salt.defaults import DEFAULT_TARGET_DELIM from salt.pillar import git_pillar @@ -227,7 +229,7 @@ def access_keys(opts): acl_users = set(publisher_acl.keys()) if opts.get('user'): acl_users.add(opts['user']) - acl_users.add(salt.utils.get_user()) + acl_users.add(salt.utils.user.get_user()) for user in acl_users: log.info('Preparing the %s key for local communication', user) key = mk_key(opts, user) @@ -286,7 +288,7 @@ class AutoKey(object): pwnam = pwd.getpwnam(user) uid = pwnam[2] gid = pwnam[3] - groups = salt.utils.get_gid_list(user, include_default=False) + groups = salt.utils.user.get_gid_list(user, include_default=False) except KeyError: log.error( 'Failed to determine groups for user {0}. The user is not ' diff --git a/salt/key.py b/salt/key.py index 40881e479a..4d315a9e74 100644 --- a/salt/key.py +++ b/salt/key.py @@ -26,8 +26,9 @@ import salt.utils import salt.utils.args import salt.utils.event import salt.utils.files +import salt.utils.kinds import salt.utils.sdb -import salt.utils.kinds as kinds +import salt.utils.user # pylint: disable=import-error,no-name-in-module,redefined-builtin from salt.ext import six @@ -145,7 +146,7 @@ class KeyCLI(object): low.update(res) low[u'eauth'] = self.opts[u'eauth'] else: - low[u'user'] = salt.utils.get_specific_user() + low[u'user'] = salt.utils.user.get_specific_user() low[u'key'] = salt.utils.get_master_key(low[u'user'], self.opts, skip_perm_errors) self.auth = low @@ -364,7 +365,7 @@ class Key(object): def __init__(self, opts, io_loop=None): self.opts = opts kind = self.opts.get(u'__role', u'') # application kind - if kind not in kinds.APPL_KINDS: + if kind not in salt.utils.kinds.APPL_KINDS: emsg = (u"Invalid application kind = '{0}'.".format(kind)) log.error(emsg + u'\n') raise ValueError(emsg) @@ -1000,7 +1001,7 @@ class RaetKey(Key): cache.flush(u'{0}/{1}'.format(self.ACC, minion)) kind = self.opts.get(u'__role', u'') # application kind - if kind not in kinds.APPL_KINDS: + if kind not in salt.utils.kinds.APPL_KINDS: emsg = (u"Invalid application kind = '{0}'.".format(kind)) log.error(emsg + u'\n') raise ValueError(emsg) diff --git a/salt/master.py b/salt/master.py index 19e11002c2..af868ea134 100644 --- a/salt/master.py +++ b/salt/master.py @@ -77,6 +77,7 @@ import salt.utils.minions import salt.utils.platform import salt.utils.process import salt.utils.schedule +import salt.utils.user import salt.utils.verify import salt.utils.zeromq from salt.defaults import DEFAULT_TARGET_DELIM @@ -507,7 +508,7 @@ class Master(SMaster): Turn on the master server components ''' self._pre_flight() - log.info(u'salt-master is starting as user \'%s\'', salt.utils.get_user()) + log.info(u'salt-master is starting as user \'%s\'', salt.utils.user.get_user()) enable_sigusr1_handler() enable_sigusr2_handler() @@ -1703,7 +1704,7 @@ class ClearFuncs(object): if salt.auth.AuthUser(username).is_sudo(): username = self.opts.get(u'user', u'root') else: - username = salt.utils.get_user() + username = salt.utils.user.get_user() # Authorized. Do the job! try: @@ -1758,7 +1759,7 @@ class ClearFuncs(object): if salt.auth.AuthUser(username).is_sudo(): username = self.opts.get(u'user', u'root') else: - username = salt.utils.get_user() + username = salt.utils.user.get_user() # Authorized. Do the job! try: diff --git a/salt/minion.py b/salt/minion.py index 1760fb4c8e..82f7a9db3a 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -98,6 +98,7 @@ import salt.utils.minions import salt.utils.network import salt.utils.platform import salt.utils.schedule +import salt.utils.user import salt.utils.zeromq import salt.defaults.exitcodes import salt.cli.daemons @@ -1112,7 +1113,7 @@ class Minion(MinionBase): self.mod_opts = self._prep_mod_opts() self.matcher = Matcher(self.opts, self.functions) self.beacons = salt.beacons.Beacon(self.opts, self.functions) - uid = salt.utils.get_uid(user=self.opts.get(u'user', None)) + uid = salt.utils.user.get_uid(user=self.opts.get(u'user', None)) self.proc_dir = get_proc_dir(self.opts[u'cachedir'], uid=uid) self.schedule = salt.utils.schedule.Schedule( @@ -1445,7 +1446,7 @@ class Minion(MinionBase): if not hasattr(minion_instance, u'serial'): minion_instance.serial = salt.payload.Serial(opts) if not hasattr(minion_instance, u'proc_dir'): - uid = salt.utils.get_uid(user=opts.get(u'user', None)) + uid = salt.utils.user.get_uid(user=opts.get(u'user', None)) minion_instance.proc_dir = ( get_proc_dir(opts[u'cachedir'], uid=uid) ) @@ -2022,7 +2023,7 @@ class Minion(MinionBase): try: log.info( u'%s is starting as user \'%s\'', - self.__class__.__name__, salt.utils.get_user() + self.__class__.__name__, salt.utils.user.get_user() ) except Exception as err: # Only windows is allowed to fail here. See #3189. Log as debug in @@ -3321,7 +3322,7 @@ class ProxyMinion(Minion): self.mod_opts = self._prep_mod_opts() self.matcher = Matcher(self.opts, self.functions) self.beacons = salt.beacons.Beacon(self.opts, self.functions) - uid = salt.utils.get_uid(user=self.opts.get(u'user', None)) + uid = salt.utils.user.get_uid(user=self.opts.get(u'user', None)) self.proc_dir = get_proc_dir(self.opts[u'cachedir'], uid=uid) if self.connected and self.opts[u'pillar']: @@ -3467,7 +3468,7 @@ class ProxyMinion(Minion): if not hasattr(minion_instance, u'serial'): minion_instance.serial = salt.payload.Serial(opts) if not hasattr(minion_instance, u'proc_dir'): - uid = salt.utils.get_uid(user=opts.get(u'user', None)) + uid = salt.utils.user.get_uid(user=opts.get(u'user', None)) minion_instance.proc_dir = ( get_proc_dir(opts[u'cachedir'], uid=uid) ) diff --git a/salt/modules/file.py b/salt/modules/file.py index 20edba8460..0ca2a16dc6 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -59,6 +59,7 @@ import salt.utils.platform import salt.utils.stringutils import salt.utils.templates import salt.utils.url +import salt.utils.user from salt.exceptions import CommandExecutionError, MinionError, SaltInvocationError, get_error_message as _get_error_message from salt.utils.files import HASHES, HASHES_REVMAP @@ -289,7 +290,7 @@ def user_to_uid(user): salt '*' file.user_to_uid root ''' if user is None: - user = salt.utils.get_user() + user = salt.utils.user.get_user() try: if isinstance(user, int): return user diff --git a/salt/modules/mac_user.py b/salt/modules/mac_user.py index 9fe8d443bb..930ee36b16 100644 --- a/salt/modules/mac_user.py +++ b/salt/modules/mac_user.py @@ -27,6 +27,7 @@ import salt.utils import salt.utils.args import salt.utils.decorators.path import salt.utils.stringutils +import salt.utils.user from salt.utils.locales import sdecode as _sdecode from salt.exceptions import CommandExecutionError, SaltInvocationError @@ -454,7 +455,7 @@ def list_groups(name): salt '*' user.list_groups foo ''' - groups = [group for group in salt.utils.get_group_list(name)] + groups = [group for group in salt.utils.user.get_group_list(name)] return groups diff --git a/salt/modules/npm.py b/salt/modules/npm.py index 4a0bbf9c91..5e430f2636 100644 --- a/salt/modules/npm.py +++ b/salt/modules/npm.py @@ -15,6 +15,7 @@ import logging # Import salt libs import salt.utils import salt.utils.path +import salt.utils.user import salt.modules.cmdmod from salt.exceptions import CommandExecutionError from salt.utils.versions import LooseVersion as _LooseVersion @@ -156,7 +157,7 @@ def install(pkg=None, env = env or {} if runas: - uid = salt.utils.get_uid(runas) + uid = salt.utils.user.get_uid(runas) if uid: env.update({'SUDO_UID': b'{0}'.format(uid), 'SUDO_USER': b''}) @@ -235,7 +236,7 @@ def uninstall(pkg, dir=None, runas=None, env=None): env = env or {} if runas: - uid = salt.utils.get_uid(runas) + uid = salt.utils.user.get_uid(runas) if uid: env.update({'SUDO_UID': b'{0}'.format(uid), 'SUDO_USER': b''}) @@ -294,7 +295,7 @@ def list_(pkg=None, dir=None, runas=None, env=None, depth=None): env = env or {} if runas: - uid = salt.utils.get_uid(runas) + uid = salt.utils.user.get_uid(runas) if uid: env.update({'SUDO_UID': b'{0}'.format(uid), 'SUDO_USER': b''}) @@ -357,7 +358,7 @@ def cache_clean(path=None, runas=None, env=None, force=False): env = env or {} if runas: - uid = salt.utils.get_uid(runas) + uid = salt.utils.user.get_uid(runas) if uid: env.update({'SUDO_UID': b'{0}'.format(uid), 'SUDO_USER': b''}) @@ -404,7 +405,7 @@ def cache_list(path=None, runas=None, env=None): env = env or {} if runas: - uid = salt.utils.get_uid(runas) + uid = salt.utils.user.get_uid(runas) if uid: env.update({'SUDO_UID': b'{0}'.format(uid), 'SUDO_USER': b''}) @@ -444,7 +445,7 @@ def cache_path(runas=None, env=None): env = env or {} if runas: - uid = salt.utils.get_uid(runas) + uid = salt.utils.user.get_uid(runas) if uid: env.update({'SUDO_UID': b'{0}'.format(uid), 'SUDO_USER': b''}) diff --git a/salt/modules/pw_user.py b/salt/modules/pw_user.py index c63b97b7b1..3d1ca97856 100644 --- a/salt/modules/pw_user.py +++ b/salt/modules/pw_user.py @@ -47,9 +47,10 @@ except ImportError: from salt.ext import six # Import salt libs -import salt.utils +import salt.utils # Can be removed once is_true is moved import salt.utils.args import salt.utils.locales +import salt.utils.user from salt.exceptions import CommandExecutionError log = logging.getLogger(__name__) @@ -489,7 +490,7 @@ def list_groups(name): salt '*' user.list_groups foo ''' - return salt.utils.get_group_list(name) + return salt.utils.user.get_group_list(name) def list_users(): diff --git a/salt/modules/rabbitmq.py b/salt/modules/rabbitmq.py index 8eb3351bea..23fe2d3ea3 100644 --- a/salt/modules/rabbitmq.py +++ b/salt/modules/rabbitmq.py @@ -16,10 +16,10 @@ import random import string # Import salt libs -import salt.utils import salt.utils.itertools import salt.utils.path import salt.utils.platform +import salt.utils.user from salt.ext import six from salt.exceptions import SaltInvocationError from salt.ext.six.moves import range @@ -222,7 +222,7 @@ def list_users(runas=None): # Due to this, don't use a default value for # runas in Windows. if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'list_users', '-q'], runas=runas, @@ -245,7 +245,7 @@ def list_vhosts(runas=None): salt '*' rabbitmq.list_vhosts ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'list_vhosts', '-q'], runas=runas, @@ -265,7 +265,7 @@ def user_exists(name, runas=None): salt '*' rabbitmq.user_exists rabbit_user ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() return name in list_users(runas=runas) @@ -280,7 +280,7 @@ def vhost_exists(name, runas=None): salt '*' rabbitmq.vhost_exists rabbit_host ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() return name in list_vhosts(runas=runas) @@ -303,7 +303,7 @@ def add_user(name, password=None, runas=None): string.ascii_uppercase + string.digits) for x in range(15)) if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() if salt.utils.platform.is_windows(): # On Windows, if the password contains a special character @@ -351,7 +351,7 @@ def delete_user(name, runas=None): salt '*' rabbitmq.delete_user rabbit_user ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'delete_user', name], python_shell=False, @@ -372,7 +372,7 @@ def change_password(name, password, runas=None): salt '*' rabbitmq.change_password rabbit_user password ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() if salt.utils.platform.is_windows(): # On Windows, if the password contains a special character # such as '|', normal execution will fail. For example: @@ -408,7 +408,7 @@ def clear_password(name, runas=None): salt '*' rabbitmq.clear_password rabbit_user ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'clear_password', name], runas=runas, @@ -433,7 +433,7 @@ def check_password(name, password, runas=None): # try to get the rabbitmq-version - adapted from _get_rabbitmq_plugin if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() try: res = __salt__['cmd.run']([RABBITMQCTL, 'status'], runas=runas, python_shell=False) @@ -508,7 +508,7 @@ def add_vhost(vhost, runas=None): salt '*' rabbitmq add_vhost '' ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'add_vhost', vhost], runas=runas, @@ -529,7 +529,7 @@ def delete_vhost(vhost, runas=None): salt '*' rabbitmq.delete_vhost '' ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'delete_vhost', vhost], runas=runas, @@ -549,7 +549,7 @@ def set_permissions(vhost, user, conf='.*', write='.*', read='.*', runas=None): salt '*' rabbitmq.set_permissions 'myvhost' 'myuser' ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'set_permissions', '-p', vhost, user, conf, write, read], @@ -570,7 +570,7 @@ def list_permissions(vhost, runas=None): salt '*' rabbitmq.list_permissions '/myvhost' ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'list_permissions', '-q', '-p', vhost], runas=runas, @@ -590,7 +590,7 @@ def list_user_permissions(name, runas=None): salt '*' rabbitmq.list_user_permissions 'user'. ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'list_user_permissions', name, '-q'], runas=runas, @@ -609,7 +609,7 @@ def set_user_tags(name, tags, runas=None): salt '*' rabbitmq.set_user_tags 'myadmin' 'administrator' ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() if not isinstance(tags, (list, tuple)): tags = [tags] @@ -633,7 +633,7 @@ def status(runas=None): salt '*' rabbitmq.status ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'status'], runas=runas, @@ -653,7 +653,7 @@ def cluster_status(runas=None): salt '*' rabbitmq.cluster_status ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'cluster_status'], runas=runas, @@ -678,7 +678,7 @@ def join_cluster(host, user='rabbit', ram_node=None, runas=None): cmd.append('{0}@{1}'.format(user, host)) if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() stop_app(runas) res = __salt__['cmd.run_all'](cmd, runas=runas, python_shell=False) start_app(runas) @@ -697,7 +697,7 @@ def stop_app(runas=None): salt '*' rabbitmq.stop_app ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'stop_app'], runas=runas, @@ -717,7 +717,7 @@ def start_app(runas=None): salt '*' rabbitmq.start_app ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'start_app'], runas=runas, @@ -737,7 +737,7 @@ def reset(runas=None): salt '*' rabbitmq.reset ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'reset'], runas=runas, @@ -757,7 +757,7 @@ def force_reset(runas=None): salt '*' rabbitmq.force_reset ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'force_reset'], runas=runas, @@ -777,7 +777,7 @@ def list_queues(runas=None, *args): salt '*' rabbitmq.list_queues messages consumers ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() cmd = [RABBITMQCTL, 'list_queues', '-q'] cmd.extend(args) res = __salt__['cmd.run_all'](cmd, runas=runas, python_shell=False) @@ -799,7 +799,7 @@ def list_queues_vhost(vhost, runas=None, *args): salt '*' rabbitmq.list_queues messages consumers ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() cmd = [RABBITMQCTL, 'list_queues', '-q', '-p', vhost] cmd.extend(args) res = __salt__['cmd.run_all'](cmd, runas=runas, python_shell=False) @@ -822,7 +822,7 @@ def list_policies(vhost="/", runas=None): ''' ret = {} if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'list_policies', '-q', '-p', vhost], runas=runas, @@ -864,7 +864,7 @@ def set_policy(vhost, name, pattern, definition, priority=None, apply_to=None, r salt '*' rabbitmq.set_policy / HA '.*' '{"ha-mode":"all"}' ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() if isinstance(definition, dict): definition = json.dumps(definition) if not isinstance(definition, six.string_types): @@ -895,7 +895,7 @@ def delete_policy(vhost, name, runas=None): salt '*' rabbitmq.delete_policy / HA' ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() res = __salt__['cmd.run_all']( [RABBITMQCTL, 'clear_policy', '-p', vhost, name], runas=runas, @@ -917,7 +917,7 @@ def policy_exists(vhost, name, runas=None): salt '*' rabbitmq.policy_exists / HA ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() policies = list_policies(runas=runas) return bool(vhost in policies and name in policies[vhost]) @@ -933,7 +933,7 @@ def list_available_plugins(runas=None): salt '*' rabbitmq.list_available_plugins ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() cmd = [_get_rabbitmq_plugin(), 'list', '-m'] ret = __salt__['cmd.run_all'](cmd, python_shell=False, runas=runas) _check_response(ret) @@ -951,7 +951,7 @@ def list_enabled_plugins(runas=None): salt '*' rabbitmq.list_enabled_plugins ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() cmd = [_get_rabbitmq_plugin(), 'list', '-m', '-e'] ret = __salt__['cmd.run_all'](cmd, python_shell=False, runas=runas) _check_response(ret) @@ -969,7 +969,7 @@ def plugin_is_enabled(name, runas=None): salt '*' rabbitmq.plugin_is_enabled rabbitmq_plugin_name ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() return name in list_enabled_plugins(runas) @@ -984,7 +984,7 @@ def enable_plugin(name, runas=None): salt '*' rabbitmq.enable_plugin foo ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() cmd = [_get_rabbitmq_plugin(), 'enable', name] ret = __salt__['cmd.run_all'](cmd, runas=runas, python_shell=False) return _format_response(ret, 'Enabled') @@ -1001,7 +1001,7 @@ def disable_plugin(name, runas=None): salt '*' rabbitmq.disable_plugin foo ''' if runas is None and not salt.utils.platform.is_windows(): - runas = salt.utils.get_user() + runas = salt.utils.user.get_user() cmd = [_get_rabbitmq_plugin(), 'disable', name] ret = __salt__['cmd.run_all'](cmd, runas=runas, python_shell=False) return _format_response(ret, 'Disabled') diff --git a/salt/modules/solaris_user.py b/salt/modules/solaris_user.py index e52c82c137..276dcbe916 100644 --- a/salt/modules/solaris_user.py +++ b/salt/modules/solaris_user.py @@ -22,7 +22,8 @@ import copy import logging # Import salt libs -import salt.utils +import salt.utils # Can be removed once is_true is moved +import salt.utils.user from salt.ext import six from salt.exceptions import CommandExecutionError @@ -431,7 +432,7 @@ def list_groups(name): salt '*' user.list_groups foo ''' - return salt.utils.get_group_list(name) + return salt.utils.user.get_group_list(name) def list_users(): diff --git a/salt/modules/useradd.py b/salt/modules/useradd.py index 24900f3480..4f74d48cc4 100644 --- a/salt/modules/useradd.py +++ b/salt/modules/useradd.py @@ -19,10 +19,10 @@ import logging import copy # Import salt libs -import salt.utils # Can be removed when get_group_list is moved import salt.utils.files import salt.utils.decorators.path import salt.utils.locales +import salt.utils.user from salt.exceptions import CommandExecutionError # Import 3rd-party libs @@ -623,7 +623,7 @@ def list_groups(name): salt '*' user.list_groups foo ''' - return salt.utils.get_group_list(name) + return salt.utils.user.get_group_list(name) def list_users(): diff --git a/salt/modules/win_file.py b/salt/modules/win_file.py index 26fa0a3c1a..ba047e5da0 100644 --- a/salt/modules/win_file.py +++ b/salt/modules/win_file.py @@ -42,9 +42,9 @@ from salt.exceptions import CommandExecutionError, SaltInvocationError # pylint: enable=W0611 # Import salt libs -import salt.utils import salt.utils.path import salt.utils.platform +import salt.utils.user from salt.modules.file import (check_hash, # pylint: disable=W0611 directory_exists, get_managed, check_managed, check_managed_changes, source_list, @@ -496,7 +496,7 @@ def user_to_uid(user): salt '*' file.user_to_uid myusername ''' if user is None: - user = salt.utils.get_user() + user = salt.utils.user.get_user() return salt.utils.win_dacl.get_sid_string(user) diff --git a/salt/runner.py b/salt/runner.py index fa71e520ee..6a44cd5f2d 100644 --- a/salt/runner.py +++ b/salt/runner.py @@ -12,10 +12,10 @@ import logging import salt.exceptions import salt.loader import salt.minion -import salt.utils # Can be removed when get_specific_user is moved import salt.utils.args import salt.utils.event import salt.utils.files +import salt.utils.user from salt.client import mixins from salt.output import display_output from salt.utils.lazy import verify_fun @@ -230,7 +230,7 @@ class Runner(RunnerClient): low.update(res) low[u'eauth'] = self.opts[u'eauth'] else: - user = salt.utils.get_specific_user() + user = salt.utils.user.get_specific_user() if low[u'fun'] == u'state.orchestrate': low[u'kwarg'][u'orchestration_jid'] = async_pub[u'jid'] diff --git a/salt/states/user.py b/salt/states/user.py index 027b587802..1328960675 100644 --- a/salt/states/user.py +++ b/salt/states/user.py @@ -29,8 +29,9 @@ import os import logging # Import Salt libs -import salt.utils +import salt.utils # Can be removed once date_format is moved import salt.utils.platform +import salt.utils.user from salt.utils.locales import sdecode, sdecode_if_string # Import 3rd-party libs @@ -799,7 +800,7 @@ def absent(name, purge=False, force=False): ret['result'] = None ret['comment'] = 'User {0} set for removal'.format(name) return ret - beforegroups = set(salt.utils.get_group_list(name)) + beforegroups = set(salt.utils.user.get_group_list(name)) ret['result'] = __salt__['user.delete'](name, purge, force) aftergroups = set([g for g in beforegroups if __salt__['group.info'](g)]) if ret['result']: diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index c83bccdf6f..2284f5d19d 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -31,7 +31,6 @@ import time import types import string import subprocess -import getpass # Import 3rd-party libs from salt.ext import six @@ -66,7 +65,6 @@ except ImportError: try: import parsedatetime - HAS_PARSEDATETIME = True except ImportError: HAS_PARSEDATETIME = False @@ -77,26 +75,6 @@ try: except ImportError: HAS_WIN32API = False -try: - import salt.utils.win_functions - HAS_WIN_FUNCTIONS = True -except ImportError: - HAS_WIN_FUNCTIONS = False - -try: - import grp - HAS_GRP = True -except ImportError: - # grp is not available on windows - HAS_GRP = False - -try: - import pwd - HAS_PWD = True -except ImportError: - # pwd is not available on windows - HAS_PWD = False - try: import setproctitle HAS_SETPROCTITLE = True @@ -166,141 +144,6 @@ def get_context(template, line, num_lines=5, marker=None): return u'---\n{0}\n---'.format(u'\n'.join(buf)) -def get_user(): - ''' - Get the current user - ''' - if HAS_PWD: - return pwd.getpwuid(os.geteuid()).pw_name - elif HAS_WIN_FUNCTIONS and salt.utils.win_functions.HAS_WIN32: - return salt.utils.win_functions.get_current_user() - else: - raise CommandExecutionError("Required external libraries not found. Need 'pwd' or 'win32api") - - -@jinja_filter('get_uid') -def get_uid(user=None): - """ - Get the uid for a given user name. If no user given, - the current euid will be returned. If the user - does not exist, None will be returned. On - systems which do not support pwd or os.geteuid - it will return None. - """ - if not HAS_PWD: - result = None - elif user is None: - try: - result = os.geteuid() - except AttributeError: - result = None - else: - try: - u_struct = pwd.getpwnam(user) - except KeyError: - result = None - else: - result = u_struct.pw_uid - return result - - -def get_gid(group=None): - """ - Get the gid for a given group name. If no group given, - the current egid will be returned. If the group - does not exist, None will be returned. On - systems which do not support grp or os.getegid - it will return None. - """ - if grp is None: - result = None - elif group is None: - try: - result = os.getegid() - except AttributeError: - result = None - else: - try: - g_struct = grp.getgrnam(group) - except KeyError: - result = None - else: - result = g_struct.gr_gid - return result - - -def _win_user_token_is_admin(user_token): - ''' - Using the win32 api, determine if the user with token 'user_token' has - administrator rights. - - See MSDN entry here: - http://msdn.microsoft.com/en-us/library/aa376389(VS.85).aspx - ''' - class SID_IDENTIFIER_AUTHORITY(ctypes.Structure): - _fields_ = [ - ("byte0", ctypes.c_byte), - ("byte1", ctypes.c_byte), - ("byte2", ctypes.c_byte), - ("byte3", ctypes.c_byte), - ("byte4", ctypes.c_byte), - ("byte5", ctypes.c_byte), - ] - nt_authority = SID_IDENTIFIER_AUTHORITY() - nt_authority.byte5 = 5 - - SECURITY_BUILTIN_DOMAIN_RID = 0x20 - DOMAIN_ALIAS_RID_ADMINS = 0x220 - administrators_group = ctypes.c_void_p() - if ctypes.windll.advapi32.AllocateAndInitializeSid( - ctypes.byref(nt_authority), - 2, - SECURITY_BUILTIN_DOMAIN_RID, - DOMAIN_ALIAS_RID_ADMINS, - 0, 0, 0, 0, 0, 0, - ctypes.byref(administrators_group)) == 0: - raise Exception("AllocateAndInitializeSid failed") - - try: - is_admin = ctypes.wintypes.BOOL() - if ctypes.windll.advapi32.CheckTokenMembership( - user_token, - administrators_group, - ctypes.byref(is_admin)) == 0: - raise Exception("CheckTokenMembership failed") - return is_admin.value != 0 - - finally: - ctypes.windll.advapi32.FreeSid(administrators_group) - - -def _win_current_user_is_admin(): - ''' - ctypes.windll.shell32.IsUserAnAdmin() is intentionally avoided due to this - function being deprecated. - ''' - return _win_user_token_is_admin(0) - - -def get_specific_user(): - ''' - Get a user name for publishing. If you find the user is "root" attempt to be - more specific - ''' - import salt.utils.platform - user = get_user() - if salt.utils.platform.is_windows(): - if _win_current_user_is_admin(): - return 'sudo_{0}'.format(user) - else: - env_vars = ('SUDO_USER',) - if user == 'root': - for evar in env_vars: - if evar in os.environ: - return 'sudo_{0}'.format(os.environ[evar]) - return user - - def get_master_key(key_user, opts, skip_perm_errors=False): # Late import to avoid circular import. import salt.utils.files @@ -1859,107 +1702,6 @@ def repack_dictlist(data, return ret -def get_default_group(user): - if HAS_GRP is False or HAS_PWD is False: - # We don't work on platforms that don't have grp and pwd - # Just return an empty list - return None - return grp.getgrgid(pwd.getpwnam(user).pw_gid).gr_name - - -def get_group_list(user=None, include_default=True): - ''' - Returns a list of all of the system group names of which the user - is a member. - ''' - if HAS_GRP is False or HAS_PWD is False: - # We don't work on platforms that don't have grp and pwd - # Just return an empty list - return [] - group_names = None - ugroups = set() - if not isinstance(user, six.string_types): - raise Exception - if hasattr(os, 'getgrouplist'): - # Try os.getgrouplist, available in python >= 3.3 - log.trace('Trying os.getgrouplist for \'{0}\''.format(user)) - try: - group_names = [ - grp.getgrgid(grpid).gr_name for grpid in - os.getgrouplist(user, pwd.getpwnam(user).pw_gid) - ] - except Exception: - pass - else: - # Try pysss.getgrouplist - log.trace('Trying pysss.getgrouplist for \'{0}\''.format(user)) - try: - import pysss # pylint: disable=import-error - group_names = list(pysss.getgrouplist(user)) - except Exception: - pass - if group_names is None: - # Fall back to generic code - # Include the user's default group to behave like - # os.getgrouplist() and pysss.getgrouplist() do - log.trace('Trying generic group list for \'{0}\''.format(user)) - group_names = [g.gr_name for g in grp.getgrall() if user in g.gr_mem] - try: - default_group = get_default_group(user) - if default_group not in group_names: - group_names.append(default_group) - except KeyError: - # If for some reason the user does not have a default group - pass - ugroups.update(group_names) - if include_default is False: - # Historically, saltstack code for getting group lists did not - # include the default group. Some things may only want - # supplemental groups, so include_default=False omits the users - # default group. - try: - default_group = grp.getgrgid(pwd.getpwnam(user).pw_gid).gr_name - ugroups.remove(default_group) - except KeyError: - # If for some reason the user does not have a default group - pass - log.trace('Group list for user \'{0}\': \'{1}\''.format(user, sorted(ugroups))) - return sorted(ugroups) - - -def get_group_dict(user=None, include_default=True): - ''' - Returns a dict of all of the system groups as keys, and group ids - as values, of which the user is a member. - E.g.: {'staff': 501, 'sudo': 27} - ''' - if HAS_GRP is False or HAS_PWD is False: - # We don't work on platforms that don't have grp and pwd - # Just return an empty dict - return {} - group_dict = {} - group_names = get_group_list(user, include_default=include_default) - for group in group_names: - group_dict.update({group: grp.getgrnam(group).gr_gid}) - return group_dict - - -def get_gid_list(user=None, include_default=True): - ''' - Returns a list of all of the system group IDs of which the user - is a member. - ''' - if HAS_GRP is False or HAS_PWD is False: - # We don't work on platforms that don't have grp and pwd - # Just return an empty list - return [] - gid_list = [ - gid for (group, gid) in - six.iteritems(salt.utils.get_group_dict(user, include_default=include_default)) - ] - return sorted(set(gid_list)) - - def total_seconds(td): ''' Takes a timedelta and returns the total number of seconds @@ -1990,77 +1732,6 @@ def appendproctitle(name): setproctitle.setproctitle(setproctitle.getproctitle() + ' ' + name) -def chugid(runas): - ''' - Change the current process to belong to - the imputed user (and the groups he belongs to) - ''' - uinfo = pwd.getpwnam(runas) - supgroups = [] - supgroups_seen = set() - - # The line below used to exclude the current user's primary gid. - # However, when root belongs to more than one group - # this causes root's primary group of '0' to be dropped from - # his grouplist. On FreeBSD, at least, this makes some - # command executions fail with 'access denied'. - # - # The Python documentation says that os.setgroups sets only - # the supplemental groups for a running process. On FreeBSD - # this does not appear to be strictly true. - group_list = get_group_dict(runas, include_default=True) - if sys.platform == 'darwin': - group_list = dict((k, v) for k, v in six.iteritems(group_list) - if not k.startswith('_')) - for group_name in group_list: - gid = group_list[group_name] - if (gid not in supgroups_seen - and not supgroups_seen.add(gid)): - supgroups.append(gid) - - if os.getgid() != uinfo.pw_gid: - try: - os.setgid(uinfo.pw_gid) - except OSError as err: - raise CommandExecutionError( - 'Failed to change from gid {0} to {1}. Error: {2}'.format( - os.getgid(), uinfo.pw_gid, err - ) - ) - - # Set supplemental groups - if sorted(os.getgroups()) != sorted(supgroups): - try: - os.setgroups(supgroups) - except OSError as err: - raise CommandExecutionError( - 'Failed to set supplemental groups to {0}. Error: {1}'.format( - supgroups, err - ) - ) - - if os.getuid() != uinfo.pw_uid: - try: - os.setuid(uinfo.pw_uid) - except OSError as err: - raise CommandExecutionError( - 'Failed to change from uid {0} to {1}. Error: {2}'.format( - os.getuid(), uinfo.pw_uid, err - ) - ) - - -def chugid_and_umask(runas, umask): - ''' - Helper method for for subprocess.Popen to initialise uid/gid and umask - for the new process. - ''' - if runas is not None and runas != getpass.getuser(): - chugid(runas) - if umask is not None: - os.umask(umask) - - def human_size_to_bytes(human_size): ''' Convert human-readable units to bytes @@ -3297,3 +2968,195 @@ def check_state_result(running, recurse=False, highstate=None): return salt.utils.state.check_result( running, recurse=recurse, highstate=highstate ) + + +def get_user(): + ''' + Returns the current user + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_user\' detected. This function ' + 'has been moved to \'salt.utils.user.get_user\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.get_user() + + +def get_uid(user=None): + ''' + Get the uid for a given user name. If no user given, the current euid will + be returned. If the user does not exist, None will be returned. On systems + which do not support pwd or os.geteuid, None will be returned. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_uid\' detected. This function ' + 'has been moved to \'salt.utils.user.get_uid\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.get_uid(user) + + +def get_specific_user(): + ''' + Get a user name for publishing. If you find the user is "root" attempt to be + more specific by checking if Salt is being run as root via sudo. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_specific_user\' detected. This function ' + 'has been moved to \'salt.utils.user.get_specific_user\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.get_specific_user() + + +def chugid(runas): + ''' + Change the current process to belong to the specified user (and the groups + to which it belongs) + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.chugid\' detected. This function ' + 'has been moved to \'salt.utils.user.chugid\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.chugid(runas) + + +def chugid_and_umask(runas, umask): + ''' + Helper method for for subprocess.Popen to initialise uid/gid and umask + for the new process. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.chugid_and_umask\' detected. This function ' + 'has been moved to \'salt.utils.user.chugid_and_umask\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.chugid_and_umask(runas, umask) + + +def get_default_group(user): + ''' + Returns the specified user's default group. If the user doesn't exist, a + KeyError will be raised. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_default_group\' detected. This function ' + 'has been moved to \'salt.utils.user.get_default_group\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.get_default_group(user) + + +def get_group_list(user, include_default=True): + ''' + Returns a list of all of the system group names of which the user + is a member. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_group_list\' detected. This function ' + 'has been moved to \'salt.utils.user.get_group_list\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.get_group_list(user, include_default) + + +def get_group_dict(user=None, include_default=True): + ''' + Returns a dict of all of the system groups as keys, and group ids + as values, of which the user is a member. + E.g.: {'staff': 501, 'sudo': 27} + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_group_dict\' detected. This function ' + 'has been moved to \'salt.utils.user.get_group_dict\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.get_group_dict(user, include_default) + + +def get_gid_list(user, include_default=True): + ''' + Returns a list of all of the system group IDs of which the user + is a member. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_gid_list\' detected. This function ' + 'has been moved to \'salt.utils.user.get_gid_list\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.get_gid_list(user, include_default) + + +def get_gid(group=None): + ''' + Get the gid for a given group name. If no group given, the current egid + will be returned. If the group does not exist, None will be returned. On + systems which do not support grp or os.getegid it will return None. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.user + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_gid\' detected. This function ' + 'has been moved to \'salt.utils.user.get_gid\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.user.get_gid(group) diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py index a0b0b20ca1..b38cce6857 100644 --- a/salt/utils/gitfs.py +++ b/salt/utils/gitfs.py @@ -20,14 +20,16 @@ import time from datetime import datetime # Import salt libs -import salt.utils +import salt.utils # Can be removed once check_whitelist_blacklist, get_hash, is_bin_file, repack_dictlist are moved import salt.utils.configparser import salt.utils.files +import salt.utils.gzip_util import salt.utils.itertools import salt.utils.path import salt.utils.platform import salt.utils.stringutils import salt.utils.url +import salt.utils.user import salt.utils.versions import salt.fileserver from salt.config import DEFAULT_MASTER_OPTS as _DEFAULT_MASTER_OPTS @@ -1494,7 +1496,7 @@ class Pygit2(GitProvider): # https://github.com/libgit2/libgit2/issues/2122 if "Error stat'ing config file" not in str(exc): raise - home = pwd.getpwnam(salt.utils.get_user()).pw_dir + home = pwd.getpwnam(salt.utils.user.get_user()).pw_dir pygit2.settings.search_path[pygit2.GIT_CONFIG_LEVEL_GLOBAL] = home self.repo = pygit2.Repository(self.cachedir) except KeyError: diff --git a/salt/utils/parsers.py b/salt/utils/parsers.py index cf926a015e..7b2fca742a 100644 --- a/salt/utils/parsers.py +++ b/salt/utils/parsers.py @@ -36,6 +36,7 @@ import salt.utils.files import salt.utils.jid import salt.utils.kinds as kinds import salt.utils.platform +import salt.utils.user import salt.utils.xdg from salt.defaults import DEFAULT_TARGET_DELIM from salt.utils.validate.path import is_writeable @@ -772,7 +773,7 @@ class LogLevelMixIn(six.with_metaclass(MixInMeta, object)): # Since we're not be able to write to the log file or its parent # directory (if the log file does not exit), are we the same user # as the one defined in the configuration file? - current_user = salt.utils.get_user() + current_user = salt.utils.user.get_user() if self.config['user'] != current_user: # Yep, not the same user! # Is the current user in ACL? diff --git a/salt/utils/user.py b/salt/utils/user.py new file mode 100644 index 0000000000..134a5658ec --- /dev/null +++ b/salt/utils/user.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Import Python libs +import ctypes +import getpass +import logging +import os +import sys + +# Import Salt libs +import salt.utils.path +import salt.utils.platform +from salt.exceptions import CommandExecutionError +from salt.utils.decorators.jinja import jinja_filter + +# Import 3rd-party libs +from salt.ext import six + +# Conditional imports +try: + import pwd + HAS_PWD = True +except ImportError: + HAS_PWD = False + +try: + import grp + HAS_GRP = True +except ImportError: + HAS_GRP = False + +try: + import pysss + HAS_PYSSS = True +except ImportError: + HAS_PYSSS = False + +try: + import salt.utils.win_functions + HAS_WIN_FUNCTIONS = True +except ImportError: + HAS_WIN_FUNCTIONS = False + +log = logging.getLogger(__name__) + + +def get_user(): + ''' + Get the current user + ''' + if HAS_PWD: + return pwd.getpwuid(os.geteuid()).pw_name + elif HAS_WIN_FUNCTIONS and salt.utils.win_functions.HAS_WIN32: + return salt.utils.win_functions.get_current_user() + else: + raise CommandExecutionError( + 'Required external library (pwd or win32api) not installed') + + +@jinja_filter('get_uid') +def get_uid(user=None): + ''' + Get the uid for a given user name. If no user given, the current euid will + be returned. If the user does not exist, None will be returned. On systems + which do not support pwd or os.geteuid, None will be returned. + ''' + if not HAS_PWD: + return None + elif user is None: + try: + return os.geteuid() + except AttributeError: + return None + else: + try: + return pwd.getpwnam(user).pw_uid + except KeyError: + return None + + +def _win_user_token_is_admin(user_token): + ''' + Using the win32 api, determine if the user with token 'user_token' has + administrator rights. + + See MSDN entry here: + http://msdn.microsoft.com/en-us/library/aa376389(VS.85).aspx + ''' + class SID_IDENTIFIER_AUTHORITY(ctypes.Structure): + _fields_ = [ + ("byte0", ctypes.c_byte), + ("byte1", ctypes.c_byte), + ("byte2", ctypes.c_byte), + ("byte3", ctypes.c_byte), + ("byte4", ctypes.c_byte), + ("byte5", ctypes.c_byte), + ] + nt_authority = SID_IDENTIFIER_AUTHORITY() + nt_authority.byte5 = 5 + + SECURITY_BUILTIN_DOMAIN_RID = 0x20 + DOMAIN_ALIAS_RID_ADMINS = 0x220 + administrators_group = ctypes.c_void_p() + if ctypes.windll.advapi32.AllocateAndInitializeSid( + ctypes.byref(nt_authority), + 2, + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + ctypes.byref(administrators_group)) == 0: + raise Exception("AllocateAndInitializeSid failed") + + try: + is_admin = ctypes.wintypes.BOOL() + if ctypes.windll.advapi32.CheckTokenMembership( + user_token, + administrators_group, + ctypes.byref(is_admin)) == 0: + raise Exception("CheckTokenMembership failed") + return is_admin.value != 0 + + finally: + ctypes.windll.advapi32.FreeSid(administrators_group) + + +def _win_current_user_is_admin(): + ''' + ctypes.windll.shell32.IsUserAnAdmin() is intentionally avoided due to this + function being deprecated. + ''' + return _win_user_token_is_admin(0) + + +def get_specific_user(): + ''' + Get a user name for publishing. If you find the user is "root" attempt to be + more specific + ''' + user = get_user() + if salt.utils.platform.is_windows(): + if _win_current_user_is_admin(): + return 'sudo_{0}'.format(user) + else: + env_vars = ('SUDO_USER',) + if user == 'root': + for evar in env_vars: + if evar in os.environ: + return 'sudo_{0}'.format(os.environ[evar]) + return user + + +def chugid(runas): + ''' + Change the current process to belong to the specified user (and the groups + to which it belongs) + ''' + uinfo = pwd.getpwnam(runas) + supgroups = [] + supgroups_seen = set() + + # The line below used to exclude the current user's primary gid. + # However, when root belongs to more than one group + # this causes root's primary group of '0' to be dropped from + # his grouplist. On FreeBSD, at least, this makes some + # command executions fail with 'access denied'. + # + # The Python documentation says that os.setgroups sets only + # the supplemental groups for a running process. On FreeBSD + # this does not appear to be strictly true. + group_list = get_group_dict(runas, include_default=True) + if sys.platform == 'darwin': + group_list = dict((k, v) for k, v in six.iteritems(group_list) + if not k.startswith('_')) + for group_name in group_list: + gid = group_list[group_name] + if (gid not in supgroups_seen + and not supgroups_seen.add(gid)): + supgroups.append(gid) + + if os.getgid() != uinfo.pw_gid: + try: + os.setgid(uinfo.pw_gid) + except OSError as err: + raise CommandExecutionError( + 'Failed to change from gid {0} to {1}. Error: {2}'.format( + os.getgid(), uinfo.pw_gid, err + ) + ) + + # Set supplemental groups + if sorted(os.getgroups()) != sorted(supgroups): + try: + os.setgroups(supgroups) + except OSError as err: + raise CommandExecutionError( + 'Failed to set supplemental groups to {0}. Error: {1}'.format( + supgroups, err + ) + ) + + if os.getuid() != uinfo.pw_uid: + try: + os.setuid(uinfo.pw_uid) + except OSError as err: + raise CommandExecutionError( + 'Failed to change from uid {0} to {1}. Error: {2}'.format( + os.getuid(), uinfo.pw_uid, err + ) + ) + + +def chugid_and_umask(runas, umask): + ''' + Helper method for for subprocess.Popen to initialise uid/gid and umask + for the new process. + ''' + if runas is not None and runas != getpass.getuser(): + chugid(runas) + if umask is not None: + os.umask(umask) + + +def get_default_group(user): + ''' + Returns the specified user's default group. If the user doesn't exist, a + KeyError will be raised. + ''' + return grp.getgrgid(pwd.getpwnam(user).pw_gid).gr_name \ + if HAS_GRP and HAS_PWD \ + else None + + +def get_group_list(user, include_default=True): + ''' + Returns a list of all of the system group names of which the user + is a member. + ''' + if HAS_GRP is False or HAS_PWD is False: + return [] + group_names = None + ugroups = set() + if hasattr(os, 'getgrouplist'): + # Try os.getgrouplist, available in python >= 3.3 + log.trace('Trying os.getgrouplist for \'%s\'', user) + try: + group_names = [ + grp.getgrgid(grpid).gr_name for grpid in + os.getgrouplist(user, pwd.getpwnam(user).pw_gid) + ] + except Exception: + pass + elif HAS_PYSSS: + # Try pysss.getgrouplist + log.trace('Trying pysss.getgrouplist for \'%s\'', user) + try: + group_names = list(pysss.getgrouplist(user)) + except Exception: + pass + + if group_names is None: + # Fall back to generic code + # Include the user's default group to match behavior of + # os.getgrouplist() and pysss.getgrouplist() + log.trace('Trying generic group list for \'%s\'', user) + group_names = [g.gr_name for g in grp.getgrall() if user in g.gr_mem] + try: + default_group = get_default_group(user) + if default_group not in group_names: + group_names.append(default_group) + except KeyError: + # If for some reason the user does not have a default group + pass + + if group_names is not None: + ugroups.update(group_names) + + if include_default is False: + # Historically, saltstack code for getting group lists did not + # include the default group. Some things may only want + # supplemental groups, so include_default=False omits the users + # default group. + try: + default_group = grp.getgrgid(pwd.getpwnam(user).pw_gid).gr_name + ugroups.remove(default_group) + except KeyError: + # If for some reason the user does not have a default group + pass + log.trace('Group list for user \'%s\': %s', user, sorted(ugroups)) + return sorted(ugroups) + + +def get_group_dict(user=None, include_default=True): + ''' + Returns a dict of all of the system groups as keys, and group ids + as values, of which the user is a member. + E.g.: {'staff': 501, 'sudo': 27} + ''' + if HAS_GRP is False or HAS_PWD is False: + return {} + group_dict = {} + group_names = get_group_list(user, include_default=include_default) + for group in group_names: + group_dict.update({group: grp.getgrnam(group).gr_gid}) + return group_dict + + +def get_gid_list(user, include_default=True): + ''' + Returns a list of all of the system group IDs of which the user + is a member. + ''' + if HAS_GRP is False or HAS_PWD is False: + return [] + gid_list = list( + six.itervalues( + get_group_dict(user, include_default=include_default) + ) + ) + return sorted(set(gid_list)) + + +def get_gid(group=None): + ''' + Get the gid for a given group name. If no group given, the current egid + will be returned. If the group does not exist, None will be returned. On + systems which do not support grp or os.getegid it will return None. + ''' + if not HAS_GRP: + return None + if group is None: + try: + return os.getegid() + except AttributeError: + return None + else: + try: + return grp.getgrnam(group).gr_gid + except KeyError: + return None diff --git a/salt/utils/verify.py b/salt/utils/verify.py index 1559224e4d..24e0decfc1 100644 --- a/salt/utils/verify.py +++ b/salt/utils/verify.py @@ -27,9 +27,9 @@ from salt.log.setup import LOG_LEVELS from salt.exceptions import SaltClientError, SaltSystemExit, \ CommandExecutionError import salt.defaults.exitcodes -import salt.utils # Can be removed once get_jid_list and get_user are moved import salt.utils.files import salt.utils.platform +import salt.utils.user log = logging.getLogger(__name__) @@ -206,7 +206,7 @@ def verify_env(dirs, user, permissive=False, pki_dir='', skip_extra=False): pwnam = pwd.getpwnam(user) uid = pwnam[2] gid = pwnam[3] - groups = salt.utils.get_gid_list(user, include_default=False) + groups = salt.utils.user.get_gid_list(user, include_default=False) except KeyError: err = ('Failed to prepare the Salt environment for user ' @@ -302,7 +302,7 @@ def check_user(user): ''' if salt.utils.platform.is_windows(): return True - if user == salt.utils.get_user(): + if user == salt.utils.user.get_user(): return True import pwd # after confirming not running Windows try: @@ -311,7 +311,7 @@ def check_user(user): if hasattr(os, 'initgroups'): os.initgroups(user, pwuser.pw_gid) # pylint: disable=minimum-python-version else: - os.setgroups(salt.utils.get_gid_list(user, include_default=False)) + os.setgroups(salt.utils.user.get_gid_list(user, include_default=False)) os.setgid(pwuser.pw_gid) os.setuid(pwuser.pw_uid) @@ -383,7 +383,7 @@ def check_path_traversal(path, user='root', skip_perm_errors=False): if not os.path.exists(tpath): msg += ' Path does not exist.' else: - current_user = salt.utils.get_user() + current_user = salt.utils.user.get_user() # Make the error message more intelligent based on how # the user invokes salt-call or whatever other script. if user != current_user: diff --git a/tests/integration/modules/test_linux_acl.py b/tests/integration/modules/test_linux_acl.py index 8f4e448ed2..07b7f5142b 100644 --- a/tests/integration/modules/test_linux_acl.py +++ b/tests/integration/modules/test_linux_acl.py @@ -12,9 +12,8 @@ from tests.support.mixins import AdaptedConfigurationTestCaseMixin from tests.support.helpers import skip_if_binaries_missing # Import salt libs -import salt.utils import salt.utils.files -# from salt.modules import linux_acl as acl +import salt.utils.user # Acl package should be installed to test linux_acl module @@ -60,8 +59,8 @@ class LinuxAclModuleTest(ModuleCase, AdaptedConfigurationTestCaseMixin): def test_getfacl_w_single_file_without_acl(self): ret = self.run_function('acl.getfacl', arg=[self.myfile]) - user = salt.utils.get_user() - group = salt.utils.get_default_group(user) + user = salt.utils.user.get_user() + group = salt.utils.user.get_default_group(user) self.maxDiff = None self.assertEqual( ret, diff --git a/tests/unit/modules/test_pw_user.py b/tests/unit/modules/test_pw_user.py index b22196f384..1b9fcc0903 100644 --- a/tests/unit/modules/test_pw_user.py +++ b/tests/unit/modules/test_pw_user.py @@ -316,7 +316,7 @@ class PwUserTestCase(TestCase, LoaderModuleMockMixin): ''' mock_group = 'saltgroup' - with patch('salt.utils.get_group_list', MagicMock(return_value=[mock_group])): + with patch('salt.utils.user.get_group_list', MagicMock(return_value=[mock_group])): self.assertEqual(pw_user.list_groups('name'), [mock_group]) def test_list_users(self): diff --git a/tests/unit/modules/test_useradd.py b/tests/unit/modules/test_useradd.py index 7fd457425f..a7744899d2 100644 --- a/tests/unit/modules/test_useradd.py +++ b/tests/unit/modules/test_useradd.py @@ -355,7 +355,7 @@ class UserAddTestCase(TestCase, LoaderModuleMockMixin): ''' Test if it return a list of groups the named user belongs to ''' - with patch('salt.utils.get_group_list', MagicMock(return_value='Salt')): + with patch('salt.utils.user.get_group_list', MagicMock(return_value='Salt')): self.assertEqual(useradd.list_groups('name'), 'Salt') # 'list_users' function tests: 1 From 0cf411671eec7de2c8f259a1e6f1f071614e76b0 Mon Sep 17 00:00:00 2001 From: John Kristensen Date: Wed, 4 Oct 2017 15:50:50 +1100 Subject: [PATCH 508/633] Check the `key_text` option doesn't conflict with any other options There are checks for `key_url` and `keyid`/`keyserver`, so we should probably also have a check for `key_text` as well. --- salt/states/pkgrepo.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/salt/states/pkgrepo.py b/salt/states/pkgrepo.py index 0c47abae1d..3c1a343f39 100644 --- a/salt/states/pkgrepo.py +++ b/salt/states/pkgrepo.py @@ -312,6 +312,16 @@ def managed(name, ppa=None, **kwargs): ret['result'] = False ret['comment'] = 'You may not use both "keyid"/"keyserver" and ' \ '"key_url" argument.' + + if 'key_text' in kwargs and ('keyid' in kwargs or 'keyserver' in kwargs): + ret['result'] = False + ret['comment'] = 'You may not use both "keyid"/"keyserver" and ' \ + '"key_text" argument.' + if 'key_text' in kwargs and ('key_url' in kwargs): + ret['result'] = False + ret['comment'] = 'You may not use both "key_url" and ' \ + '"key_text" argument.' + if 'repo' in kwargs: ret['result'] = False ret['comment'] = ('\'repo\' is not a supported argument for this ' From b7edddd215f52dfc59525bf761691b53755cce6e Mon Sep 17 00:00:00 2001 From: John Kristensen Date: Fri, 6 Oct 2017 12:20:46 +1100 Subject: [PATCH 509/633] Document `key_text` option in pkgrepo state The `key_text` option was added a while back but the documentation for it was not included. It would be a good idea to have all options documented properly. --- salt/states/pkgrepo.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/salt/states/pkgrepo.py b/salt/states/pkgrepo.py index 3c1a343f39..ac6340431f 100644 --- a/salt/states/pkgrepo.py +++ b/salt/states/pkgrepo.py @@ -259,6 +259,14 @@ def managed(name, ppa=None, **kwargs): Use either ``keyid``/``keyserver`` or ``key_url``, but not both. + key_text + The string representation of the GPG key to install. + + .. note:: + + Use either ``keyid``/``keyserver``, ``key_url``, or ``key_text`` but + not more than one method. + consolidate : False If set to ``True``, this will consolidate all sources definitions to the sources.list file, cleanup the now unused files, consolidate components From 08f59a0962eda069c45024ca54ab99081a155524 Mon Sep 17 00:00:00 2001 From: "pierre.bellicini" Date: Fri, 6 Oct 2017 00:53:44 +0200 Subject: [PATCH 510/633] fix missing space after cores in lxc section modified: salt/cloud/clouds/proxmox.py --- salt/cloud/clouds/proxmox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cloud/clouds/proxmox.py b/salt/cloud/clouds/proxmox.py index 54ec4cea4d..b5952c8958 100644 --- a/salt/cloud/clouds/proxmox.py +++ b/salt/cloud/clouds/proxmox.py @@ -723,7 +723,7 @@ def create_node(vm_, newid): newnode['hostname'] = vm_['name'] newnode['ostemplate'] = vm_['image'] - static_props = ('cpuunits', 'cpulimit', 'rootfs', 'cores','description', 'memory', 'onboot', 'net0', + static_props = ('cpuunits', 'cpulimit', 'rootfs', 'cores', 'description', 'memory', 'onboot', 'net0', 'password', 'nameserver', 'swap', 'storage', 'rootfs') for prop in _get_properties('/nodes/{node}/lxc', 'POST', From 4ce20bb2f71ebea339feeeaf07c6cf1af9861a30 Mon Sep 17 00:00:00 2001 From: Arthur Lutz Date: Fri, 6 Oct 2017 13:10:34 +0200 Subject: [PATCH 511/633] [log/sentry] avoid KeyError: 'SENTRY_PROJECT' if compute_scope is not present on transport registry, we can't update options dictionnary with empty dsn_config dictionary fix #43949 --- salt/log/handlers/sentry_mod.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/salt/log/handlers/sentry_mod.py b/salt/log/handlers/sentry_mod.py index 496c9929a1..229ee02bb1 100644 --- a/salt/log/handlers/sentry_mod.py +++ b/salt/log/handlers/sentry_mod.py @@ -128,12 +128,12 @@ def setup_handlers(): callable(transport_registry.compute_scope)): conf_extras = transport_registry.compute_scope(url, dsn_config) dsn_config.update(conf_extras) - options.update({ - 'project': dsn_config['SENTRY_PROJECT'], - 'servers': dsn_config['SENTRY_SERVERS'], - 'public_key': dsn_config['SENTRY_PUBLIC_KEY'], - 'secret_key': dsn_config['SENTRY_SECRET_KEY'] - }) + options.update({ + 'project': dsn_config['SENTRY_PROJECT'], + 'servers': dsn_config['SENTRY_SERVERS'], + 'public_key': dsn_config['SENTRY_PUBLIC_KEY'], + 'secret_key': dsn_config['SENTRY_SECRET_KEY'] + }) except ValueError as exc: log.info( 'Raven failed to parse the configuration provided ' From 89200ff28e9183f0923563321f98fc8efb6bd57d Mon Sep 17 00:00:00 2001 From: JerzyX Drozdz Date: Fri, 6 Oct 2017 13:18:16 +0200 Subject: [PATCH 512/633] rebase from 2017.7.2 --- salt/renderers/stateconf.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/salt/renderers/stateconf.py b/salt/renderers/stateconf.py index fe41692e3a..fe66ade752 100644 --- a/salt/renderers/stateconf.py +++ b/salt/renderers/stateconf.py @@ -363,7 +363,8 @@ def statelist(states_dict, sid_excludes=frozenset(['include', 'exclude'])): REQUISITES = set([ - 'require', 'require_in', 'watch', 'watch_in', 'use', 'use_in', 'listen', 'listen_in' + 'require', 'require_in', 'watch', 'watch_in', 'use', 'use_in', 'listen', 'listen_in', + 'onchanges', 'onchanges_in', 'onfail', 'onfail_in' ]) @@ -405,8 +406,8 @@ def rename_state_ids(data, sls, is_extend=False): del data[sid] -REQUIRE = set(['require', 'watch', 'listen']) -REQUIRE_IN = set(['require_in', 'watch_in', 'listen_in']) +REQUIRE = set(['require', 'watch', 'listen', 'onchanges', 'onfail']) +REQUIRE_IN = set(['require_in', 'watch_in', 'listen_in', 'onchanges_in', 'onfail_in']) EXTENDED_REQUIRE = {} EXTENDED_REQUIRE_IN = {} @@ -414,8 +415,8 @@ from itertools import chain # To avoid cycles among states when each state requires the one before it: -# explicit require/watch/listen can only contain states before it -# explicit require_in/watch_in/listen_in can only contain states after it +# explicit require/watch/listen/onchanges/onfail can only contain states before it +# explicit require_in/watch_in/listen_in/onchanges_in/onfail_in can only contain states after it def add_implicit_requires(data): def T(sid, state): # pylint: disable=C0103 @@ -449,7 +450,7 @@ def add_implicit_requires(data): for _, rstate, rsid in reqs: if T(rsid, rstate) in states_after: raise SaltRenderError( - 'State({0}) can\'t require/watch/listen a state({1}) defined ' + 'State({0}) can\'t require/watch/listen/onchanges/onfail a state({1}) defined ' 'after it!'.format(tag, T(rsid, rstate)) ) @@ -459,7 +460,7 @@ def add_implicit_requires(data): for _, rstate, rsid in reqs: if T(rsid, rstate) in states_before: raise SaltRenderError( - 'State({0}) can\'t require_in/watch_in/listen_in a state({1}) ' + 'State({0}) can\'t require_in/watch_in/listen_in/onchanges_in/onfail_in a state({1}) ' 'defined before it!'.format(tag, T(rsid, rstate)) ) @@ -571,7 +572,7 @@ def extract_state_confs(data, is_extend=False): if not is_extend and state_id in STATE_CONF_EXT: extend = STATE_CONF_EXT[state_id] - for requisite in 'require', 'watch', 'listen': + for requisite in 'require', 'watch', 'listen', 'onchanges', 'onfail': if requisite in extend: extend[requisite] += to_dict[state_id].get(requisite, []) to_dict[state_id].update(STATE_CONF_EXT[state_id]) From 63961cc7d9fd6a0fdbcb06972afd8b6c0659c91c Mon Sep 17 00:00:00 2001 From: Massimiliano Torromeo Date: Wed, 27 Sep 2017 11:14:40 +0200 Subject: [PATCH 513/633] Copy git ssh-id-wrapper to /tmp only if necessary (Fixes #10582, Fixes #19532) This adds a check that only copies the ssh wrapper to a temporary location if git is to be run by a specific user and at the same time the predefined wrapper file is not executable by "others" by verifying every path part for the others executable bit. By doing this the temp file is kept only as a last resort which should work with salt-ssh as per bug #19532, while avoiding a needless copy on /tmp which could be potentially mounted with noexec as per bug #10582. --- salt/modules/git.py | 52 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/salt/modules/git.py b/salt/modules/git.py index d52922ea34..7cd2acb704 100644 --- a/salt/modules/git.py +++ b/salt/modules/git.py @@ -9,6 +9,7 @@ import copy import logging import os import re +import stat # Import salt libs import salt.utils @@ -118,6 +119,22 @@ def _expand_path(cwd, user): return os.path.join(os.path.expanduser(to_expand), str(cwd)) +def _path_is_executable_others(path): + ''' + Check every part of path for executable permission + ''' + prevpath = None + while path and path != prevpath: + try: + if not os.stat(path).st_mode & stat.S_IXOTH: + return False + except OSError: + return False + prevpath = path + path, _ = os.path.split(path) + return True + + def _format_opts(opts): ''' Common code to inspect opts and split them if necessary @@ -217,11 +234,12 @@ def _git_run(command, cwd=None, user=None, password=None, identity=None, } # copy wrapper to area accessible by ``runas`` user - # currently no suppport in windows for wrapping git ssh + # currently no support in windows for wrapping git ssh ssh_id_wrapper = os.path.join( salt.utils.templates.TEMPLATE_DIRNAME, 'git/ssh-id-wrapper' ) + tmp_ssh_wrapper = None if salt.utils.platform.is_windows(): for suffix in ('', ' (x86)'): ssh_exe = ( @@ -238,12 +256,14 @@ def _git_run(command, cwd=None, user=None, password=None, identity=None, # Use the windows batch file instead of the bourne shell script ssh_id_wrapper += '.bat' env['GIT_SSH'] = ssh_id_wrapper + elif not user or _path_is_executable_others(ssh_id_wrapper): + env['GIT_SSH'] = ssh_id_wrapper else: - tmp_file = salt.utils.files.mkstemp() - salt.utils.files.copyfile(ssh_id_wrapper, tmp_file) - os.chmod(tmp_file, 0o500) - os.chown(tmp_file, __salt__['file.user_to_uid'](user), -1) - env['GIT_SSH'] = tmp_file + tmp_ssh_wrapper = salt.utils.files.mkstemp() + salt.utils.files.copyfile(ssh_id_wrapper, tmp_ssh_wrapper) + os.chmod(tmp_ssh_wrapper, 0o500) + os.chown(tmp_ssh_wrapper, __salt__['file.user_to_uid'](user), -1) + env['GIT_SSH'] = tmp_ssh_wrapper if 'salt-call' not in _salt_cli \ and __salt__['ssh.key_is_encrypted'](id_file): @@ -273,13 +293,25 @@ def _git_run(command, cwd=None, user=None, password=None, identity=None, redirect_stderr=redirect_stderr, **kwargs) finally: - if not salt.utils.platform.is_windows() and 'GIT_SSH' in env: - os.remove(env['GIT_SSH']) + # Cleanup the temporary ssh wrapper file + try: + __salt__['file.remove'](tmp_ssh_wrapper) + log.debug('Removed ssh wrapper file %s', tmp_ssh_wrapper) + except AttributeError: + # No wrapper was used + pass + except (SaltInvocationError, CommandExecutionError) as exc: + log.warning('Failed to remove ssh wrapper file %s: %s', tmp_ssh_wrapper, exc) # Cleanup the temporary identity file - if tmp_identity_file and os.path.exists(tmp_identity_file): - log.debug('Removing identity file {0}'.format(tmp_identity_file)) + try: __salt__['file.remove'](tmp_identity_file) + log.debug('Removed identity file %s', tmp_identity_file) + except AttributeError: + # No identify file was used + pass + except (SaltInvocationError, CommandExecutionError) as exc: + log.warning('Failed to remove identity file %s: %s', tmp_identity_file, exc) # If the command was successful, no need to try additional IDs if result['retcode'] == 0: From 11e577b9cedd908ddc3a5c79003c6d532d2f4679 Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Fri, 6 Oct 2017 07:55:04 -0600 Subject: [PATCH 514/633] Add deps message to __virtual__() --- salt/cloud/clouds/azurearm.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/salt/cloud/clouds/azurearm.py b/salt/cloud/clouds/azurearm.py index 2b79255f52..81d6557e41 100644 --- a/salt/cloud/clouds/azurearm.py +++ b/salt/cloud/clouds/azurearm.py @@ -147,7 +147,12 @@ def __virtual__(): return False if get_dependencies() is False: - return False + return ( + False, + 'The following dependencies are required to use the AzureARM driver: ' + 'Microsoft Azure SDK for Python >= 2.0rc5, ' + 'Microsoft Azure Storage SDK for Python >= 0.32, ' + 'Microsoft Azure CLI >= 2.0.12' global cache # pylint: disable=global-statement,invalid-name cache = salt.cache.Cache(__opts__) From 44bc91bb986df9b6b04590326a57d8e84985cb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Fri, 6 Oct 2017 17:12:15 +0100 Subject: [PATCH 515/633] Enable '--with-salt-version' parameter for setup.py script --- setup.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6fcbbfce14..d5eacf9a79 100755 --- a/setup.py +++ b/setup.py @@ -178,17 +178,22 @@ class WriteSaltVersion(Command): ''' def run(self): - if not os.path.exists(SALT_VERSION_HARDCODED): + if not os.path.exists(SALT_VERSION_HARDCODED) or self.distribution.with_salt_version: # Write the version file if getattr(self.distribution, 'salt_version_hardcoded_path', None) is None: print('This command is not meant to be called on it\'s own') exit(1) + if not self.distribution.with_salt_version: + salt_version = __saltstack_version__ + else: + salt_version = SaltStackVersion.parse(self.distribution.with_salt_version) + # pylint: disable=E0602 open(self.distribution.salt_version_hardcoded_path, 'w').write( INSTALL_VERSION_TEMPLATE.format( date=DATE, - full_version_info=__saltstack_version__.full_info + full_version_info=salt_version.full_info ) ) # pylint: enable=E0602 @@ -701,6 +706,13 @@ class Build(build): def run(self): # Run build.run function build.run(self) + if getattr(self.distribution, 'with_salt_version', False): + # Write the hardcoded salt version module salt/_version.py + self.distribution.salt_version_hardcoded_path = os.path.join( + self.build_lib, 'salt', '_version.py' + ) + self.run_command('write_salt_version') + if getattr(self.distribution, 'running_salt_install', False): # If our install attribute is present and set to True, we'll go # ahead and write our install time python modules. @@ -805,6 +817,7 @@ class SaltDistribution(distutils.dist.Distribution): ('ssh-packaging', None, 'Run in SSH packaging mode'), ('salt-transport=', None, 'The transport to prepare salt for. Choices are \'zeromq\' ' '\'raet\' or \'both\'. Defaults to \'zeromq\'', 'zeromq')] + [ + ('with-salt-version=', None, 'Set a fixed version for Salt instead calculating it'), # Salt's Paths Configuration Settings ('salt-root-dir=', None, 'Salt\'s pre-configured root directory'), @@ -856,6 +869,9 @@ class SaltDistribution(distutils.dist.Distribution): self.salt_spm_pillar_dir = None self.salt_spm_reactor_dir = None + # Salt version + self.with_salt_version = None + self.name = 'salt-ssh' if PACKAGED_FOR_SALT_SSH else 'salt' self.salt_version = __version__ # pylint: disable=undefined-variable self.description = 'Portable, distributed, remote execution and configuration management system' From 5a852c815ca03ce7f7a8afe3242f5794e7b3e443 Mon Sep 17 00:00:00 2001 From: Kunal Ajay Bajpai Date: Fri, 6 Oct 2017 22:02:06 +0530 Subject: [PATCH 516/633] Do not raise exception --- salt/config/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/config/__init__.py b/salt/config/__init__.py index 8448fdee4d..a37e2cef3f 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -3324,11 +3324,11 @@ def _cache_id(minion_id, cache_file): path = os.path.dirname(cache_file) try: os.makedirs(path) - except OSError: + except OSError as exc: if os.path.isdir(path): pass else: - raise + log.error('Failed to create dirs to minion_id file: {0}'.format(exc)) try: with salt.utils.files.fopen(cache_file, 'w') as idf: From 9a4f6a260f7bc5a47d17538bf76161a40cee605d Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 6 Oct 2017 13:38:24 -0500 Subject: [PATCH 517/633] Fix fileclient's get_url when redirecting to a redirect When a 30x leads to a 200 OK, we properly reset write_body[0] so that we save the body of the response. However, when both A) a 30x redirects to another 30x and B) we've already determined the encoding from the Content-Type (and thus set write_body[2]), then we don't properly set write_body[0], resulting in a zero-length file. This commit fixes this by also resetting the write_body[2] when following redirects, so that we make sure we are getting the encoding for the request to the URL that resulted in the 200 instead of the one that resulted in the 30x. --- salt/fileclient.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/salt/fileclient.py b/salt/fileclient.py index fc396fcc56..26d831fdd0 100644 --- a/salt/fileclient.py +++ b/salt/fileclient.py @@ -623,10 +623,11 @@ class Client(object): if write_body[1] is not False and write_body[2] is None: if not hdr.strip() and 'Content-Type' not in write_body[1]: # We've reached the end of the headers and not yet - # found the Content-Type. Reset the values we're - # tracking so that we properly follow the redirect. - write_body[0] = None - write_body[1] = False + # found the Content-Type. Reset write_body[0] so that + # we properly follow the redirect. Note that slicing is + # used below to ensure that we re-use the same list + # rather than creating a new one. + write_body[0:2] = (None, False) return # Try to find out what content type encoding is used if # this is a text file @@ -648,9 +649,12 @@ class Client(object): # If write_body[0] is False, this means that this # header is a 30x redirect, so we need to reset # write_body[0] to None so that we parse the HTTP - # status code from the redirect target. + # status code from the redirect target. Additionally, + # we need to reset write_body[2] so that we inspect the + # headers for the Content-Type of the URL we're + # following. if write_body[0] is write_body[1] is False: - write_body[0] = None + write_body[0] = write_body[2] = None # Check the status line of the HTTP request if write_body[0] is None: From 10282a0fe7862c960235addf50a7ae23fae08fe4 Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Fri, 6 Oct 2017 14:42:30 -0600 Subject: [PATCH 518/633] Typo --- salt/cloud/clouds/azurearm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/cloud/clouds/azurearm.py b/salt/cloud/clouds/azurearm.py index 81d6557e41..ad4e8b2a10 100644 --- a/salt/cloud/clouds/azurearm.py +++ b/salt/cloud/clouds/azurearm.py @@ -153,6 +153,7 @@ def __virtual__(): 'Microsoft Azure SDK for Python >= 2.0rc5, ' 'Microsoft Azure Storage SDK for Python >= 0.32, ' 'Microsoft Azure CLI >= 2.0.12' + ) global cache # pylint: disable=global-statement,invalid-name cache = salt.cache.Cache(__opts__) From 9df3d91d8fa579d1c9a3847e18e308b376e591e7 Mon Sep 17 00:00:00 2001 From: "C. R. Oldham" Date: Fri, 6 Oct 2017 15:33:10 -0600 Subject: [PATCH 519/633] Release notes blurb for change to bindpw requirements --- doc/topics/releases/2016.11.8.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/topics/releases/2016.11.8.rst b/doc/topics/releases/2016.11.8.rst index 9f4eb68dab..16d6fce0d8 100644 --- a/doc/topics/releases/2016.11.8.rst +++ b/doc/topics/releases/2016.11.8.rst @@ -4,6 +4,11 @@ Salt 2016.11.8 Release Notes Version 2016.11.8 is a bugfix release for :ref:`2016.11.0 `.] +Anonymous Binds and LDAP/Active Directory +----------------------------------------- + +When auth.ldap.anonymous is set to False, the bind password can no longer be empty. + Changes for v2016.11.7..v2016.11.8 ---------------------------------- From 962a20cf4be986c88c3b19ab2db47b931961f9f5 Mon Sep 17 00:00:00 2001 From: "C. R. Oldham" Date: Fri, 6 Oct 2017 15:41:07 -0600 Subject: [PATCH 520/633] Require that bindpw be non-empty if auth.ldap.anonymous=False --- doc/topics/eauth/index.rst | 7 ++++++- salt/auth/ldap.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/topics/eauth/index.rst b/doc/topics/eauth/index.rst index bbf858e1a5..4633b0c8f9 100644 --- a/doc/topics/eauth/index.rst +++ b/doc/topics/eauth/index.rst @@ -218,6 +218,7 @@ Server configuration values and their defaults: # Bind to LDAP anonymously to determine group membership # Active Directory does not allow anonymous binds without special configuration + # In addition, if auth.ldap.anonymous is True, empty bind passwords are not permitted. auth.ldap.anonymous: False # FOR TESTING ONLY, this is a VERY insecure setting. @@ -250,7 +251,11 @@ and groups, it re-authenticates as the user running the Salt commands. If you are already aware of the structure of your DNs and permissions in your LDAP store are set such that users can look up their own group memberships, then the first and second users can be the same. To tell Salt this is -the case, omit the ``auth.ldap.bindpw`` parameter. You can template the ``binddn`` like this: +the case, omit the ``auth.ldap.bindpw`` parameter. Note this is not the same thing as using an anonymous bind. +Most LDAP servers will not permit anonymous bind, and as mentioned above, if `auth.ldap.anonymous` is False you +cannot use an empty password. + +You can template the ``binddn`` like this: .. code-block:: yaml diff --git a/salt/auth/ldap.py b/salt/auth/ldap.py index c2f002ab84..1033654981 100644 --- a/salt/auth/ldap.py +++ b/salt/auth/ldap.py @@ -110,6 +110,10 @@ class _LDAPConnection(object): self.ldap.set_option(ldap.OPT_REFERRALS, 0) # Needed for AD if not anonymous: + if self.bindpw is None or len(self.bindpw) < 1: + raise CommandExecutionError( + 'LDAP bind password is not set: password cannot be empty if auth.ldap.anonymous is False' + ) self.ldap.simple_bind_s(self.binddn, self.bindpw) except Exception as ldap_error: raise CommandExecutionError( From f5a8682f951ff849b9279d0cb03bcbd15aedee3e Mon Sep 17 00:00:00 2001 From: Kunal Ajay Bajpai Date: Sat, 7 Oct 2017 11:11:38 +0530 Subject: [PATCH 521/633] Check before creating dir --- salt/config/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/salt/config/__init__.py b/salt/config/__init__.py index abb31bca3c..2026f02318 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -3389,8 +3389,10 @@ def _cache_id(minion_id, cache_file): ''' path = os.path.dirname(cache_file) try: - os.makedirs(path) + if not os.path.isdir(path): + os.makedirs(path) except OSError as exc: + # Handle race condition where dir is created after os.path.isdir check if os.path.isdir(path): pass else: From 045c409784bc8f79603621bfdde5007bb72a1402 Mon Sep 17 00:00:00 2001 From: Stephan Looney Date: Sat, 7 Oct 2017 09:33:34 -0400 Subject: [PATCH 522/633] Removed unused six and additional code cleanup --- salt/cloud/clouds/clc.py | 371 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 salt/cloud/clouds/clc.py diff --git a/salt/cloud/clouds/clc.py b/salt/cloud/clouds/clc.py new file mode 100644 index 0000000000..c1a2b7531c --- /dev/null +++ b/salt/cloud/clouds/clc.py @@ -0,0 +1,371 @@ +# -*- coding: utf-8 -*- +''' +CenturyLink Cloud Module +=================== + +.. versionadded:: 0yxgen + +The CLC cloud module allows you to manage CLC Via the CLC SDK. + +:codeauthor: Stephan Looney + + +Dependencies +============ + +- clc-sdk Python Module +- flask + +CLC SDK +------- + +clc-sdk can be installed via pip: + +.. code-block:: bash + + pip install clc-sdk + +.. note:: + For sdk reference see: https://github.com/CenturyLinkCloud/clc-python-sdk + +Flask +------- +flask can be installed via pip: +.. code-block:: bash + pip install flask + +Configuration +============= + +To use this module: set up the clc-sdk, user, password, key in the +cloud configuration at +``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/clc.conf``: + +.. code-block:: yaml + + my-clc-config: + driver: clc + user: 'web-user' + password: 'verybadpass' + token: '' + token_pass:'' + accountalias: 'ACT' +.. note:: + + The ``provider`` parameter in cloud provider configuration was renamed to ``driver``. + This change was made to avoid confusion with the ``provider`` parameter that is + used in cloud profile configuration. Cloud provider configuration now uses ``driver`` + to refer to the salt-cloud driver that provides the underlying functionality to + connect to a cloud provider, while cloud profile configuration continues to use + ``provider`` to refer to the cloud provider configuration that you define. + +''' + +# Import python libs +from __future__ import absolute_import +import logging +import time +import json +# Import salt libs +import importlib +from salt.exceptions import SaltCloudSystemExit +# Import salt cloud libs +import salt.config as config + +# Get logging started +log = logging.getLogger(__name__) + +# Attempt to import clc-sdk lib +try: + #when running this in linode's Ubuntu 16.x version the following line is required to get the clc sdk libraries to load + importlib.import_module('clc') + import clc + HAS_CLC = True +except ImportError: + HAS_CLC = False +# Disable InsecureRequestWarning generated on python > 2.6 +try: + from requests.packages.urllib3 import disable_warnings # pylint: disable=no-name-in-module + disable_warnings() +except Exception: + pass + + +__virtualname__ = 'clc' + + +# Only load in this module if the CLC configurations are in place +def __virtual__(): + ''' + Check for CLC configuration and if required libs are available. + ''' + if get_configured_provider() is False or get_dependencies() is False: + return False + + return __virtualname__ + + +def get_configured_provider(): + return config.is_provider_configured( + __opts__, + __active_provider_name__ or __virtualname__, + ('token', 'token_pass', 'user', 'password', ) + ) + + +def get_dependencies(): + ''' + Warn if dependencies aren't met. + ''' + deps = { + 'clc': HAS_CLC, + } + return config.check_driver_dependencies( + __virtualname__, + deps + ) + + +def get_creds(): + user = config.get_cloud_config_value( + 'user', get_configured_provider(), __opts__, search_global=False + ) + password = config.get_cloud_config_value( + 'password', get_configured_provider(), __opts__, search_global=False + ) + accountalias = config.get_cloud_config_value( + 'accountalias', get_configured_provider(), __opts__, search_global=False + ) + token = config.get_cloud_config_value( + 'token', get_configured_provider(), __opts__, search_global=False + ) + token_pass = config.get_cloud_config_value( + 'token_pass', get_configured_provider(), __opts__, search_global=False + ) + creds = {'user': user, 'password': password, 'token': token, 'token_pass': token_pass, 'accountalias': accountalias} + return creds + + +def list_nodes_full(call=None, for_output=True): + ''' + Return a list of the VMs that are on the provider + ''' + if call == 'action': + raise SaltCloudSystemExit( + 'The list_nodes_full function must be called with -f or --function.' + ) + creds = get_creds() + clc.v1.SetCredentials(creds["token"], creds["token_pass"]) + servers_raw = clc.v1.Server.GetServers(location=None) + servers_raw = json.dumps(servers_raw) + servers = json.loads(servers_raw) + return servers + + +def get_queue_data(call=None, for_output=True): + creds = get_creds() + clc.v1.SetCredentials(creds["token"], creds["token_pass"]) + cl_queue = clc.v1.Queue.List() + return cl_queue + + +def get_monthly_estimate(call=None, for_output=True): + ''' + Return a list of the VMs that are on the provider + ''' + creds = get_creds() + clc.v1.SetCredentials(creds["token"], creds["token_pass"]) + if call == 'action': + raise SaltCloudSystemExit( + 'The list_nodes_full function must be called with -f or --function.' + ) + try: + billing_raw = clc.v1.Billing.GetAccountSummary(alias=creds["accountalias"]) + billing_raw = json.dumps(billing_raw) + billing = json.loads(billing_raw) + billing = round(billing["MonthlyEstimate"], 2) + return {"Monthly Estimate": billing} + except RuntimeError: + return {"Monthly Estimate": 0} + + +def get_month_to_date(call=None, for_output=True): + ''' + Return a list of the VMs that are on the provider + ''' + creds = get_creds() + clc.v1.SetCredentials(creds["token"], creds["token_pass"]) + if call == 'action': + raise SaltCloudSystemExit( + 'The list_nodes_full function must be called with -f or --function.' + ) + try: + billing_raw = clc.v1.Billing.GetAccountSummary(alias=creds["accountalias"]) + billing_raw = json.dumps(billing_raw) + billing = json.loads(billing_raw) + billing = round(billing["MonthToDateTotal"], 2) + return {"Month To Date": billing} + except RuntimeError: + return 0 + + +def get_server_alerts(call=None, for_output=True, **kwargs): + ''' + Return a list of alerts from CLC as reported by their infra + ''' + for key, value in kwargs.items(): + servername = "" + if key == "servername": + servername = value + creds = get_creds() + clc.v2.SetCredentials(creds["user"], creds["password"]) + alerts = clc.v2.Server(servername).Alerts() + return alerts + + +def get_group_estimate(call=None, for_output=True, **kwargs): + ''' + Return a list of the VMs that are on the provider + usage: "salt-cloud -f get_group_estimate clc group=Dev location=VA1" + ''' + for key, value in kwargs.items(): + group = "" + location = "" + if key == "group": + group = value + if key == "location": + location = value + creds = get_creds() + clc.v1.SetCredentials(creds["token"], creds["token_pass"]) + if call == 'action': + raise SaltCloudSystemExit( + 'The list_nodes_full function must be called with -f or --function.' + ) + try: + billing_raw = clc.v1.Billing.GetGroupEstimate(group=group, alias=creds["accountalias"], location=location) + billing_raw = json.dumps(billing_raw) + billing = json.loads(billing_raw) + estimate = round(billing["MonthlyEstimate"], 2) + month_to_date = round(billing["MonthToDate"], 2) + return {"Monthly Estimate": estimate, "Month to Date": month_to_date} + except RuntimeError: + return 0 + + +def avail_images(call=None): + ''' + returns a list of images available to you + ''' + all_servers = list_nodes_full() + templates = {} + for server in all_servers: + if server["IsTemplate"]: + templates.update({"Template Name": server["Name"]}) + return templates + + +def avail_locations(call=None): + ''' + returns a list of locations available to you + ''' + creds = get_creds() + clc.v1.SetCredentials(creds["token"], creds["token_pass"]) + locations = clc.v1.Account.GetLocations() + return locations + + +def avail_sizes(call=None): + ''' + use templates for this + ''' + return {"Sizes": "Sizes are built into templates. Choose appropriate template"} + + +def get_build_status(req_id, nodename): + ''' + get the build status from CLC to make sure we dont return to early + ''' + counter = 0 + req_id = str(req_id) + while counter < 10: + queue = clc.v1.Blueprint.GetStatus(request_id=(req_id)) + if queue["PercentComplete"] == 100: + server_name = queue["Servers"][0] + creds = get_creds() + clc.v2.SetCredentials(creds["user"], creds["password"]) + ip_addresses = clc.v2.Server(server_name).ip_addresses + internal_ip_address = ip_addresses[0]["internal"] + return internal_ip_address + else: + counter = counter + 1 + log.info("Creating Cloud VM " + nodename + " Time out in " + str(10 - counter) + " minutes") + time.sleep(60) + + +def create(vm_): + ''' + get the system build going + ''' + creds = get_creds() + clc.v1.SetCredentials(creds["token"], creds["token_pass"]) + cloud_profile = config.is_provider_configured( + __opts__, + __active_provider_name__ or __virtualname__, + ('token',) + ) + group = config.get_cloud_config_value( + 'group', vm_, __opts__, search_global=False, default=None, + ) + name = vm_['name'] + description = config.get_cloud_config_value( + 'description', vm_, __opts__, search_global=False, default=None, + ) + ram = config.get_cloud_config_value( + 'ram', vm_, __opts__, search_global=False, default=None, + ) + backup_level = config.get_cloud_config_value( + 'backup_level', vm_, __opts__, search_global=False, default=None, + ) + template = config.get_cloud_config_value( + 'template', vm_, __opts__, search_global=False, default=None, + ) + password = config.get_cloud_config_value( + 'password', vm_, __opts__, search_global=False, default=None, + ) + cpu = config.get_cloud_config_value( + 'cpu', vm_, __opts__, search_global=False, default=None, + ) + network = config.get_cloud_config_value( + 'network', vm_, __opts__, search_global=False, default=None, + ) + location = config.get_cloud_config_value( + 'location', vm_, __opts__, search_global=False, default=None, + ) + if len(name) > 6: + name = name[0:6] + if len(password) < 9: + password = '' + clc_return = clc.v1.Server.Create(alias=None, location=(location), name=(name), template=(template), cpu=(cpu), ram=(ram), backup_level=(backup_level), group=(group), network=(network), description=(description), password=(password)) + req_id = clc_return["RequestID"] + vm_['ssh_host'] = get_build_status(req_id, name) + __utils__['cloud.fire_event']( + 'event', + 'waiting for ssh', + 'salt/cloud/{0}/waiting_for_ssh'.format(name), + sock_dir=__opts__['sock_dir'], + args={'ip_address': vm_['ssh_host']}, + transport=__opts__['transport'] + ) + + # Bootstrap! + ret = __utils__['cloud.bootstrap'](vm_, __opts__) + return_message = {"Server Name": name, "IP Address": vm_['ssh_host']} + ret.update(return_message) + return return_message + + +def destroy(name, call=None): + ''' + destroy the vm + ''' + return {"status": "destroying must be done via https://control.ctl.io at this time"} From bf45ae6e6ad02e14ca8da289463b4399d7bc84dd Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 9 Oct 2017 07:32:24 -0500 Subject: [PATCH 523/633] Fix grains.has_value when value is False This function returns a boolean based on the boolean result of the value returned when looking up the key. However, this means that if a key exists and has a value of 0, an empty string, or False/None, grains.has_value will incorrectly return False. This fixes that by only returning False when the key is not present. --- salt/modules/grains.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/salt/modules/grains.py b/salt/modules/grains.py index 033dd3b375..df813fbf5b 100644 --- a/salt/modules/grains.py +++ b/salt/modules/grains.py @@ -121,7 +121,7 @@ def get(key, default='', delimiter=DEFAULT_TARGET_DELIM, ordered=True): def has_value(key): ''' - Determine whether a named value exists in the grains dictionary. + Determine whether a key exists in the grains dictionary. Given a grains dictionary that contains the following structure:: @@ -137,7 +137,10 @@ def has_value(key): salt '*' grains.has_value pkg:apache ''' - return True if salt.utils.traverse_dict_and_list(__grains__, key, False) else False + return salt.utils.traverse_dict_and_list( + __grains__, + key, + KeyError) is not KeyError def items(sanitize=False): From 1dc298c5ebde9fc0c00dd9430a5f0513cbce31cb Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 9 Oct 2017 17:57:48 +0200 Subject: [PATCH 524/633] Bugfix: always return a string "list" on unknown job target type. --- salt/returners/couchbase_return.py | 2 +- salt/returners/postgres_local_cache.py | 2 +- salt/runners/jobs.py | 2 +- salt/utils/jid.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/salt/returners/couchbase_return.py b/salt/returners/couchbase_return.py index 000fe6e03b..b9e770bb13 100644 --- a/salt/returners/couchbase_return.py +++ b/salt/returners/couchbase_return.py @@ -313,7 +313,7 @@ def _format_job_instance(job): 'Arguments': list(job.get('arg', [])), # unlikely but safeguard from invalid returns 'Target': job.get('tgt', 'unknown-target'), - 'Target-type': job.get('tgt_type', []), + 'Target-type': job.get('tgt_type', 'list'), 'User': job.get('user', 'root')} if 'metadata' in job: diff --git a/salt/returners/postgres_local_cache.py b/salt/returners/postgres_local_cache.py index 0485fa4fe9..7b1a134458 100644 --- a/salt/returners/postgres_local_cache.py +++ b/salt/returners/postgres_local_cache.py @@ -170,7 +170,7 @@ def _format_job_instance(job): 'Arguments': json.loads(job.get('arg', '[]')), # unlikely but safeguard from invalid returns 'Target': job.get('tgt', 'unknown-target'), - 'Target-type': job.get('tgt_type', []), + 'Target-type': job.get('tgt_type', 'list'), 'User': job.get('user', 'root')} # TODO: Add Metadata support when it is merged from develop return ret diff --git a/salt/runners/jobs.py b/salt/runners/jobs.py index 65f1158049..605839eee4 100644 --- a/salt/runners/jobs.py +++ b/salt/runners/jobs.py @@ -543,7 +543,7 @@ def _format_job_instance(job): 'Arguments': list(job.get('arg', [])), # unlikely but safeguard from invalid returns 'Target': job.get('tgt', 'unknown-target'), - 'Target-type': job.get('tgt_type', []), + 'Target-type': job.get('tgt_type', 'list'), 'User': job.get('user', 'root')} if 'metadata' in job: diff --git a/salt/utils/jid.py b/salt/utils/jid.py index b65293d8d5..f8a0664e22 100644 --- a/salt/utils/jid.py +++ b/salt/utils/jid.py @@ -75,7 +75,7 @@ def format_job_instance(job): 'Arguments': list(job.get('arg', [])), # unlikely but safeguard from invalid returns 'Target': job.get('tgt', 'unknown-target'), - 'Target-type': job.get('tgt_type', []), + 'Target-type': job.get('tgt_type', 'list'), 'User': job.get('user', 'root')} if 'metadata' in job: From f0c3184288ef55fe4cbd0b236e4ee42c8d470634 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 27 Sep 2017 14:54:14 -0400 Subject: [PATCH 525/633] Add Security Notes to 2016.11.8 Release Notes --- doc/topics/releases/2016.11.8.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/topics/releases/2016.11.8.rst b/doc/topics/releases/2016.11.8.rst index 9f4eb68dab..b3b04cedc6 100644 --- a/doc/topics/releases/2016.11.8.rst +++ b/doc/topics/releases/2016.11.8.rst @@ -7,6 +7,13 @@ Version 2016.11.8 is a bugfix release for :ref:`2016.11.0 `.] Changes for v2016.11.7..v2016.11.8 ---------------------------------- +Security Fix +============ + +CVE-2017-14695 Directory traversal vulnerability in minion id validation in SaltStack. Allows remote minions with incorrect credentials to authenticate to a master via a crafted minion ID. Credit for discovering the security flaw goes to: Julian Brost (julian@0x4a42.net) + +CVE-2017-14696 Remote Denial of Service with a specially crafted authentication request. Credit for discovering the security flaw goes to: Julian Brost (julian@0x4a42.net) + Extended changelog courtesy of Todd Stansell (https://github.com/tjstansell/salt-changelogs): *Generated at: 2017-09-11T14:52:27Z* From 57fd6f7bcb4f20583caaf21a372339f35b50f681 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 27 Sep 2017 14:56:04 -0400 Subject: [PATCH 526/633] Add Security Notes to 2017.7.2 Release Notes --- doc/topics/releases/2017.7.2.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/topics/releases/2017.7.2.rst b/doc/topics/releases/2017.7.2.rst index 1f823d7417..c9123529cb 100644 --- a/doc/topics/releases/2017.7.2.rst +++ b/doc/topics/releases/2017.7.2.rst @@ -7,6 +7,13 @@ Version 2017.7.2 is a bugfix release for :ref:`2017.7.0 `. Changes for v2017.7.1..v2017.7.2 -------------------------------- +Security Fix +============ + +CVE-2017-14695 Directory traversal vulnerability in minion id validation in SaltStack. Allows remote minions with incorrect credentials to authenticate to a master via a crafted minion ID. Credit for discovering the security flaw goes to: Julian Brost (julian@0x4a42.net) + +CVE-2017-14696 Remote Denial of Service with a specially crafted authentication request. Credit for discovering the security flaw goes to: Julian Brost (julian@0x4a42.net) + Extended changelog courtesy of Todd Stansell (https://github.com/tjstansell/salt-changelogs): *Generated at: 2017-09-26T21:06:19Z* From b8507c90ec02582dd00ca40b5a89836f57b2941e Mon Sep 17 00:00:00 2001 From: Mike Place Date: Mon, 9 Oct 2017 11:31:54 -0600 Subject: [PATCH 527/633] Lower the radon threshold for CodeClimate Lower to 'D' which is cyclomatic complexity "moderate" or above. More info on grading: http://radon.readthedocs.io/en/latest/commandline.html --- .codeclimate.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 171bc4fd48..b9423ab4d1 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,5 +1,11 @@ languages: - Ruby: false - JavaScript: false - Python: true - PHP: false \ No newline at end of file + Ruby: false + JavaScript: false + Python: true + PHP: false + +engines: + radon: + enabled: true + config: + threshold: "D" From e6d31c1ea6050be93f1c255ef44fe843b04615f2 Mon Sep 17 00:00:00 2001 From: Rossen Georgiev Date: Sun, 8 Oct 2017 13:18:31 +0100 Subject: [PATCH 528/633] fix zenoss state module not respecting test=true --- salt/states/zenoss.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/salt/states/zenoss.py b/salt/states/zenoss.py index 269599cbe0..c28e52f395 100644 --- a/salt/states/zenoss.py +++ b/salt/states/zenoss.py @@ -6,7 +6,7 @@ State to manage monitoring in Zenoss. This state module depends on the 'zenoss' Salt execution module. -Allows for setting a state of minions in Zenoss using the Zenoss API. Currently Zenoss 4.x is supported. +Allows for setting a state of minions in Zenoss using the Zenoss API. Currently Zenoss 4.x and 5.x are supported. .. code-block:: yaml @@ -30,6 +30,8 @@ def __virtual__(): ''' if 'zenoss.add_device' in __salt__: return 'zenoss' + else: + return False, "The zenoss execution module is not available" def monitored(name, device_class=None, collector='localhost', prod_state=None): @@ -57,21 +59,28 @@ def monitored(name, device_class=None, collector='localhost', prod_state=None): ret['comment'] = '{0} is already monitored'.format(name) # if prod_state is set, ensure it matches with the current state - if prod_state: - if device['productionState'] != prod_state: + if prod_state is not None and device['productionState'] != prod_state: + if __opts__['test']: + ret['comment'] = '{0} is already monitored but prodState will be updated'.format(name) + ret['result'] = None + else: __salt__['zenoss.set_prod_state'](prod_state, name) - ret['changes'] = {'old': 'prodState == {0}'.format(device['productionState']), 'new': 'prodState == {0}'.format(prod_state)} - ret['comment'] = '{0} is already monitored but prodState was incorrect, setting to Production'.format(name) + ret['comment'] = '{0} is already monitored but prodState was updated'.format(name) + ret['changes'] = { + 'old': 'prodState == {0}'.format(device['productionState']), + 'new': 'prodState == {0}'.format(prod_state) + } return ret + # Device not yet in Zenoss if __opts__['test']: ret['comment'] = 'The state of "{0}" will be changed.'.format(name) ret['changes'] = {'old': 'monitored == False', 'new': 'monitored == True'} ret['result'] = None return ret - # Device not yet in Zenoss. Add and check result + # Add and check result if __salt__['zenoss.add_device'](name, device_class, collector, prod_state): ret['result'] = True ret['changes'] = {'old': 'monitored == False', 'new': 'monitored == True'} From d41de4ec1639b74af69711ae2b01816d90742b11 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Mon, 9 Oct 2017 12:03:10 -0600 Subject: [PATCH 529/633] Exclude templates in radon codeclimate test --- .codeclimate.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.codeclimate.yml b/.codeclimate.yml index b9423ab4d1..49825b4bca 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -7,5 +7,7 @@ languages: engines: radon: enabled: true + exclude_paths: + - "templates/" config: threshold: "D" From 1977df8462d6001fab31e4a40697a21c0fb3910b Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Mon, 9 Oct 2017 16:26:13 -0400 Subject: [PATCH 530/633] Add Security Notes to 2016.3.8 Release Notes --- doc/topics/releases/2016.3.8.rst | 22 ++++------------------ doc/topics/releases/2016.3.9.rst | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 doc/topics/releases/2016.3.9.rst diff --git a/doc/topics/releases/2016.3.8.rst b/doc/topics/releases/2016.3.8.rst index c5f0c01da8..4d729cca34 100644 --- a/doc/topics/releases/2016.3.8.rst +++ b/doc/topics/releases/2016.3.8.rst @@ -7,23 +7,9 @@ Version 2016.3.8 is a bugfix release for :ref:`2016.3.0 `. Changes for v2016.3.7..v2016.3.8 -------------------------------- -New master configuration option `allow_minion_key_revoke`, defaults to True. This option -controls whether a minion can request that the master revoke its key. When True, a minion -can request a key revocation and the master will comply. If it is False, the key will not -be revoked by the msater. +Security Fix +============ -New master configuration option `require_minion_sign_messages` -This requires that minions cryptographically sign the messages they -publish to the master. If minions are not signing, then log this information -at loglevel 'INFO' and drop the message without acting on it. +CVE-2017-14695 Directory traversal vulnerability in minion id validation in SaltStack. Allows remote minions with incorrect credentials to authenticate to a master via a crafted minion ID. Credit for discovering the security flaw goes to: Julian Brost (julian@0x4a42.net) -New master configuration option `drop_messages_signature_fail` -Drop messages from minions when their signatures do not validate. -Note that when this option is False but `require_minion_sign_messages` is True -minions MUST sign their messages but the validity of their signatures -is ignored. - -New minion configuration option `minion_sign_messages` -Causes the minion to cryptographically sign the payload of messages it places -on the event bus for the master. The payloads are signed with the minion's -private key so the master can verify the signature with its public key. +CVE-2017-14696 Remote Denial of Service with a specially crafted authentication request. Credit for discovering the security flaw goes to: Julian Brost (julian@0x4a42.net) diff --git a/doc/topics/releases/2016.3.9.rst b/doc/topics/releases/2016.3.9.rst new file mode 100644 index 0000000000..630801cbe5 --- /dev/null +++ b/doc/topics/releases/2016.3.9.rst @@ -0,0 +1,29 @@ +=========================== +Salt 2016.3.9 Release Notes +=========================== + +Version 2016.3.9 is a bugfix release for :ref:`2016.3.0 `. + +Changes for v2016.3.7..v2016.3.9 +-------------------------------- + +New master configuration option `allow_minion_key_revoke`, defaults to True. This option +controls whether a minion can request that the master revoke its key. When True, a minion +can request a key revocation and the master will comply. If it is False, the key will not +be revoked by the msater. + +New master configuration option `require_minion_sign_messages` +This requires that minions cryptographically sign the messages they +publish to the master. If minions are not signing, then log this information +at loglevel 'INFO' and drop the message without acting on it. + +New master configuration option `drop_messages_signature_fail` +Drop messages from minions when their signatures do not validate. +Note that when this option is False but `require_minion_sign_messages` is True +minions MUST sign their messages but the validity of their signatures +is ignored. + +New minion configuration option `minion_sign_messages` +Causes the minion to cryptographically sign the payload of messages it places +on the event bus for the master. The payloads are signed with the minion's +private key so the master can verify the signature with its public key. From da0749cc0536d05342a34a5b6848d0a39949c206 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Mon, 9 Oct 2017 17:26:03 +0200 Subject: [PATCH 531/633] Improved sensehat execution module documentation --- salt/modules/sensehat.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/salt/modules/sensehat.py b/salt/modules/sensehat.py index f857a1e74d..0b1006f4a0 100644 --- a/salt/modules/sensehat.py +++ b/salt/modules/sensehat.py @@ -6,8 +6,8 @@ Module for controlling the LED matrix or reading environment data on the SenseHa :maturity: new :depends: sense_hat Python module -You can specify the rotation of the Pi in a pillar. -This is useful if it is used upside down or sideways to correct the orientation of the image being shown. +The rotation of the Pi can be specified in a pillar. +This is useful if the Pi is used upside down or sideways to correct the orientation of the image being shown. Example: @@ -52,7 +52,7 @@ def set_pixels(pixels): Sets the entire LED matrix based on a list of 64 pixel values pixels - A list of 64 color values [R, G, B]. + A list of 64 `[R, G, B]` color values. ''' _sensehat.set_pixels(pixels) return {'pixels': pixels} @@ -81,12 +81,12 @@ def get_pixels(): def set_pixel(x, y, color): ''' - Sets the a single pixel on the LED matrix to a specified color. + Sets a single pixel on the LED matrix to a specified color. x - The x coodrinate of th pixel. Ranges from 0 on the left to 7 on the right. + The x coordinate of the pixel. Ranges from 0 on the left to 7 on the right. y - The y coodrinate of th pixel. Ranges from 0 at the top to 7 at the bottom. + The y coordinate of the pixel. Ranges from 0 at the top to 7 at the bottom. color The new color of the pixel as a list of `[R, G, B]` values. @@ -105,9 +105,9 @@ def get_pixel(x, y): Returns the color of a single pixel on the LED matrix. x - The x coodrinate of th pixel. Ranges from 0 on the left to 7 on the right. + The x coordinate of the pixel. Ranges from 0 on the left to 7 on the right. y - The y coodrinate of th pixel. Ranges from 0 at the top to 7 at the bottom. + The y coordinate of the pixel. Ranges from 0 at the top to 7 at the bottom. .. note:: Please read the note for `get_pixels` @@ -131,7 +131,7 @@ def low_light(low_light=True): def show_message(message, msg_type=None, - text_color=None, back_color=None, scroll_speed=None): + text_color=None, back_color=None, scroll_speed=0.1): ''' Displays a message on the LED matrix. @@ -167,7 +167,6 @@ def show_message(message, msg_type=None, ''' text_color = text_color or [255, 255, 255] back_color = back_color or [0, 0, 0] - scroll_speed = scroll_speed or 0.1 color_by_type = { 'error': [255, 0, 0], @@ -211,7 +210,7 @@ def show_letter(letter, text_color=None, back_color=None): def show_image(image): ''' - Displays a 8 x 8 image on the LED matrix. + Displays an 8 x 8 image on the LED matrix. image The path to the image to display. The image must be 8 x 8 pixels in size. @@ -262,7 +261,7 @@ def get_temperature(): Gets the temperature in degrees Celsius from the humidity sensor. Equivalent to calling `get_temperature_from_humidity`. - If you get strange results try using 'get_temperature_from_pressure'. + If you get strange results try using `get_temperature_from_pressure`. ''' return _sensehat.get_temperature() From 95ab901553ebaaf59d553236b338872fc38855fc Mon Sep 17 00:00:00 2001 From: Ivan Babrou Date: Fri, 6 Oct 2017 16:23:46 -0700 Subject: [PATCH 532/633] Report built-in modiles in kmod.available, fixes #43945 --- salt/modules/kmod.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/salt/modules/kmod.py b/salt/modules/kmod.py index 615b43e769..a6ae6e6e9c 100644 --- a/salt/modules/kmod.py +++ b/salt/modules/kmod.py @@ -123,7 +123,16 @@ def available(): salt '*' kmod.available ''' ret = [] + mod_dir = os.path.join('/lib/modules/', os.uname()[2]) + + built_in_file = os.path.join(mod_dir, 'modules.builtin') + if os.path.exists(built_in_file): + with salt.utils.fopen(built_in_file, 'r') as f: + for line in f: + # Strip .ko from the basename + ret.append(os.path.basename(line)[:-4]) + for root, dirs, files in os.walk(mod_dir): for fn_ in files: if '.ko' in fn_: From dfa91e379f65cb7412473f87ca228b19215f6314 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Mon, 9 Oct 2017 20:43:48 -0600 Subject: [PATCH 533/633] fix jenkins Python3 loader error warnings --- salt/modules/vagrant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 3b9bbe3453..f94fd01c82 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -55,7 +55,7 @@ def __virtual__(): run Vagrant commands if possible ''' # noinspection PyUnresolvedReferences - if __utils__['path.which']('vagrant') is None: + if salt.utils.path.which('vagrant') is None: return False, 'The vagrant module could not be loaded: vagrant command not found' return __virtualname__ From ef50acf56f2617a413425bdbfdc735ddf549768c Mon Sep 17 00:00:00 2001 From: David McKay Date: Tue, 10 Oct 2017 10:53:23 +0100 Subject: [PATCH 534/633] Adding requirement for requests 2.4.2 when using Vault module --- salt/modules/vault.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/salt/modules/vault.py b/salt/modules/vault.py index c5dab85044..ab5e5ba684 100644 --- a/salt/modules/vault.py +++ b/salt/modules/vault.py @@ -6,6 +6,11 @@ Functions to interact with Hashicorp Vault. +:note: If you see the following error, you'll need to upgrade ``requests`` to atleast 2.4.2 +.. code-block:: shell + [salt.pillar][CRITICAL][14337] Pillar render error: Failed to load ext_pillar vault: {'error': "request() got an unexpected keyword argument 'json'"} + + :configuration: The salt-master must be configured to allow peer-runner configuration, as well as configuration for the module. From 293a369a9570d0342063723e4846444267f171ec Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 10 Oct 2017 09:10:02 -0400 Subject: [PATCH 535/633] Include versionadded tags for new key_text kwarg Refs #43946 and #40040 --- salt/modules/aptpkg.py | 2 ++ salt/states/pkgrepo.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py index 97f2c7f885..cba158b98b 100644 --- a/salt/modules/aptpkg.py +++ b/salt/modules/aptpkg.py @@ -2155,6 +2155,8 @@ def mod_repo(repo, saltenv='base', **kwargs): key_text GPG key in string form to add to the APT GPG keyring + .. versionadded:: Oxygen + consolidate : False If ``True``, will attempt to de-duplicate and consolidate sources diff --git a/salt/states/pkgrepo.py b/salt/states/pkgrepo.py index ac6340431f..dd19a30bb3 100644 --- a/salt/states/pkgrepo.py +++ b/salt/states/pkgrepo.py @@ -262,6 +262,8 @@ def managed(name, ppa=None, **kwargs): key_text The string representation of the GPG key to install. + .. versionadded:: Oxygen + .. note:: Use either ``keyid``/``keyserver``, ``key_url``, or ``key_text`` but From 8aab65c718c35fafacd5f84587b51d44d65b34b0 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Tue, 10 Oct 2017 10:53:38 -0400 Subject: [PATCH 536/633] fix 2016.3.7 release notes merge conflict --- doc/topics/releases/2016.3.7.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/topics/releases/2016.3.7.rst b/doc/topics/releases/2016.3.7.rst index 0fca807661..a201f0cbe0 100644 --- a/doc/topics/releases/2016.3.7.rst +++ b/doc/topics/releases/2016.3.7.rst @@ -13,3 +13,4 @@ Security Fix CVE-2017-12791 Maliciously crafted minion IDs can cause unwanted directory traversals on the Salt-master Correct a flaw in minion id validation which could allow certain minions to authenticate to a master despite not having the correct credentials. To exploit the vulnerability, an attacker must create a salt-minion with an ID containing characters that will cause a directory traversal. Credit for discovering the security flaw goes to: Vernhk@qq.com + From ee792581fc03730566cda300d4ea6a0abcf4e12e Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 23 Aug 2017 10:20:50 -0500 Subject: [PATCH 537/633] Don't allow path separators in minion ID --- salt/utils/verify.py | 15 ++++----------- tests/unit/utils/verify_test.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/salt/utils/verify.py b/salt/utils/verify.py index 9cc31201b8..b3455a639a 100644 --- a/salt/utils/verify.py +++ b/salt/utils/verify.py @@ -485,22 +485,15 @@ def clean_path(root, path, subdir=False): return '' -def clean_id(id_): - ''' - Returns if the passed id is clean. - ''' - if re.search(r'\.\.{sep}'.format(sep=os.sep), id_): - return False - return True - - def valid_id(opts, id_): ''' Returns if the passed id is valid ''' try: - return bool(clean_path(opts['pki_dir'], id_)) and clean_id(id_) - except (AttributeError, KeyError) as e: + if any(x in id_ for x in ('/', '\\', '\0')): + return False + return bool(clean_path(opts['pki_dir'], id_)) + except (AttributeError, KeyError, TypeError): return False diff --git a/tests/unit/utils/verify_test.py b/tests/unit/utils/verify_test.py index 370c2428f9..4794f76f3a 100644 --- a/tests/unit/utils/verify_test.py +++ b/tests/unit/utils/verify_test.py @@ -60,6 +60,16 @@ class TestVerify(TestCase): opts = {'pki_dir': '/tmp/whatever'} self.assertFalse(valid_id(opts, None)) + def test_valid_id_pathsep(self): + ''' + Path separators in id should make it invalid + ''' + opts = {'pki_dir': '/tmp/whatever'} + # We have to test both path separators because os.path.normpath will + # convert forward slashes to backslashes on Windows. + for pathsep in ('/', '\\'): + self.assertFalse(valid_id(opts, pathsep.join(('..', 'foobar')))) + def test_zmq_verify(self): self.assertTrue(zmq_version()) From 63da1214dbc185d6c0c7afca1672d99917948b2f Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 25 Aug 2017 14:15:58 -0500 Subject: [PATCH 538/633] Do not allow IDs with null bytes in decoded payloads --- salt/crypt.py | 3 +++ salt/transport/tcp.py | 11 +++++++++++ salt/transport/zeromq.py | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/salt/crypt.py b/salt/crypt.py index d1ad8efa3b..56d6a77d6a 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -577,6 +577,9 @@ class AsyncAuth(object): log.warning('SaltReqTimeoutError: {0}'.format(e)) raise tornado.gen.Return('retry') raise SaltClientError('Attempt to authenticate with the salt master failed with timeout error') + if not isinstance(payload, dict): + log.error('Sign-in attempt failed: %s', payload) + raise tornado.gen.Return(False) if 'load' in payload: if 'ret' in payload['load']: if not payload['load']['ret']: diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py index 218ac7925c..44912ee759 100644 --- a/salt/transport/tcp.py +++ b/salt/transport/tcp.py @@ -499,6 +499,17 @@ class TCPReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt.tra 'payload and load must be a dict', header=header)) raise tornado.gen.Return() + try: + id_ = payload['load'].get('id', '') + if '\0' in id_: + log.error('Payload contains an id with a null byte: %s', payload) + stream.send(self.serial.dumps('bad load: id contains a null byte')) + raise tornado.gen.Return() + except TypeError: + log.error('Payload contains non-string id: %s', payload) + stream.send(self.serial.dumps('bad load: id {0} is not a string'.format(id_))) + raise tornado.gen.Return() + # intercept the "_auth" commands, since the main daemon shouldn't know # anything about our key auth if payload['enc'] == 'clear' and payload.get('load', {}).get('cmd') == '_auth': diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py index 7a7f682e11..4ac8b7f927 100644 --- a/salt/transport/zeromq.py +++ b/salt/transport/zeromq.py @@ -593,6 +593,17 @@ class ZeroMQReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt. stream.send(self.serial.dumps('payload and load must be a dict')) raise tornado.gen.Return() + try: + id_ = payload['load'].get('id', '') + if '\0' in id_: + log.error('Payload contains an id with a null byte: %s', payload) + stream.send(self.serial.dumps('bad load: id contains a null byte')) + raise tornado.gen.Return() + except TypeError: + log.error('Payload contains non-string id: %s', payload) + stream.send(self.serial.dumps('bad load: id {0} is not a string'.format(id_))) + raise tornado.gen.Return() + # intercept the "_auth" commands, since the main daemon shouldn't know # anything about our key auth if payload['enc'] == 'clear' and payload.get('load', {}).get('cmd') == '_auth': From 9a00302cd860e95250f5f12849762a81dec8fe4d Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Tue, 10 Oct 2017 10:55:18 -0400 Subject: [PATCH 539/633] fix 2016.3.7 release notes merge conflict --- doc/topics/releases/2016.3.7.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/topics/releases/2016.3.7.rst b/doc/topics/releases/2016.3.7.rst index a201f0cbe0..0fca807661 100644 --- a/doc/topics/releases/2016.3.7.rst +++ b/doc/topics/releases/2016.3.7.rst @@ -13,4 +13,3 @@ Security Fix CVE-2017-12791 Maliciously crafted minion IDs can cause unwanted directory traversals on the Salt-master Correct a flaw in minion id validation which could allow certain minions to authenticate to a master despite not having the correct credentials. To exploit the vulnerability, an attacker must create a salt-minion with an ID containing characters that will cause a directory traversal. Credit for discovering the security flaw goes to: Vernhk@qq.com - From 19481423dd856c04d8d0999b7f462a21991449a5 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 23 Aug 2017 10:20:50 -0500 Subject: [PATCH 540/633] Don't allow path separators in minion ID --- salt/utils/verify.py | 15 ++++----------- tests/unit/utils/verify_test.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/salt/utils/verify.py b/salt/utils/verify.py index db513ba675..45581f02ce 100644 --- a/salt/utils/verify.py +++ b/salt/utils/verify.py @@ -481,22 +481,15 @@ def clean_path(root, path, subdir=False): return '' -def clean_id(id_): - ''' - Returns if the passed id is clean. - ''' - if re.search(r'\.\.{sep}'.format(sep=os.sep), id_): - return False - return True - - def valid_id(opts, id_): ''' Returns if the passed id is valid ''' try: - return bool(clean_path(opts['pki_dir'], id_)) and clean_id(id_) - except (AttributeError, KeyError) as e: + if any(x in id_ for x in ('/', '\\', '\0')): + return False + return bool(clean_path(opts['pki_dir'], id_)) + except (AttributeError, KeyError, TypeError): return False diff --git a/tests/unit/utils/verify_test.py b/tests/unit/utils/verify_test.py index 7e60f886d0..c3fa373290 100644 --- a/tests/unit/utils/verify_test.py +++ b/tests/unit/utils/verify_test.py @@ -60,6 +60,16 @@ class TestVerify(TestCase): opts = {'pki_dir': '/tmp/whatever'} self.assertFalse(valid_id(opts, None)) + def test_valid_id_pathsep(self): + ''' + Path separators in id should make it invalid + ''' + opts = {'pki_dir': '/tmp/whatever'} + # We have to test both path separators because os.path.normpath will + # convert forward slashes to backslashes on Windows. + for pathsep in ('/', '\\'): + self.assertFalse(valid_id(opts, pathsep.join(('..', 'foobar')))) + def test_zmq_verify(self): self.assertTrue(zmq_version()) From c0149101c0c27dfdf44ecfbd7dad82be97c66253 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 25 Aug 2017 14:15:58 -0500 Subject: [PATCH 541/633] Do not allow IDs with null bytes in decoded payloads --- salt/crypt.py | 3 +++ salt/transport/tcp.py | 11 +++++++++++ salt/transport/zeromq.py | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/salt/crypt.py b/salt/crypt.py index d330594a2a..46c3329741 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -606,6 +606,9 @@ class AsyncAuth(object): raise tornado.gen.Return('retry') else: raise SaltClientError('Attempt to authenticate with the salt master failed with timeout error') + if not isinstance(payload, dict): + log.error('Sign-in attempt failed: %s', payload) + raise tornado.gen.Return(False) if 'load' in payload: if 'ret' in payload['load']: if not payload['load']['ret']: diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py index a9001f03a5..f274240a1e 100644 --- a/salt/transport/tcp.py +++ b/salt/transport/tcp.py @@ -623,6 +623,17 @@ class TCPReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt.tra 'payload and load must be a dict', header=header)) raise tornado.gen.Return() + try: + id_ = payload['load'].get('id', '') + if '\0' in id_: + log.error('Payload contains an id with a null byte: %s', payload) + stream.send(self.serial.dumps('bad load: id contains a null byte')) + raise tornado.gen.Return() + except TypeError: + log.error('Payload contains non-string id: %s', payload) + stream.send(self.serial.dumps('bad load: id {0} is not a string'.format(id_))) + raise tornado.gen.Return() + # intercept the "_auth" commands, since the main daemon shouldn't know # anything about our key auth if payload['enc'] == 'clear' and payload.get('load', {}).get('cmd') == '_auth': diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py index 94aeb3e21a..eed012aec7 100644 --- a/salt/transport/zeromq.py +++ b/salt/transport/zeromq.py @@ -596,6 +596,17 @@ class ZeroMQReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt. stream.send(self.serial.dumps('payload and load must be a dict')) raise tornado.gen.Return() + try: + id_ = payload['load'].get('id', '') + if '\0' in id_: + log.error('Payload contains an id with a null byte: %s', payload) + stream.send(self.serial.dumps('bad load: id contains a null byte')) + raise tornado.gen.Return() + except TypeError: + log.error('Payload contains non-string id: %s', payload) + stream.send(self.serial.dumps('bad load: id {0} is not a string'.format(id_))) + raise tornado.gen.Return() + # intercept the "_auth" commands, since the main daemon shouldn't know # anything about our key auth if payload['enc'] == 'clear' and payload.get('load', {}).get('cmd') == '_auth': From 70133aa305fde424f3aec652f5bcae7b88320348 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 25 Aug 2017 14:15:58 -0500 Subject: [PATCH 542/633] Do not allow IDs with null bytes in decoded payloads --- salt/crypt.py | 3 +++ salt/transport/tcp.py | 11 +++++++++++ salt/transport/zeromq.py | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/salt/crypt.py b/salt/crypt.py index f4f65940d5..ef4e7645d2 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -607,6 +607,9 @@ class AsyncAuth(object): raise tornado.gen.Return('retry') else: raise SaltClientError('Attempt to authenticate with the salt master failed with timeout error') + if not isinstance(payload, dict): + log.error('Sign-in attempt failed: %s', payload) + raise tornado.gen.Return(False) if 'load' in payload: if 'ret' in payload['load']: if not payload['load']['ret']: diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py index a9001f03a5..f274240a1e 100644 --- a/salt/transport/tcp.py +++ b/salt/transport/tcp.py @@ -623,6 +623,17 @@ class TCPReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt.tra 'payload and load must be a dict', header=header)) raise tornado.gen.Return() + try: + id_ = payload['load'].get('id', '') + if '\0' in id_: + log.error('Payload contains an id with a null byte: %s', payload) + stream.send(self.serial.dumps('bad load: id contains a null byte')) + raise tornado.gen.Return() + except TypeError: + log.error('Payload contains non-string id: %s', payload) + stream.send(self.serial.dumps('bad load: id {0} is not a string'.format(id_))) + raise tornado.gen.Return() + # intercept the "_auth" commands, since the main daemon shouldn't know # anything about our key auth if payload['enc'] == 'clear' and payload.get('load', {}).get('cmd') == '_auth': diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py index 1bb3abebe1..2be6c829f3 100644 --- a/salt/transport/zeromq.py +++ b/salt/transport/zeromq.py @@ -596,6 +596,17 @@ class ZeroMQReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt. stream.send(self.serial.dumps('payload and load must be a dict')) raise tornado.gen.Return() + try: + id_ = payload['load'].get('id', '') + if '\0' in id_: + log.error('Payload contains an id with a null byte: %s', payload) + stream.send(self.serial.dumps('bad load: id contains a null byte')) + raise tornado.gen.Return() + except TypeError: + log.error('Payload contains non-string id: %s', payload) + stream.send(self.serial.dumps('bad load: id {0} is not a string'.format(id_))) + raise tornado.gen.Return() + # intercept the "_auth" commands, since the main daemon shouldn't know # anything about our key auth if payload['enc'] == 'clear' and payload.get('load', {}).get('cmd') == '_auth': From 92e05cf1c0c8be3bf3769b99e01957736ff1a2b0 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 23 Aug 2017 10:20:50 -0500 Subject: [PATCH 543/633] Don't allow path separators in minion ID --- salt/utils/verify.py | 15 ++++----------- tests/unit/utils/test_verify.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/salt/utils/verify.py b/salt/utils/verify.py index 6e69283f07..24b2a1d962 100644 --- a/salt/utils/verify.py +++ b/salt/utils/verify.py @@ -480,22 +480,15 @@ def clean_path(root, path, subdir=False): return '' -def clean_id(id_): - ''' - Returns if the passed id is clean. - ''' - if re.search(r'\.\.\{sep}'.format(sep=os.sep), id_): - return False - return True - - def valid_id(opts, id_): ''' Returns if the passed id is valid ''' try: - return bool(clean_path(opts['pki_dir'], id_)) and clean_id(id_) - except (AttributeError, KeyError, TypeError) as e: + if any(x in id_ for x in ('/', '\\', '\0')): + return False + return bool(clean_path(opts['pki_dir'], id_)) + except (AttributeError, KeyError, TypeError): return False diff --git a/tests/unit/utils/test_verify.py b/tests/unit/utils/test_verify.py index f0335718dd..c1bd942adf 100644 --- a/tests/unit/utils/test_verify.py +++ b/tests/unit/utils/test_verify.py @@ -63,6 +63,16 @@ class TestVerify(TestCase): opts = {'pki_dir': '/tmp/whatever'} self.assertFalse(valid_id(opts, None)) + def test_valid_id_pathsep(self): + ''' + Path separators in id should make it invalid + ''' + opts = {'pki_dir': '/tmp/whatever'} + # We have to test both path separators because os.path.normpath will + # convert forward slashes to backslashes on Windows. + for pathsep in ('/', '\\'): + self.assertFalse(valid_id(opts, pathsep.join(('..', 'foobar')))) + def test_zmq_verify(self): self.assertTrue(zmq_version()) From 6e9f0fa24e9e28737659813b48c3b44ea2653397 Mon Sep 17 00:00:00 2001 From: Daniel Mueller Date: Tue, 10 Oct 2017 13:13:49 -0300 Subject: [PATCH 544/633] Fix GCE provider: #create returns bootstrap result fixes #43997 --- salt/cloud/clouds/gce.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/salt/cloud/clouds/gce.py b/salt/cloud/clouds/gce.py index 950fa533e6..3e6a54e57c 100644 --- a/salt/cloud/clouds/gce.py +++ b/salt/cloud/clouds/gce.py @@ -2580,7 +2580,10 @@ def create(vm_=None, call=None): ssh_user, ssh_key = __get_ssh_credentials(vm_) vm_['ssh_host'] = __get_host(node_data, vm_) vm_['key_filename'] = ssh_key - __utils__['cloud.bootstrap'](vm_, __opts__) + + ret = __utils__['cloud.bootstrap'](vm_, __opts__) + + ret.update(node_dict) log.info('Created Cloud VM \'{0[name]}\''.format(vm_)) log.trace( @@ -2598,7 +2601,7 @@ def create(vm_=None, call=None): transport=__opts__['transport'] ) - return node_dict + return ret def update_pricing(kwargs=None, call=None): From 425ede4b844524d9e4644dfca5c9cf5f4e91e267 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 10 Oct 2017 11:24:57 -0500 Subject: [PATCH 545/633] Fix on_header callback when not redirecting and no Content-Type present --- salt/fileclient.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/salt/fileclient.py b/salt/fileclient.py index 26d831fdd0..872b206dce 100644 --- a/salt/fileclient.py +++ b/salt/fileclient.py @@ -622,12 +622,19 @@ class Client(object): def on_header(hdr): if write_body[1] is not False and write_body[2] is None: if not hdr.strip() and 'Content-Type' not in write_body[1]: - # We've reached the end of the headers and not yet - # found the Content-Type. Reset write_body[0] so that - # we properly follow the redirect. Note that slicing is - # used below to ensure that we re-use the same list - # rather than creating a new one. - write_body[0:2] = (None, False) + if write_body[0] is True: + # We are not following a redirect (initial response + # was a 200 OK). So there is no need to reset + # write_body[0], but we do need to set the encoding + # since no Content-Type was present in the headers + # to tell us which to use. + write_body[2] = 'utf-8' + else: + # We are following a redirect, so we need to reset + # write_body[0] so that we properly follow it. + write_body[0] = None + # We don't need the HTTPHeaders object anymore + write_body[1] = False return # Try to find out what content type encoding is used if # this is a text file From d594b95f9265ddb280120f185f1dfd5b5ca31854 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 10 Oct 2017 11:36:58 -0500 Subject: [PATCH 546/633] No need to set a specific encoding if one hasn't been provided via the headers --- salt/fileclient.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/salt/fileclient.py b/salt/fileclient.py index 872b206dce..7b4e2235df 100644 --- a/salt/fileclient.py +++ b/salt/fileclient.py @@ -622,14 +622,10 @@ class Client(object): def on_header(hdr): if write_body[1] is not False and write_body[2] is None: if not hdr.strip() and 'Content-Type' not in write_body[1]: - if write_body[0] is True: - # We are not following a redirect (initial response - # was a 200 OK). So there is no need to reset - # write_body[0], but we do need to set the encoding - # since no Content-Type was present in the headers - # to tell us which to use. - write_body[2] = 'utf-8' - else: + # If write_body[0] is True, then we are not following a + # redirect (initial response was a 200 OK). So there is + # no need to reset write_body[0]. + if write_body[0] is not True: # We are following a redirect, so we need to reset # write_body[0] so that we properly follow it. write_body[0] = None From c5dde87d888552c9315c3b1d842ff7304fa4e696 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Tue, 10 Oct 2017 02:42:17 +0200 Subject: [PATCH 547/633] Renamed digitalocean cloud module doc file --- ...s.digital_ocean.rst => salt.cloud.clouds.digitalocean.rst} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename doc/ref/clouds/all/{salt.cloud.clouds.digital_ocean.rst => salt.cloud.clouds.digitalocean.rst} (50%) diff --git a/doc/ref/clouds/all/salt.cloud.clouds.digital_ocean.rst b/doc/ref/clouds/all/salt.cloud.clouds.digitalocean.rst similarity index 50% rename from doc/ref/clouds/all/salt.cloud.clouds.digital_ocean.rst rename to doc/ref/clouds/all/salt.cloud.clouds.digitalocean.rst index 1eeb2b2a41..7b8f32b5c7 100644 --- a/doc/ref/clouds/all/salt.cloud.clouds.digital_ocean.rst +++ b/doc/ref/clouds/all/salt.cloud.clouds.digitalocean.rst @@ -1,6 +1,6 @@ -=============================== +============================== salt.cloud.clouds.digitalocean -=============================== +============================== .. automodule:: salt.cloud.clouds.digitalocean :members: \ No newline at end of file From 6c303448247b3968213c1043a9b39688b82a46f0 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Tue, 10 Oct 2017 19:02:35 +0200 Subject: [PATCH 548/633] Added missing tutorial docs to the tutorial index --- doc/topics/tutorials/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/topics/tutorials/index.rst b/doc/topics/tutorials/index.rst index 7bb26a6001..313d7fb876 100644 --- a/doc/topics/tutorials/index.rst +++ b/doc/topics/tutorials/index.rst @@ -33,3 +33,5 @@ Tutorials Index * :ref:`The macOS (Maverick) Developer Step By Step Guide To Salt Installation ` * :ref:`SaltStack Walk-through ` * :ref:`Writing Salt Tests ` +* :ref:`Running Salt States and Commands in Docker Containers ` +* :ref:`Preseed Minion with Accepted Key ` From bc53598027f731fdc61da6a7ccd1792cd814f8c0 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Tue, 10 Oct 2017 19:03:07 +0200 Subject: [PATCH 549/633] Fixed spelling mistake in salt_bootstrap tutorial --- doc/topics/tutorials/salt_bootstrap.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/topics/tutorials/salt_bootstrap.rst b/doc/topics/tutorials/salt_bootstrap.rst index d5d93a3aa4..93b5dc3b2d 100644 --- a/doc/topics/tutorials/salt_bootstrap.rst +++ b/doc/topics/tutorials/salt_bootstrap.rst @@ -23,7 +23,7 @@ Supported Operating Systems .. note:: In the event you do not see your distribution or version available please - review the develop branch on GitHub as it main contain updates that are + review the develop branch on GitHub as it may contain updates that are not present in the stable release: https://github.com/saltstack/salt-bootstrap/tree/develop From ac363eaae7178c06997b9a81845ef4ad6c98177e Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Tue, 10 Oct 2017 20:32:29 +0200 Subject: [PATCH 550/633] Fixec back-ticks in sensehat module --- salt/modules/sensehat.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/salt/modules/sensehat.py b/salt/modules/sensehat.py index 0b1006f4a0..2849f5374f 100644 --- a/salt/modules/sensehat.py +++ b/salt/modules/sensehat.py @@ -52,7 +52,7 @@ def set_pixels(pixels): Sets the entire LED matrix based on a list of 64 pixel values pixels - A list of 64 `[R, G, B]` color values. + A list of 64 ``[R, G, B]`` color values. ''' _sensehat.set_pixels(pixels) return {'pixels': pixels} @@ -60,12 +60,12 @@ def set_pixels(pixels): def get_pixels(): ''' - Returns a list of 64 smaller lists of `[R, G, B]` pixels representing the + Returns a list of 64 smaller lists of ``[R, G, B]`` pixels representing the the currently displayed image on the LED matrix. .. note:: - When using `set_pixels` the pixel values can sometimes change when - you read them again using `get_pixels`. This is because we specify each + When using ``set_pixels`` the pixel values can sometimes change when + you read them again using ``get_pixels``. This is because we specify each pixel element as 8 bit numbers (0 to 255) but when they're passed into the Linux frame buffer for the LED matrix the numbers are bit shifted down to fit into RGB 565. 5 bits for red, 6 bits for green and 5 bits for blue. @@ -73,8 +73,8 @@ def get_pixels(): (3 bits lost for red, 2 for green and 3 for blue) accounts for the discrepancies you see. - The `get_pixels` method provides an accurate representation of how the - pixels end up in frame buffer memory after you have called `set_pixels`. + The ``get_pixels`` method provides an accurate representation of how the + pixels end up in frame buffer memory after you have called ``set_pixels``. ''' return _sensehat.get_pixels() @@ -88,7 +88,7 @@ def set_pixel(x, y, color): y The y coordinate of the pixel. Ranges from 0 at the top to 7 at the bottom. color - The new color of the pixel as a list of `[R, G, B]` values. + The new color of the pixel as a list of ``[R, G, B]`` values. CLI Example: @@ -110,7 +110,7 @@ def get_pixel(x, y): The y coordinate of the pixel. Ranges from 0 at the top to 7 at the bottom. .. note:: - Please read the note for `get_pixels` + Please read the note for ``get_pixels`` ''' return _sensehat.get_pixel(x, y) @@ -259,9 +259,9 @@ def get_pressure(): def get_temperature(): ''' Gets the temperature in degrees Celsius from the humidity sensor. - Equivalent to calling `get_temperature_from_humidity`. + Equivalent to calling ``get_temperature_from_humidity``. - If you get strange results try using `get_temperature_from_pressure`. + If you get strange results try using ``get_temperature_from_pressure``. ''' return _sensehat.get_temperature() From 349782635d3fb98b9efdc0476107b5615508619c Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Tue, 10 Oct 2017 11:38:41 -0700 Subject: [PATCH 551/633] This should be mount_opts and I typoed it as opts. Fixing that. --- salt/states/mount.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/states/mount.py b/salt/states/mount.py index dd609a2c7a..09a705ee71 100644 --- a/salt/states/mount.py +++ b/salt/states/mount.py @@ -644,7 +644,7 @@ def mounted(name, device, mkmnt=mkmnt, fstype=fstype, - opts=opts) + mount_opts=opts) if out == 'present': ret['comment'] += '. Entry already exists in the fstab.' From 7f9015eb41098d14e6a4c9fe43a5d34016f24e4e Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Tue, 10 Oct 2017 15:31:19 -0400 Subject: [PATCH 552/633] Add 2016.11.9 Release Note File --- doc/topics/releases/2016.11.9.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/topics/releases/2016.11.9.rst diff --git a/doc/topics/releases/2016.11.9.rst b/doc/topics/releases/2016.11.9.rst new file mode 100644 index 0000000000..44c0a7d5ca --- /dev/null +++ b/doc/topics/releases/2016.11.9.rst @@ -0,0 +1,6 @@ +============================ +Salt 2016.11.9 Release Notes +============================ + +Version 2016.11.9 is a bugfix release for :ref:`2016.11.0 `.] + From 027f50936887b5c83a8777d8820fe3cb3163a730 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Tue, 10 Oct 2017 15:33:42 -0400 Subject: [PATCH 553/633] Add 2017.7.3 Release Note File --- doc/topics/releases/2017.7.3.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/topics/releases/2017.7.3.rst diff --git a/doc/topics/releases/2017.7.3.rst b/doc/topics/releases/2017.7.3.rst new file mode 100644 index 0000000000..6b99239712 --- /dev/null +++ b/doc/topics/releases/2017.7.3.rst @@ -0,0 +1,6 @@ +============================ +Salt 2017.7.3 Release Notes +============================ + +Version 2017.7.3 is a bugfix release for :ref:`2017.7.0 `. + From 00dbba571219f8aa8adbf0695ba6c43031983320 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 4 Oct 2017 17:31:37 -0600 Subject: [PATCH 554/633] Fix `unit.test_pillar` for Windows Use delete=True in NamedTemporaryFile command otherwise the handle to the file isn't released and the file can't be opened by the actual test. --- tests/unit/test_pillar.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_pillar.py b/tests/unit/test_pillar.py index 2c385f8085..5076e70b44 100644 --- a/tests/unit/test_pillar.py +++ b/tests/unit/test_pillar.py @@ -192,7 +192,7 @@ class PillarTestCase(TestCase): def _setup_test_topfile_mocks(self, Matcher, get_file_client, nodegroup_order, glob_order): # Write a simple topfile and two pillar state files - self.top_file = tempfile.NamedTemporaryFile(dir=TMP) + self.top_file = tempfile.NamedTemporaryFile(dir=TMP, delete=False) s = ''' base: group: @@ -209,19 +209,19 @@ base: '''.format(nodegroup_order=nodegroup_order, glob_order=glob_order) self.top_file.write(salt.utils.to_bytes(s)) self.top_file.flush() - self.ssh_file = tempfile.NamedTemporaryFile(dir=TMP) + self.ssh_file = tempfile.NamedTemporaryFile(dir=TMP, delete=False) self.ssh_file.write(b''' ssh: foo ''') self.ssh_file.flush() - self.ssh_minion_file = tempfile.NamedTemporaryFile(dir=TMP) + self.ssh_minion_file = tempfile.NamedTemporaryFile(dir=TMP, delete=False) self.ssh_minion_file.write(b''' ssh: bar ''') self.ssh_minion_file.flush() - self.generic_file = tempfile.NamedTemporaryFile(dir=TMP) + self.generic_file = tempfile.NamedTemporaryFile(dir=TMP, delete=False) self.generic_file.write(b''' generic: key1: @@ -231,7 +231,7 @@ generic: sub_key1: [] ''') self.generic_file.flush() - self.generic_minion_file = tempfile.NamedTemporaryFile(dir=TMP) + self.generic_minion_file = tempfile.NamedTemporaryFile(dir=TMP, delete=False) self.generic_minion_file.write(b''' generic: key1: @@ -260,7 +260,7 @@ generic: client.get_state.side_effect = get_state def _setup_test_include_mocks(self, Matcher, get_file_client): - self.top_file = top_file = tempfile.NamedTemporaryFile(dir=TMP) + self.top_file = top_file = tempfile.NamedTemporaryFile(dir=TMP, delete=False) top_file.write(b''' base: '*': @@ -271,21 +271,21 @@ base: - test ''') top_file.flush() - self.init_sls = init_sls = tempfile.NamedTemporaryFile(dir=TMP) + self.init_sls = init_sls = tempfile.NamedTemporaryFile(dir=TMP, delete=False) init_sls.write(b''' include: - test.sub1 - test.sub2 ''') init_sls.flush() - self.sub1_sls = sub1_sls = tempfile.NamedTemporaryFile(dir=TMP) + self.sub1_sls = sub1_sls = tempfile.NamedTemporaryFile(dir=TMP, delete=False) sub1_sls.write(b''' p1: - value1_1 - value1_2 ''') sub1_sls.flush() - self.sub2_sls = sub2_sls = tempfile.NamedTemporaryFile(dir=TMP) + self.sub2_sls = sub2_sls = tempfile.NamedTemporaryFile(dir=TMP, delete=False) sub2_sls.write(b''' p1: - value1_3 From 266dc00a2352941c4670557d76c9058487bf176e Mon Sep 17 00:00:00 2001 From: "David A. Pocock" Date: Tue, 10 Oct 2017 15:04:00 -0500 Subject: [PATCH 555/633] Typo correction of lover to lower --- doc/ref/configuration/minion.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index 31317a06fc..33ed5e40a6 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -1164,7 +1164,7 @@ be able to execute a certain module. The ``sys`` module is built into the minion and cannot be disabled. This setting can also tune the minion. Because all modules are loaded into system -memory, disabling modules will lover the minion's memory footprint. +memory, disabling modules will lower the minion's memory footprint. Modules should be specified according to their file name on the system and not by their virtual name. For example, to disable ``cmd``, use the string ``cmdmod`` which From a37e0bad62ef1465b309d856011fc2451d5eec6c Mon Sep 17 00:00:00 2001 From: Arthur Lutz Date: Fri, 6 Oct 2017 13:10:34 +0200 Subject: [PATCH 556/633] [log/sentry] avoid KeyError: 'SENTRY_PROJECT' if compute_scope is not present on transport registry, we can't update options dictionnary with empty dsn_config dictionary fix #43949 --- salt/log/handlers/sentry_mod.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/salt/log/handlers/sentry_mod.py b/salt/log/handlers/sentry_mod.py index 496c9929a1..229ee02bb1 100644 --- a/salt/log/handlers/sentry_mod.py +++ b/salt/log/handlers/sentry_mod.py @@ -128,12 +128,12 @@ def setup_handlers(): callable(transport_registry.compute_scope)): conf_extras = transport_registry.compute_scope(url, dsn_config) dsn_config.update(conf_extras) - options.update({ - 'project': dsn_config['SENTRY_PROJECT'], - 'servers': dsn_config['SENTRY_SERVERS'], - 'public_key': dsn_config['SENTRY_PUBLIC_KEY'], - 'secret_key': dsn_config['SENTRY_SECRET_KEY'] - }) + options.update({ + 'project': dsn_config['SENTRY_PROJECT'], + 'servers': dsn_config['SENTRY_SERVERS'], + 'public_key': dsn_config['SENTRY_PUBLIC_KEY'], + 'secret_key': dsn_config['SENTRY_SECRET_KEY'] + }) except ValueError as exc: log.info( 'Raven failed to parse the configuration provided ' From 44060dc9c1cc6c84f2e2b2ce11100e307fcf9d0d Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 25 Aug 2017 14:15:58 -0500 Subject: [PATCH 557/633] Do not allow IDs with null bytes in decoded payloads --- salt/crypt.py | 3 +++ salt/transport/tcp.py | 11 +++++++++++ salt/transport/zeromq.py | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/salt/crypt.py b/salt/crypt.py index f4f65940d5..ef4e7645d2 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -607,6 +607,9 @@ class AsyncAuth(object): raise tornado.gen.Return('retry') else: raise SaltClientError('Attempt to authenticate with the salt master failed with timeout error') + if not isinstance(payload, dict): + log.error('Sign-in attempt failed: %s', payload) + raise tornado.gen.Return(False) if 'load' in payload: if 'ret' in payload['load']: if not payload['load']['ret']: diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py index a9001f03a5..f274240a1e 100644 --- a/salt/transport/tcp.py +++ b/salt/transport/tcp.py @@ -623,6 +623,17 @@ class TCPReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt.tra 'payload and load must be a dict', header=header)) raise tornado.gen.Return() + try: + id_ = payload['load'].get('id', '') + if '\0' in id_: + log.error('Payload contains an id with a null byte: %s', payload) + stream.send(self.serial.dumps('bad load: id contains a null byte')) + raise tornado.gen.Return() + except TypeError: + log.error('Payload contains non-string id: %s', payload) + stream.send(self.serial.dumps('bad load: id {0} is not a string'.format(id_))) + raise tornado.gen.Return() + # intercept the "_auth" commands, since the main daemon shouldn't know # anything about our key auth if payload['enc'] == 'clear' and payload.get('load', {}).get('cmd') == '_auth': diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py index 1bb3abebe1..2be6c829f3 100644 --- a/salt/transport/zeromq.py +++ b/salt/transport/zeromq.py @@ -596,6 +596,17 @@ class ZeroMQReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt. stream.send(self.serial.dumps('payload and load must be a dict')) raise tornado.gen.Return() + try: + id_ = payload['load'].get('id', '') + if '\0' in id_: + log.error('Payload contains an id with a null byte: %s', payload) + stream.send(self.serial.dumps('bad load: id contains a null byte')) + raise tornado.gen.Return() + except TypeError: + log.error('Payload contains non-string id: %s', payload) + stream.send(self.serial.dumps('bad load: id {0} is not a string'.format(id_))) + raise tornado.gen.Return() + # intercept the "_auth" commands, since the main daemon shouldn't know # anything about our key auth if payload['enc'] == 'clear' and payload.get('load', {}).get('cmd') == '_auth': From f7824e41f3105c7a7e6571d8fb66c8671ff94b74 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 23 Aug 2017 10:20:50 -0500 Subject: [PATCH 558/633] Don't allow path separators in minion ID --- salt/utils/verify.py | 15 ++++----------- tests/unit/utils/test_verify.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/salt/utils/verify.py b/salt/utils/verify.py index 6e69283f07..24b2a1d962 100644 --- a/salt/utils/verify.py +++ b/salt/utils/verify.py @@ -480,22 +480,15 @@ def clean_path(root, path, subdir=False): return '' -def clean_id(id_): - ''' - Returns if the passed id is clean. - ''' - if re.search(r'\.\.\{sep}'.format(sep=os.sep), id_): - return False - return True - - def valid_id(opts, id_): ''' Returns if the passed id is valid ''' try: - return bool(clean_path(opts['pki_dir'], id_)) and clean_id(id_) - except (AttributeError, KeyError, TypeError) as e: + if any(x in id_ for x in ('/', '\\', '\0')): + return False + return bool(clean_path(opts['pki_dir'], id_)) + except (AttributeError, KeyError, TypeError): return False diff --git a/tests/unit/utils/test_verify.py b/tests/unit/utils/test_verify.py index 795298877d..fa091f6cb3 100644 --- a/tests/unit/utils/test_verify.py +++ b/tests/unit/utils/test_verify.py @@ -58,6 +58,16 @@ class TestVerify(TestCase): opts = {'pki_dir': '/tmp/whatever'} self.assertFalse(valid_id(opts, None)) + def test_valid_id_pathsep(self): + ''' + Path separators in id should make it invalid + ''' + opts = {'pki_dir': '/tmp/whatever'} + # We have to test both path separators because os.path.normpath will + # convert forward slashes to backslashes on Windows. + for pathsep in ('/', '\\'): + self.assertFalse(valid_id(opts, pathsep.join(('..', 'foobar')))) + def test_zmq_verify(self): self.assertTrue(zmq_version()) From 18fb0be96a146589ccbd642caa9244480c51140b Mon Sep 17 00:00:00 2001 From: Matthew Summers Date: Mon, 9 Oct 2017 20:38:52 -0500 Subject: [PATCH 559/633] addresses issue #43307, disk.format_ to disk.format This change fixes breakage. It appears the disk.format_ func is aliased to disk.format in modules/disk.py --- salt/states/blockdev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/states/blockdev.py b/salt/states/blockdev.py index 4b0dc5ca81..e6ecfeab3f 100644 --- a/salt/states/blockdev.py +++ b/salt/states/blockdev.py @@ -159,7 +159,7 @@ def formatted(name, fs_type='ext4', force=False, **kwargs): ret['result'] = None return ret - __salt__['disk.format_'](name, fs_type, force=force, **kwargs) + __salt__['disk.format'](name, fs_type, force=force, **kwargs) # Repeat fstype check up to 10 times with 3s sleeping between each # to avoid detection failing although mkfs has succeeded From d947ddf1765d05b795ee10784242f07cc0d6783e Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 9 Oct 2017 12:53:31 -0500 Subject: [PATCH 560/633] Move 13 functions from salt.utils to salt.utils.data These functions are: - salt.utils.compare_dicts - salt.utils.compare_lists - salt.utils.decode_dict - salt.utils.decode_list - salt.utils.exactly_n - salt.utils.exactly_one - salt.utils.traverse_dict - salt.utils.filter_by - salt.utils.traverse_dict_and_list - salt.utils.subdict_match - salt.utils.substr_in_list - salt.utils.is_dictlist - salt.utils.repack_dictlist --- doc/topics/development/package_providers.rst | 4 +- salt/client/ssh/wrapper/__init__.py | 4 +- salt/client/ssh/wrapper/config.py | 11 +- salt/client/ssh/wrapper/grains.py | 16 +- salt/client/ssh/wrapper/pillar.py | 16 +- salt/client/ssh/wrapper/state.py | 15 +- salt/cloud/clouds/aliyun.py | 3 +- salt/cloud/clouds/qingcloud.py | 3 +- salt/config/__init__.py | 5 +- salt/daemons/flo/jobber.py | 3 +- salt/fileserver/hgfs.py | 3 +- salt/fileserver/svnfs.py | 3 +- salt/minion.py | 13 +- salt/modules/apk.py | 7 +- salt/modules/aptpkg.py | 11 +- salt/modules/boto_ec2.py | 24 +- salt/modules/cmdmod.py | 3 +- salt/modules/config.py | 46 +- salt/modules/defaults.py | 4 +- salt/modules/dpkg.py | 4 +- salt/modules/dummyproxy_package.py | 4 +- salt/modules/ebuild.py | 11 +- salt/modules/freebsdpkg.py | 5 +- salt/modules/freebsdports.py | 6 +- salt/modules/grains.py | 40 +- salt/modules/kernelpkg_linux_yum.py | 13 +- salt/modules/mac_brew.py | 9 +- salt/modules/mac_ports.py | 9 +- salt/modules/openbsdpkg.py | 7 +- salt/modules/opkg.py | 9 +- salt/modules/pacman.py | 7 +- salt/modules/pillar.py | 41 +- salt/modules/pip.py | 4 +- salt/modules/pkg_resource.py | 5 +- salt/modules/pkgin.py | 9 +- salt/modules/pkgng.py | 9 +- salt/modules/pkgutil.py | 9 +- salt/modules/rbenv.py | 4 +- salt/modules/rest_package.py | 9 +- salt/modules/saltcloudmod.py | 5 +- salt/modules/solarisips.py | 9 +- salt/modules/solarispkg.py | 7 +- salt/modules/state.py | 5 +- salt/modules/tomcat.py | 9 +- salt/modules/win_pkg.py | 9 +- salt/modules/xbpspkg.py | 9 +- salt/modules/yumpkg.py | 11 +- salt/modules/zypper.py | 9 +- salt/pillar/__init__.py | 49 +- salt/pillar/makostack.py | 8 +- salt/pillar/stack.py | 11 +- salt/roster/cache.py | 3 +- salt/runners/config.py | 7 +- salt/sdb/yaml.py | 4 +- salt/serializers/python.py | 4 +- salt/states/boto_cloudtrail.py | 4 +- salt/states/boto_ec2.py | 18 +- salt/states/boto_iam.py | 4 +- salt/states/boto_iot.py | 4 +- salt/states/boto_lambda.py | 4 +- salt/states/boto_rds.py | 4 +- salt/states/chocolatey.py | 9 +- salt/states/docker_network.py | 6 +- salt/states/docker_volume.py | 6 +- salt/states/logadm.py | 4 +- salt/states/pdbedit.py | 4 +- salt/states/ports.py | 4 +- salt/states/saltmod.py | 4 +- salt/states/smartos.py | 13 +- salt/states/win_dism.py | 14 +- salt/states/win_network.py | 4 +- salt/states/win_servermanager.py | 6 +- salt/utils/__init__.py | 670 ++++++++----------- salt/utils/boto.py | 2 +- salt/utils/boto3.py | 2 +- salt/utils/crypt.py | 20 + salt/utils/data.py | 422 ++++++++++++ salt/utils/docker/__init__.py | 6 +- salt/utils/gitfs.py | 5 +- salt/utils/minions.py | 11 +- salt/utils/reactor.py | 5 +- tests/unit/utils/test_reactor.py | 4 +- tests/unit/utils/test_utils.py | 51 +- 83 files changed, 1128 insertions(+), 749 deletions(-) create mode 100644 salt/utils/data.py diff --git a/doc/topics/development/package_providers.rst b/doc/topics/development/package_providers.rst index 1b3cd2e110..fb71918df6 100644 --- a/doc/topics/development/package_providers.rst +++ b/doc/topics/development/package_providers.rst @@ -137,11 +137,11 @@ The second return value will be a string with two possible values: Both before and after the installing the target(s), you should run :strong:`list_pkgs` to obtain a list of the installed packages. You should then -return the output of ``salt.utils.compare_dicts()`` +return the output of ``salt.utils.data.compare_dicts()``: .. code-block:: python - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) remove diff --git a/salt/client/ssh/wrapper/__init__.py b/salt/client/ssh/wrapper/__init__.py index 3c67f1f7af..abaef82746 100644 --- a/salt/client/ssh/wrapper/__init__.py +++ b/salt/client/ssh/wrapper/__init__.py @@ -13,7 +13,7 @@ import copy # Import salt libs import salt.loader -import salt.utils +import salt.utils.data import salt.client.ssh # Import 3rd-party libs @@ -121,7 +121,7 @@ class FunctionWrapper(object): u'stderr': stderr, u'retcode': retcode} try: - ret = json.loads(stdout, object_hook=salt.utils.decode_dict) + ret = json.loads(stdout, object_hook=salt.utils.data.decode_dict) if len(ret) < 2 and u'local' in ret: ret = ret[u'local'] ret = ret.get(u'return', {}) diff --git a/salt/client/ssh/wrapper/config.py b/salt/client/ssh/wrapper/config.py index beb66d2beb..cf97316928 100644 --- a/salt/client/ssh/wrapper/config.py +++ b/salt/client/ssh/wrapper/config.py @@ -9,7 +9,8 @@ import re import os # Import salt libs -import salt.utils +import salt.utils # Can be removed once normalize_mode is moved +import salt.utils.data import salt.syspaths as syspaths # Import 3rd-party libs @@ -215,16 +216,16 @@ def get(key, default=u''): salt '*' config.get pkg:apache ''' - ret = salt.utils.traverse_dict_and_list(__opts__, key, u'_|-') + ret = salt.utils.data.traverse_dict_and_list(__opts__, key, u'_|-') if ret != u'_|-': return ret - ret = salt.utils.traverse_dict_and_list(__grains__, key, u'_|-') + ret = salt.utils.data.traverse_dict_and_list(__grains__, key, u'_|-') if ret != u'_|-': return ret - ret = salt.utils.traverse_dict_and_list(__pillar__, key, u'_|-') + ret = salt.utils.data.traverse_dict_and_list(__pillar__, key, u'_|-') if ret != u'_|-': return ret - ret = salt.utils.traverse_dict_and_list(__pillar__.get(u'master', {}), key, u'_|-') + ret = salt.utils.data.traverse_dict_and_list(__pillar__.get(u'master', {}), key, u'_|-') if ret != u'_|-': return ret return default diff --git a/salt/client/ssh/wrapper/grains.py b/salt/client/ssh/wrapper/grains.py index 3c0fdf9272..1d610da52d 100644 --- a/salt/client/ssh/wrapper/grains.py +++ b/salt/client/ssh/wrapper/grains.py @@ -11,7 +11,8 @@ import math import json # Import salt libs -import salt.utils +import salt.utils # Can be removed once is_true is moved +import salt.utils.data import salt.utils.dictupdate from salt.defaults import DEFAULT_TARGET_DELIM from salt.exceptions import SaltException @@ -75,10 +76,11 @@ def get(key, default=u'', delimiter=DEFAULT_TARGET_DELIM, ordered=True): grains = __grains__ else: grains = json.loads(json.dumps(__grains__)) - return salt.utils.traverse_dict_and_list(__grains__, - key, - default, - delimiter) + return salt.utils.data.traverse_dict_and_list( + __grains__, + key, + default, + delimiter) def has_value(key): @@ -99,7 +101,9 @@ def has_value(key): salt '*' grains.has_value pkg:apache ''' - return True if salt.utils.traverse_dict_and_list(__grains__, key, False) else False + return True \ + if salt.utils.data.traverse_dict_and_list(__grains__, key, False) \ + else False def items(sanitize=False): diff --git a/salt/client/ssh/wrapper/pillar.py b/salt/client/ssh/wrapper/pillar.py index 8b71558e4a..c6096d5f7c 100644 --- a/salt/client/ssh/wrapper/pillar.py +++ b/salt/client/ssh/wrapper/pillar.py @@ -9,7 +9,8 @@ import collections # Import salt libs import salt.pillar -import salt.utils +import salt.utils.data +import salt.utils.dictupdate from salt.defaults import DEFAULT_TARGET_DELIM @@ -51,15 +52,16 @@ def get(key, default=u'', merge=False, delimiter=DEFAULT_TARGET_DELIM): salt '*' pillar.get pkg:apache ''' if merge: - ret = salt.utils.traverse_dict_and_list(__pillar__, key, {}, delimiter) + ret = salt.utils.data.traverse_dict_and_list(__pillar__, key, {}, delimiter) if isinstance(ret, collections.Mapping) and \ isinstance(default, collections.Mapping): return salt.utils.dictupdate.update(default, ret) - return salt.utils.traverse_dict_and_list(__pillar__, - key, - default, - delimiter) + return salt.utils.data.traverse_dict_and_list( + __pillar__, + key, + default, + delimiter) def item(*args): @@ -126,7 +128,7 @@ def keys(key, delimiter=DEFAULT_TARGET_DELIM): salt '*' pillar.keys web:sites ''' - ret = salt.utils.traverse_dict_and_list( + ret = salt.utils.data.traverse_dict_and_list( __pillar__, key, KeyError, delimiter) if ret is KeyError: diff --git a/salt/client/ssh/wrapper/state.py b/salt/client/ssh/wrapper/state.py index d927ce2e55..bc4c610a28 100644 --- a/salt/client/ssh/wrapper/state.py +++ b/salt/client/ssh/wrapper/state.py @@ -13,7 +13,8 @@ import logging # Import salt libs import salt.client.ssh.shell import salt.client.ssh.state -import salt.utils +import salt.utils # Can be removed once get_hash, test_mode are moved +import salt.utils.data import salt.utils.thin import salt.roster import salt.state @@ -125,7 +126,7 @@ def sls(mods, saltenv=u'base', test=None, exclude=None, **kwargs): # Read in the JSON data and return the data structure try: - return json.loads(stdout, object_hook=salt.utils.decode_dict) + return json.loads(stdout, object_hook=salt.utils.data.decode_dict) except Exception as e: log.error(u"JSON Render failed for: %s\n%s", stdout, stderr) log.error(str(e)) @@ -201,7 +202,7 @@ def low(data, **kwargs): # Read in the JSON data and return the data structure try: - return json.loads(stdout, object_hook=salt.utils.decode_dict) + return json.loads(stdout, object_hook=salt.utils.data.decode_dict) except Exception as e: log.error(u"JSON Render failed for: %s\n%s", stdout, stderr) log.error(str(e)) @@ -274,7 +275,7 @@ def high(data, **kwargs): # Read in the JSON data and return the data structure try: - return json.loads(stdout, object_hook=salt.utils.decode_dict) + return json.loads(stdout, object_hook=salt.utils.data.decode_dict) except Exception as e: log.error(u"JSON Render failed for: %s\n%s", stdout, stderr) log.error(str(e)) @@ -378,7 +379,7 @@ def highstate(test=None, **kwargs): # Read in the JSON data and return the data structure try: - return json.loads(stdout, object_hook=salt.utils.decode_dict) + return json.loads(stdout, object_hook=salt.utils.data.decode_dict) except Exception as e: log.error(u"JSON Render failed for: %s\n%s", stdout, stderr) log.error(str(e)) @@ -458,7 +459,7 @@ def top(topfn, test=None, **kwargs): # Read in the JSON data and return the data structure try: - return json.loads(stdout, object_hook=salt.utils.decode_dict) + return json.loads(stdout, object_hook=salt.utils.data.decode_dict) except Exception as e: log.error(u"JSON Render failed for: %s\n%s", stdout, stderr) log.error(str(e)) @@ -728,7 +729,7 @@ def single(fun, name, test=None, **kwargs): # Read in the JSON data and return the data structure try: - return json.loads(stdout, object_hook=salt.utils.decode_dict) + return json.loads(stdout, object_hook=salt.utils.data.decode_dict) except Exception as e: log.error(u"JSON Render failed for: %s\n%s", stdout, stderr) log.error(str(e)) diff --git a/salt/cloud/clouds/aliyun.py b/salt/cloud/clouds/aliyun.py index 1980e8f175..92f2f61a18 100644 --- a/salt/cloud/clouds/aliyun.py +++ b/salt/cloud/clouds/aliyun.py @@ -42,6 +42,7 @@ from salt.ext.six.moves.urllib.parse import quote as _quote # pylint: disable=i # Import salt cloud libs import salt.utils.cloud +import salt.utils.data import salt.config as config from salt.exceptions import ( SaltCloudNotFound, @@ -823,7 +824,7 @@ def query(params=None): content = request.text - result = json.loads(content, object_hook=salt.utils.decode_dict) + result = json.loads(content, object_hook=salt.utils.data.decode_dict) if 'Code' in result: raise SaltCloudSystemExit( pprint.pformat(result.get('Message', {})) diff --git a/salt/cloud/clouds/qingcloud.py b/salt/cloud/clouds/qingcloud.py index 702980d557..3458cd405c 100644 --- a/salt/cloud/clouds/qingcloud.py +++ b/salt/cloud/clouds/qingcloud.py @@ -40,6 +40,7 @@ from hashlib import sha256 from salt.ext.six.moves.urllib.parse import quote as _quote # pylint: disable=import-error,no-name-in-module from salt.ext.six.moves import range import salt.utils.cloud +import salt.utils.data import salt.config as config from salt.exceptions import ( SaltCloudNotFound, @@ -187,7 +188,7 @@ def query(params=None): log.debug(request.url) content = request.text - result = json.loads(content, object_hook=salt.utils.decode_dict) + result = json.loads(content, object_hook=salt.utils.data.decode_dict) # print('response:') # pprint.pprint(result) diff --git a/salt/config/__init__.py b/salt/config/__init__.py index 2caf21a403..76ba7f5d66 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -25,6 +25,7 @@ from salt.ext.six.moves.urllib.parse import urlparse # Import salt libs import salt.utils # Can be removed once is_dictlist, ip_bracket are moved +import salt.utils.data import salt.utils.dictupdate import salt.utils.files import salt.utils.network @@ -3696,8 +3697,8 @@ def master_config(path, env_var='SALT_MASTER_CONFIG', defaults=None, exit_on_con # out or not present. if opts.get('nodegroups') is None: opts['nodegroups'] = DEFAULT_MASTER_OPTS.get('nodegroups', {}) - if salt.utils.is_dictlist(opts['nodegroups']): - opts['nodegroups'] = salt.utils.repack_dictlist(opts['nodegroups']) + if salt.utils.data.is_dictlist(opts['nodegroups']): + opts['nodegroups'] = salt.utils.data.repack_dictlist(opts['nodegroups']) if opts.get('transport') == 'raet' and 'aes' in opts: opts.pop('aes') apply_sdb(opts) diff --git a/salt/daemons/flo/jobber.py b/salt/daemons/flo/jobber.py index e18edc3c66..da11b7e5c6 100644 --- a/salt/daemons/flo/jobber.py +++ b/salt/daemons/flo/jobber.py @@ -21,6 +21,7 @@ from salt.ext import six import salt.daemons.masterapi import salt.utils import salt.utils.args +import salt.utils.data import salt.utils.files import salt.utils.kinds as kinds import salt.utils.stringutils @@ -61,7 +62,7 @@ def jobber_check(self): rms.append(jid) data = self.shells.value[jid] stdout, stderr = data['proc'].communicate() - ret = json.loads(salt.utils.stringutils.to_str(stdout), object_hook=salt.utils.decode_dict)['local'] + ret = json.loads(salt.utils.stringutils.to_str(stdout), object_hook=salt.utils.data.decode_dict)['local'] route = {'src': (self.stack.value.local.name, 'manor', 'jid_ret'), 'dst': (data['msg']['route']['src'][0], None, 'remote_cmd')} ret['cmd'] = '_return' diff --git a/salt/fileserver/hgfs.py b/salt/fileserver/hgfs.py index e386a9e6e8..42c830c528 100644 --- a/salt/fileserver/hgfs.py +++ b/salt/fileserver/hgfs.py @@ -60,6 +60,7 @@ except ImportError: # Import salt libs import salt.utils +import salt.utils.data import salt.utils.files import salt.utils.gzip_util import salt.utils.url @@ -203,7 +204,7 @@ def init(): repo_url = next(iter(remote)) per_remote_conf = dict( [(key, six.text_type(val)) for key, val in - six.iteritems(salt.utils.repack_dictlist(remote[repo_url]))] + six.iteritems(salt.utils.data.repack_dictlist(remote[repo_url]))] ) if not per_remote_conf: log.error( diff --git a/salt/fileserver/svnfs.py b/salt/fileserver/svnfs.py index 2ba3cc227e..cc60fbb312 100644 --- a/salt/fileserver/svnfs.py +++ b/salt/fileserver/svnfs.py @@ -55,6 +55,7 @@ except ImportError: # Import salt libs import salt.utils +import salt.utils.data import salt.utils.files import salt.utils.gzip_util import salt.utils.url @@ -139,7 +140,7 @@ def init(): repo_url = next(iter(remote)) per_remote_conf = dict( [(key, six.text_type(val)) for key, val in - six.iteritems(salt.utils.repack_dictlist(remote[repo_url]))] + six.iteritems(salt.utils.data.repack_dictlist(remote[repo_url]))] ) if not per_remote_conf: log.error( diff --git a/salt/minion.py b/salt/minion.py index 82f7a9db3a..885fb0541b 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -89,6 +89,7 @@ import salt.syspaths import salt.utils import salt.utils.args import salt.utils.context +import salt.utils.data import salt.utils.error import salt.utils.event import salt.utils.files @@ -2963,7 +2964,7 @@ class Matcher(object): log.error(u'Got insufficient arguments for grains match ' u'statement from master') return False - return salt.utils.subdict_match( + return salt.utils.data.subdict_match( self.opts[u'grains'], tgt, delimiter=delimiter ) @@ -2976,8 +2977,8 @@ class Matcher(object): log.error(u'Got insufficient arguments for grains pcre match ' u'statement from master') return False - return salt.utils.subdict_match(self.opts[u'grains'], tgt, - delimiter=delimiter, regex_match=True) + return salt.utils.data.subdict_match( + self.opts[u'grains'], tgt, delimiter=delimiter, regex_match=True) def data_match(self, tgt): ''' @@ -3017,7 +3018,7 @@ class Matcher(object): log.error(u'Got insufficient arguments for pillar match ' u'statement from master') return False - return salt.utils.subdict_match( + return salt.utils.data.subdict_match( self.opts[u'pillar'], tgt, delimiter=delimiter ) @@ -3030,7 +3031,7 @@ class Matcher(object): log.error(u'Got insufficient arguments for pillar PCRE match ' u'statement from master') return False - return salt.utils.subdict_match( + return salt.utils.data.subdict_match( self.opts[u'pillar'], tgt, delimiter=delimiter, regex_match=True ) @@ -3043,7 +3044,7 @@ class Matcher(object): log.error(u'Got insufficient arguments for pillar match ' u'statement from master') return False - return salt.utils.subdict_match(self.opts[u'pillar'], + return salt.utils.data.subdict_match(self.opts[u'pillar'], tgt, delimiter=delimiter, exact_match=True) diff --git a/salt/modules/apk.py b/salt/modules/apk.py index 2674158bd6..51c1ad6627 100644 --- a/salt/modules/apk.py +++ b/salt/modules/apk.py @@ -19,6 +19,7 @@ import logging # Import salt libs import salt.utils +import salt.utils.data import salt.utils.itertools from salt.exceptions import CommandExecutionError @@ -338,7 +339,7 @@ def install(name=None, __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -414,7 +415,7 @@ def remove(name=None, pkgs=None, purge=False, **kwargs): # pylint: disable=unus __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -480,7 +481,7 @@ def upgrade(name=None, pkgs=None, refresh=True): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret['changes'] = salt.utils.compare_dicts(old, new) + ret['changes'] = salt.utils.data.compare_dicts(old, new) return ret diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py index 97f2c7f885..692974a6f3 100644 --- a/salt/modules/aptpkg.py +++ b/salt/modules/aptpkg.py @@ -40,6 +40,7 @@ import salt.syspaths from salt.modules.cmdmod import _parse_env import salt.utils import salt.utils.args +import salt.utils.data import salt.utils.files import salt.utils.itertools import salt.utils.path @@ -819,7 +820,7 @@ def install(name=None, __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) for pkgname in to_reinstall: if pkgname not in ret or pkgname in old: @@ -878,10 +879,10 @@ def _uninstall(action='remove', name=None, pkgs=None, **kwargs): new = list_pkgs() new_removed = list_pkgs(removed=True) - changes = salt.utils.compare_dicts(old, new) + changes = salt.utils.data.compare_dicts(old, new) if action == 'purge': ret = { - 'removed': salt.utils.compare_dicts(old_removed, new_removed), + 'removed': salt.utils.data.compare_dicts(old_removed, new_removed), 'installed': changes } else: @@ -951,7 +952,7 @@ def autoremove(list_only=False, purge=False): __salt__['cmd.run'](cmd, python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) def remove(name=None, pkgs=None, **kwargs): @@ -1130,7 +1131,7 @@ def upgrade(refresh=True, dist_upgrade=False, **kwargs): env=DPKG_ENV_VARS.copy()) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( diff --git a/salt/modules/boto_ec2.py b/salt/modules/boto_ec2.py index cd77b8f129..c235a02a8d 100644 --- a/salt/modules/boto_ec2.py +++ b/salt/modules/boto_ec2.py @@ -52,8 +52,8 @@ import time import json # Import Salt libs -import salt.utils import salt.utils.compat +import salt.utils.data from salt.ext import six from salt.exceptions import SaltInvocationError, CommandExecutionError from salt.utils.versions import LooseVersion as _LooseVersion @@ -293,7 +293,7 @@ def release_eip_address(public_ip=None, allocation_id=None, region=None, key=Non .. versionadded:: 2016.3.0 ''' - if not salt.utils.exactly_one((public_ip, allocation_id)): + if not salt.utils.data.exactly_one((public_ip, allocation_id)): raise SaltInvocationError("Exactly one of 'public_ip' OR " "'allocation_id' must be provided") @@ -344,9 +344,9 @@ def associate_eip_address(instance_id=None, instance_name=None, public_ip=None, .. versionadded:: 2016.3.0 ''' - if not salt.utils.exactly_one((instance_id, instance_name, - network_interface_id, - network_interface_name)): + if not salt.utils.data.exactly_one((instance_id, instance_name, + network_interface_id, + network_interface_name)): raise SaltInvocationError("Exactly one of 'instance_id', " "'instance_name', 'network_interface_id', " "'network_interface_name' must be provided") @@ -452,8 +452,8 @@ def assign_private_ip_addresses(network_interface_name=None, network_interface_i .. versionadded:: 2017.7.0 ''' - if not salt.utils.exactly_one((network_interface_name, - network_interface_id)): + if not salt.utils.data.exactly_one((network_interface_name, + network_interface_id)): raise SaltInvocationError("Exactly one of 'network_interface_name', " "'network_interface_id' must be provided") @@ -506,8 +506,8 @@ def unassign_private_ip_addresses(network_interface_name=None, network_interface .. versionadded:: 2017.7.0 ''' - if not salt.utils.exactly_one((network_interface_name, - network_interface_id)): + if not salt.utils.data.exactly_one((network_interface_name, + network_interface_id)): raise SaltInvocationError("Exactly one of 'network_interface_name', " "'network_interface_id' must be provided") @@ -1425,7 +1425,7 @@ def create_network_interface(name, subnet_id=None, subnet_name=None, salt myminion boto_ec2.create_network_interface my_eni subnet-12345 description=my_eni groups=['my_group'] ''' - if not salt.utils.exactly_one((subnet_id, subnet_name)): + if not salt.utils.data.exactly_one((subnet_id, subnet_name)): raise SaltInvocationError('One (but not both) of subnet_id or ' 'subnet_name must be provided.') @@ -1524,13 +1524,13 @@ def attach_network_interface(device_index, name=None, network_interface_id=None, salt myminion boto_ec2.attach_network_interface my_eni instance_name=salt-master device_index=0 ''' - if not salt.utils.exactly_one((name, network_interface_id)): + if not salt.utils.data.exactly_one((name, network_interface_id)): raise SaltInvocationError( "Exactly one (but not both) of 'name' or 'network_interface_id' " "must be provided." ) - if not salt.utils.exactly_one((instance_name, instance_id)): + if not salt.utils.data.exactly_one((instance_name, instance_id)): raise SaltInvocationError( "Exactly one (but not both) of 'instance_name' or 'instance_id' " "must be provided." diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index 653e9a170b..de4f503019 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -26,6 +26,7 @@ import tempfile # Import salt libs import salt.utils import salt.utils.args +import salt.utils.data import salt.utils.files import salt.utils.path import salt.utils.platform @@ -204,7 +205,7 @@ def _parse_env(env): if not env: env = {} if isinstance(env, list): - env = salt.utils.repack_dictlist(env) + env = salt.utils.data.repack_dictlist(env) if not isinstance(env, dict): env = {} return env diff --git a/salt/modules/config.py b/salt/modules/config.py index e7b1445a8d..d9dfeafbaf 100644 --- a/salt/modules/config.py +++ b/salt/modules/config.py @@ -13,6 +13,7 @@ import logging # Import salt libs import salt.config import salt.utils +import salt.utils.data import salt.utils.platform try: # Gated for salt-ssh (salt.utils.cloud imports msgpack) @@ -352,31 +353,35 @@ def get(key, default='', delimiter=':', merge=None): salt '*' config.get lxc.container_profile:centos merge=recurse ''' if merge is None: - ret = salt.utils.traverse_dict_and_list(__opts__, - key, - '_|-', - delimiter=delimiter) + ret = salt.utils.data.traverse_dict_and_list( + __opts__, + key, + '_|-', + delimiter=delimiter) if ret != '_|-': return sdb.sdb_get(ret, __opts__) - ret = salt.utils.traverse_dict_and_list(__grains__, - key, - '_|-', - delimiter) + ret = salt.utils.data.traverse_dict_and_list( + __grains__, + key, + '_|-', + delimiter) if ret != '_|-': return sdb.sdb_get(ret, __opts__) - ret = salt.utils.traverse_dict_and_list(__pillar__, - key, - '_|-', - delimiter=delimiter) + ret = salt.utils.data.traverse_dict_and_list( + __pillar__, + key, + '_|-', + delimiter=delimiter) if ret != '_|-': return sdb.sdb_get(ret, __opts__) - ret = salt.utils.traverse_dict_and_list(__pillar__.get('master', {}), - key, - '_|-', - delimiter=delimiter) + ret = salt.utils.data.traverse_dict_and_list( + __pillar__.get('master', {}), + key, + '_|-', + delimiter=delimiter) if ret != '_|-': return sdb.sdb_get(ret, __opts__) else: @@ -391,10 +396,11 @@ def get(key, default='', delimiter=':', merge=None): data = salt.utils.dictupdate.merge(data, __pillar__, strategy=merge, merge_lists=merge_lists) data = salt.utils.dictupdate.merge(data, __grains__, strategy=merge, merge_lists=merge_lists) data = salt.utils.dictupdate.merge(data, __opts__, strategy=merge, merge_lists=merge_lists) - ret = salt.utils.traverse_dict_and_list(data, - key, - '_|-', - delimiter=delimiter) + ret = salt.utils.data.traverse_dict_and_list( + data, + key, + '_|-', + delimiter=delimiter) if ret != '_|-': return sdb.sdb_get(ret, __opts__) diff --git a/salt/modules/defaults.py b/salt/modules/defaults.py index 8056aef431..75e827c1ad 100644 --- a/salt/modules/defaults.py +++ b/salt/modules/defaults.py @@ -6,7 +6,7 @@ import os import yaml import salt.fileclient -import salt.utils +import salt.utils.data import salt.utils.dictupdate as dictupdate import salt.utils.files import salt.utils.url @@ -97,7 +97,7 @@ def get(key, default=''): # Fetch value if key: - return salt.utils.traverse_dict_and_list(defaults, key, default) + return salt.utils.data.traverse_dict_and_list(defaults, key, default) else: return defaults diff --git a/salt/modules/dpkg.py b/salt/modules/dpkg.py index 0ca210a6e8..69317bbadf 100644 --- a/salt/modules/dpkg.py +++ b/salt/modules/dpkg.py @@ -11,8 +11,8 @@ import re import datetime # Import salt libs -import salt.utils # Can be removed once compare_dicts is moved import salt.utils.args +import salt.utils.data import salt.utils.files import salt.utils.path from salt.exceptions import CommandExecutionError, SaltInvocationError @@ -131,7 +131,7 @@ def unpurge(*packages): ) __context__.pop('pkg.list_pkgs', None) new = __salt__['pkg.list_pkgs'](purge_desired=True) - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) def list_pkgs(*packages): diff --git a/salt/modules/dummyproxy_package.py b/salt/modules/dummyproxy_package.py index 66f0c43637..b6e7219cbe 100644 --- a/salt/modules/dummyproxy_package.py +++ b/salt/modules/dummyproxy_package.py @@ -6,7 +6,7 @@ from __future__ import absolute_import # Import python libs import logging -import salt.utils +import salt.utils.data import salt.utils.platform @@ -80,7 +80,7 @@ def upgrade(name=None, pkgs=None, refresh=True, skip_verify=True, old = __proxy__['dummy.package_list']() new = __proxy__['dummy.uptodate']() pkg_installed = __proxy__['dummy.upgrade']() - ret = salt.utils.compare_dicts(old, pkg_installed) + ret = salt.utils.data.compare_dicts(old, pkg_installed) return ret diff --git a/salt/modules/ebuild.py b/salt/modules/ebuild.py index 5295c2652c..354a37732d 100644 --- a/salt/modules/ebuild.py +++ b/salt/modules/ebuild.py @@ -23,6 +23,7 @@ import re # Import salt libs import salt.utils import salt.utils.args +import salt.utils.data import salt.utils.path import salt.utils.pkg import salt.utils.systemd @@ -711,7 +712,7 @@ def install(name=None, __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - changes.update(salt.utils.compare_dicts(old, new)) + changes.update(salt.utils.data.compare_dicts(old, new)) if needed_changes: raise CommandExecutionError( @@ -804,7 +805,7 @@ def update(pkg, slot=None, fromrepo=None, refresh=False, binhost=None): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if needed_changes: raise CommandExecutionError( @@ -894,7 +895,7 @@ def upgrade(refresh=True, binhost=None, backtrack=3): python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( @@ -993,7 +994,7 @@ def remove(name=None, slot=None, fromrepo=None, pkgs=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -1106,7 +1107,7 @@ def depclean(name=None, slot=None, fromrepo=None, pkgs=None): python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) def version_cmp(pkg1, pkg2, **kwargs): diff --git a/salt/modules/freebsdpkg.py b/salt/modules/freebsdpkg.py index 34d242864f..ebec22ef7f 100644 --- a/salt/modules/freebsdpkg.py +++ b/salt/modules/freebsdpkg.py @@ -81,6 +81,7 @@ import re # Import salt libs import salt.utils +import salt.utils.data import salt.utils.pkg from salt.exceptions import CommandExecutionError, MinionError from salt.ext import six @@ -412,7 +413,7 @@ def install(name=None, __context__.pop('pkg.list_pkgs', None) new = list_pkgs() _rehash() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -474,7 +475,7 @@ def remove(name=None, pkgs=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( diff --git a/salt/modules/freebsdports.py b/salt/modules/freebsdports.py index 1f2c0c3e32..daa3b6fd9a 100644 --- a/salt/modules/freebsdports.py +++ b/salt/modules/freebsdports.py @@ -23,7 +23,7 @@ import re import logging # Import salt libs -import salt.utils +import salt.utils.data import salt.utils.files from salt.ext.six import string_types from salt.exceptions import SaltInvocationError, CommandExecutionError @@ -185,7 +185,7 @@ def install(name, clean=True): __context__['ports.install_error'] = result['stderr'] __context__.pop('pkg.list_pkgs', None) new = __salt__['pkg.list_pkgs']() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if not ret and result['retcode'] == 0: # No change in package list, but the make install was successful. # Assume that the installation was a recompile with new options, and @@ -215,7 +215,7 @@ def deinstall(name): ) __context__.pop('pkg.list_pkgs', None) new = __salt__['pkg.list_pkgs']() - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) def rmconfig(name): diff --git a/salt/modules/grains.py b/salt/modules/grains.py index e59b4b1005..0b21a43672 100644 --- a/salt/modules/grains.py +++ b/salt/modules/grains.py @@ -17,8 +17,9 @@ from functools import reduce # pylint: disable=redefined-builtin # Import Salt libs from salt.ext import six -import salt.utils +import salt.utils # Can be removed once is_true is moved import salt.utils.compat +import salt.utils.data import salt.utils.files import salt.utils.yamldumper from salt.defaults import DEFAULT_TARGET_DELIM @@ -109,15 +110,16 @@ def get(key, default='', delimiter=DEFAULT_TARGET_DELIM, ordered=True): grains = __grains__ else: grains = json.loads(json.dumps(__grains__)) - return salt.utils.traverse_dict_and_list(grains, - key, - default, - delimiter) + return salt.utils.data.traverse_dict_and_list( + grains, + key, + default, + delimiter) def has_value(key): ''' - Determine whether a named value exists in the grains dictionary. + Determine whether a key exists in the grains dictionary. Given a grains dictionary that contains the following structure:: @@ -133,7 +135,10 @@ def has_value(key): salt '*' grains.has_value pkg:apache ''' - return True if salt.utils.traverse_dict_and_list(__grains__, key, False) else False + return salt.utils.data.traverse_dict_and_list( + __grains__, + key, + KeyError) is not KeyError def items(sanitize=False): @@ -185,10 +190,11 @@ def item(*args, **kwargs): try: for arg in args: - ret[arg] = salt.utils.traverse_dict_and_list(__grains__, - arg, - default, - delimiter) + ret[arg] = salt.utils.data.traverse_dict_and_list( + __grains__, + arg, + default, + delimiter) except KeyError: pass @@ -552,12 +558,12 @@ def filter_by(lookup_dict, grain='os_family', merge=None, default='default', bas salt '*' grains.filter_by '{default: {A: {B: C}, D: E}, F: {A: {B: G}}, H: {D: I}}' 'xxx' '{D: J}' 'F' 'default' # next same as above when default='H' instead of 'F' renders {A: {B: C}, D: J} ''' - return salt.utils.filter_by(lookup_dict=lookup_dict, - lookup=grain, - traverse=__grains__, - merge=merge, - default=default, - base=base) + return salt.utils.data.filter_by(lookup_dict=lookup_dict, + lookup=grain, + traverse=__grains__, + merge=merge, + default=default, + base=base) def _dict_from_path(path, val, delimiter=DEFAULT_TARGET_DELIM): diff --git a/salt/modules/kernelpkg_linux_yum.py b/salt/modules/kernelpkg_linux_yum.py index 39110e5a16..661fdc92e7 100644 --- a/salt/modules/kernelpkg_linux_yum.py +++ b/salt/modules/kernelpkg_linux_yum.py @@ -11,11 +11,12 @@ try: from salt.ext import six from salt.utils.versions import LooseVersion as _LooseVersion from salt.exceptions import CommandExecutionError + import salt.utils.data import salt.utils.systemd import salt.modules.yumpkg - HAS_REQUIRED_LIBS = True -except ImportError: - HAS_REQUIRED_LIBS = False + __IMPORT_ERROR = None +except ImportError as exc: + __IMPORT_ERROR = exc.__str__() log = logging.getLogger(__name__) @@ -31,8 +32,8 @@ def __virtual__(): Load this module on RedHat-based systems only ''' - if not HAS_REQUIRED_LIBS: - return (False, "Required library could not be imported") + if __IMPORT_ERROR: + return (False, __IMPORT_ERROR) if __grains__.get('os_family', '') == 'RedHat': return __virtualname__ @@ -233,7 +234,7 @@ def remove(release): # Look for the changes in installed packages __context__.pop('pkg.list_pkgs', None) new = __salt__['pkg.list_pkgs']() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) # Look for command execution errors if out['retcode'] != 0: diff --git a/salt/modules/mac_brew.py b/salt/modules/mac_brew.py index 27252ebd89..076f2873b6 100644 --- a/salt/modules/mac_brew.py +++ b/salt/modules/mac_brew.py @@ -17,7 +17,8 @@ import json import logging # Import salt libs -import salt.utils +import salt.utils # Can be removed when alias_function, is_true are moved +import salt.utils.data import salt.utils.path import salt.utils.pkg import salt.utils.versions @@ -240,7 +241,7 @@ def remove(name=None, pkgs=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -393,7 +394,7 @@ def install(name=None, pkgs=None, taps=None, options=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -480,7 +481,7 @@ def upgrade(refresh=True): result = _call_brew('brew upgrade', failhard=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( diff --git a/salt/modules/mac_ports.py b/salt/modules/mac_ports.py index 4d720e5a03..07bf2c8f90 100644 --- a/salt/modules/mac_ports.py +++ b/salt/modules/mac_ports.py @@ -37,7 +37,8 @@ import logging import re # Import salt libs -import salt.utils +import salt.utils # Can be removed when alias_function, is_true are removed +import salt.utils.data import salt.utils.path import salt.utils.pkg import salt.utils.platform @@ -228,7 +229,7 @@ def remove(name=None, pkgs=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if err_message: raise CommandExecutionError( @@ -338,7 +339,7 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if err_message: raise CommandExecutionError( @@ -431,7 +432,7 @@ def upgrade(refresh=True): # pylint: disable=W0613 python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( diff --git a/salt/modules/openbsdpkg.py b/salt/modules/openbsdpkg.py index 238d879ba2..bcf1123fea 100644 --- a/salt/modules/openbsdpkg.py +++ b/salt/modules/openbsdpkg.py @@ -29,7 +29,8 @@ import re import logging # Import Salt libs -import salt.utils +import salt.utils # Can be removed when is_true is moved +import salt.utils.data import salt.utils.versions from salt.exceptions import CommandExecutionError, MinionError @@ -209,7 +210,7 @@ def install(name=None, pkgs=None, sources=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -268,7 +269,7 @@ def remove(name=None, pkgs=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( diff --git a/salt/modules/opkg.py b/salt/modules/opkg.py index 36284dfab8..2b583b3cf9 100644 --- a/salt/modules/opkg.py +++ b/salt/modules/opkg.py @@ -24,8 +24,9 @@ import re import logging # Import salt libs -import salt.utils # Can be removed when is_true, compare_dicts are moved +import salt.utils # Can be removed when is_true is moved import salt.utils.args +import salt.utils.data import salt.utils.files import salt.utils.itertools import salt.utils.path @@ -386,7 +387,7 @@ def install(name=None, __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if pkg_type == 'file' and reinstall: # For file-based packages, prepare 'to_reinstall' to have a list @@ -474,7 +475,7 @@ def remove(name=None, pkgs=None, **kwargs): # pylint: disable=unused-argument __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -548,7 +549,7 @@ def upgrade(refresh=True): python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( diff --git a/salt/modules/pacman.py b/salt/modules/pacman.py index ab35b9d85d..757387160b 100644 --- a/salt/modules/pacman.py +++ b/salt/modules/pacman.py @@ -20,6 +20,7 @@ import os.path # Import salt libs import salt.utils import salt.utils.args +import salt.utils.data import salt.utils.pkg import salt.utils.itertools import salt.utils.systemd @@ -614,7 +615,7 @@ def install(name=None, __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: try: @@ -688,7 +689,7 @@ def upgrade(refresh=False, root=None, **kwargs): python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( @@ -739,7 +740,7 @@ def _uninstall(action='remove', name=None, pkgs=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( diff --git a/salt/modules/pillar.py b/salt/modules/pillar.py index a81892c069..fa1b091e3b 100644 --- a/salt/modules/pillar.py +++ b/salt/modules/pillar.py @@ -17,8 +17,11 @@ from salt.ext import six # Import salt libs import salt.pillar -import salt.utils +import salt.utils # Can be removed once alias_function is moved import salt.utils.crypt +import salt.utils.data +import salt.utils.dictupdate +import salt.utils.odict from salt.defaults import DEFAULT_TARGET_DELIM from salt.exceptions import CommandExecutionError @@ -125,7 +128,7 @@ def get(key, if merge: if isinstance(default, dict): - ret = salt.utils.traverse_dict_and_list( + ret = salt.utils.data.traverse_dict_and_list( pillar_dict, key, {}, @@ -143,7 +146,7 @@ def get(key, 'skipped.', default, ret, type(ret).__name__ ) elif isinstance(default, list): - ret = salt.utils.traverse_dict_and_list( + ret = salt.utils.data.traverse_dict_and_list( pillar_dict, key, [], @@ -165,10 +168,11 @@ def get(key, default, type(default).__name__ ) - ret = salt.utils.traverse_dict_and_list(pillar_dict, - key, - default, - delimiter) + ret = salt.utils.data.traverse_dict_and_list( + pillar_dict, + key, + default, + delimiter) if ret is KeyError: raise KeyError('Pillar key not found: {0}'.format(key)) @@ -369,10 +373,11 @@ def item(*args, **kwargs): try: for arg in args: - ret[arg] = salt.utils.traverse_dict_and_list(__pillar__, - arg, - default, - delimiter) + ret[arg] = salt.utils.data.traverse_dict_and_list( + __pillar__, + arg, + default, + delimiter) except KeyError: pass @@ -490,7 +495,7 @@ def keys(key, delimiter=DEFAULT_TARGET_DELIM): salt '*' pillar.keys web:sites ''' - ret = salt.utils.traverse_dict_and_list( + ret = salt.utils.data.traverse_dict_and_list( __pillar__, key, KeyError, delimiter) if ret is KeyError: @@ -606,9 +611,9 @@ def filter_by(lookup_dict, salt '*' pillar.filter_by '{web: Serve it up, db: I query, default: x_x}' role ''' - return salt.utils.filter_by(lookup_dict=lookup_dict, - lookup=pillar, - traverse=__pillar__, - merge=merge, - default=default, - base=base) + return salt.utils.data.filter_by(lookup_dict=lookup_dict, + lookup=pillar, + traverse=__pillar__, + merge=merge, + default=default, + base=base) diff --git a/salt/modules/pip.py b/salt/modules/pip.py index 31a59458ee..21dd43d0e1 100644 --- a/salt/modules/pip.py +++ b/salt/modules/pip.py @@ -85,7 +85,7 @@ import sys import tempfile # Import Salt libs -import salt.utils # Can be removed once compare_dicts is moved +import salt.utils.data import salt.utils.files import salt.utils.locales import salt.utils.platform @@ -1216,6 +1216,6 @@ def upgrade(bin_env=None, new = list_(bin_env=bin_env, user=user, cwd=cwd) - ret['changes'] = salt.utils.compare_dicts(old, new) + ret['changes'] = salt.utils.data.compare_dicts(old, new) return ret diff --git a/salt/modules/pkg_resource.py b/salt/modules/pkg_resource.py index 70a2737af9..d72829830b 100644 --- a/salt/modules/pkg_resource.py +++ b/salt/modules/pkg_resource.py @@ -16,7 +16,8 @@ import yaml from salt.ext import six # Import salt libs -import salt.utils +import salt.utils # Can be removed once is_true is moved +import salt.utils.data import salt.utils.versions from salt.exceptions import SaltInvocationError @@ -36,7 +37,7 @@ def _repack_pkgs(pkgs, normalize=True): return dict( [ (_normalize_name(str(x)), str(y) if y is not None else y) - for x, y in six.iteritems(salt.utils.repack_dictlist(pkgs)) + for x, y in six.iteritems(salt.utils.data.repack_dictlist(pkgs)) ] ) diff --git a/salt/modules/pkgin.py b/salt/modules/pkgin.py index a5f253cc16..fdd8ed4b8e 100644 --- a/salt/modules/pkgin.py +++ b/salt/modules/pkgin.py @@ -17,7 +17,8 @@ import os import re # Import salt libs -import salt.utils +import salt.utils # Can be removed when alias_function, is_true are moved +import salt.utils.data import salt.utils.path import salt.utils.pkg import salt.utils.decorators as decorators @@ -397,7 +398,7 @@ def install(name=None, refresh=False, fromrepo=None, __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -440,7 +441,7 @@ def upgrade(): python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( @@ -521,7 +522,7 @@ def remove(name=None, pkgs=None, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( diff --git a/salt/modules/pkgng.py b/salt/modules/pkgng.py index dc20541f94..ceac214a80 100644 --- a/salt/modules/pkgng.py +++ b/salt/modules/pkgng.py @@ -44,7 +44,8 @@ import logging import os # Import salt libs -import salt.utils +import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils.data import salt.utils.files import salt.utils.itertools import salt.utils.pkg @@ -875,7 +876,7 @@ def install(name=None, __context__.pop(_contextkey(jail, chroot, root), None) __context__.pop(_contextkey(jail, chroot, root, prefix='pkg.origin'), None) new = list_pkgs(jail=jail, chroot=chroot, root=root) - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -1050,7 +1051,7 @@ def remove(name=None, __context__.pop(_contextkey(jail, chroot, root), None) __context__.pop(_contextkey(jail, chroot, root, prefix='pkg.origin'), None) new = list_pkgs(jail=jail, chroot=chroot, root=root, with_origin=True) - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -1171,7 +1172,7 @@ def upgrade(*names, **kwargs): __context__.pop(_contextkey(jail, chroot, root), None) __context__.pop(_contextkey(jail, chroot, root, prefix='pkg.origin'), None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( diff --git a/salt/modules/pkgutil.py b/salt/modules/pkgutil.py index 9b16ea56fa..8c8bc95750 100644 --- a/salt/modules/pkgutil.py +++ b/salt/modules/pkgutil.py @@ -14,7 +14,8 @@ from __future__ import absolute_import import copy # Import salt libs -import salt.utils +import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils.data import salt.utils.pkg import salt.utils.versions from salt.exceptions import CommandExecutionError, MinionError @@ -124,7 +125,7 @@ def upgrade(refresh=True): __salt__['cmd.run_all'](cmd) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) def list_pkgs(versions_as_list=False, **kwargs): @@ -302,7 +303,7 @@ def install(name=None, refresh=False, version=None, pkgs=None, **kwargs): __salt__['cmd.run_all'](cmd) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) def remove(name=None, pkgs=None, **kwargs): @@ -346,7 +347,7 @@ def remove(name=None, pkgs=None, **kwargs): __salt__['cmd.run_all'](cmd) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) def purge(name=None, pkgs=None, **kwargs): diff --git a/salt/modules/rbenv.py b/salt/modules/rbenv.py index bcbefa1582..469d809d42 100644 --- a/salt/modules/rbenv.py +++ b/salt/modules/rbenv.py @@ -17,8 +17,8 @@ import re import logging # Import Salt libs -import salt.utils import salt.utils.args +import salt.utils.data import salt.utils.platform from salt.exceptions import SaltInvocationError @@ -62,7 +62,7 @@ def _parse_env(env): if not env: env = {} if isinstance(env, list): - env = salt.utils.repack_dictlist(env) + env = salt.utils.data.repack_dictlist(env) if not isinstance(env, dict): env = {} diff --git a/salt/modules/rest_package.py b/salt/modules/rest_package.py index a0bb1abed8..bc2fb60019 100644 --- a/salt/modules/rest_package.py +++ b/salt/modules/rest_package.py @@ -4,9 +4,11 @@ Package support for the REST example ''' from __future__ import absolute_import -# Import python libs +# Import Python libs import logging -import salt.utils + +# Import Salt libs +import salt.utils.data import salt.utils.platform @@ -72,8 +74,7 @@ def upgrade(refresh=True, skip_verify=True, **kwargs): old = __proxy__['rest_sample.package_list']() new = __proxy__['rest_sample.uptodate']() pkg_installed = __proxy__['rest_sample.upgrade']() - ret = salt.utils.compare_dicts(old, pkg_installed) - return ret + return salt.utils.data.compare_dicts(old, pkg_installed) def installed( diff --git a/salt/modules/saltcloudmod.py b/salt/modules/saltcloudmod.py index 4f6ef9fa3d..2fd8367901 100644 --- a/salt/modules/saltcloudmod.py +++ b/salt/modules/saltcloudmod.py @@ -8,7 +8,8 @@ from __future__ import absolute_import import json # Import salt libs -import salt.utils +import salt.utils.data + HAS_CLOUD = False try: import saltcloud # pylint: disable=W0611 @@ -42,7 +43,7 @@ def create(name, profile): cmd = 'salt-cloud --out json -p {0} {1}'.format(profile, name) out = __salt__['cmd.run_stdout'](cmd, python_shell=False) try: - ret = json.loads(out, object_hook=salt.utils.decode_dict) + ret = json.loads(out, object_hook=salt.utils.data.decode_dict) except ValueError: ret = {} return ret diff --git a/salt/modules/solarisips.py b/salt/modules/solarisips.py index 1e87a46144..106f4d5f83 100644 --- a/salt/modules/solarisips.py +++ b/salt/modules/solarisips.py @@ -43,7 +43,8 @@ import logging # Import salt libs -import salt.utils +import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils.data import salt.utils.path import salt.utils.pkg from salt.exceptions import CommandExecutionError @@ -230,7 +231,7 @@ def upgrade(refresh=False, **kwargs): python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( @@ -509,7 +510,7 @@ def install(name=None, refresh=False, pkgs=None, version=None, test=False, **kwa # Get a list of the packages again, including newly installed ones. __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if out['retcode'] != 0: raise CommandExecutionError( @@ -577,7 +578,7 @@ def remove(name=None, pkgs=None, **kwargs): # Get a list of the packages after the uninstall __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if out['retcode'] != 0: raise CommandExecutionError( diff --git a/salt/modules/solarispkg.py b/salt/modules/solarispkg.py index 84588976d7..e3928e8d3c 100644 --- a/salt/modules/solarispkg.py +++ b/salt/modules/solarispkg.py @@ -16,7 +16,8 @@ import os import logging # Import salt libs -import salt.utils +import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils.data import salt.utils.files from salt.exceptions import CommandExecutionError, MinionError @@ -361,7 +362,7 @@ def install(name=None, sources=None, saltenv='base', **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -487,7 +488,7 @@ def remove(name=None, pkgs=None, saltenv='base', **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( diff --git a/salt/modules/state.py b/salt/modules/state.py index bd2d90893f..e5616af7d7 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -29,6 +29,7 @@ import salt.config import salt.payload import salt.state import salt.utils +import salt.utils.data import salt.utils.event import salt.utils.files import salt.utils.jid @@ -1789,7 +1790,7 @@ def pkg(pkg_path, s_pkg.close() lowstate_json = os.path.join(root, 'lowstate.json') with salt.utils.files.fopen(lowstate_json, 'r') as fp_: - lowstate = json.load(fp_, object_hook=salt.utils.decode_dict) + lowstate = json.load(fp_, object_hook=salt.utils.data.decode_dict) # Check for errors in the lowstate for chunk in lowstate: if not isinstance(chunk, dict): @@ -1804,7 +1805,7 @@ def pkg(pkg_path, roster_grains_json = os.path.join(root, 'roster_grains.json') if os.path.isfile(roster_grains_json): with salt.utils.files.fopen(roster_grains_json, 'r') as fp_: - roster_grains = json.load(fp_, object_hook=salt.utils.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): diff --git a/salt/modules/tomcat.py b/salt/modules/tomcat.py index 41c4e8e577..f5a950ab7b 100644 --- a/salt/modules/tomcat.py +++ b/salt/modules/tomcat.py @@ -84,7 +84,7 @@ from salt.ext.six.moves.urllib.request import ( # pylint: enable=no-name-in-module,import-error # Import Salt libs -import salt.utils +import salt.utils.data log = logging.getLogger(__name__) @@ -146,9 +146,10 @@ def _get_credentials(): # Look for the config key # Support old-style config format and new for config_key in __valid_configs[item]: - value = salt.utils.traverse_dict_and_list(struct, - config_key, - None) + value = salt.utils.data.traverse_dict_and_list( + struct, + config_key, + None) if value: ret[item] = value break diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index bf167934a2..ff23435800 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -50,8 +50,9 @@ from salt.ext.six.moves.urllib.parse import urlparse as _urlparse from salt.exceptions import (CommandExecutionError, SaltInvocationError, SaltRenderError) -import salt.utils # Can be removed once is_true, get_hash, compare_dicts are moved +import salt.utils # Can be removed once is_true, get_hash are moved import salt.utils.args +import salt.utils.data import salt.utils.files import salt.utils.pkg import salt.utils.platform @@ -1388,7 +1389,7 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): ret[pkg_name] = {'current': new[pkg_name]} # Check for changes in the registry - difference = salt.utils.compare_dicts(old, new) + difference = salt.utils.data.compare_dicts(old, new) # Compare the software list before and after # Add the difference to ret @@ -1671,11 +1672,11 @@ def remove(name=None, pkgs=None, version=None, **kwargs): # preparation for the comparison below. __salt__['pkg_resource.stringify'](old) - difference = salt.utils.compare_dicts(old, new) + difference = salt.utils.data.compare_dicts(old, new) tries = 0 while not all(name in difference for name in changed) and tries <= 1000: new = list_pkgs(saltenv=saltenv) - difference = salt.utils.compare_dicts(old, new) + difference = salt.utils.data.compare_dicts(old, new) tries += 1 if tries == 1000: ret['_comment'] = 'Registry not updated.' diff --git a/salt/modules/xbpspkg.py b/salt/modules/xbpspkg.py index 732adcf3aa..33e80be532 100644 --- a/salt/modules/xbpspkg.py +++ b/salt/modules/xbpspkg.py @@ -16,7 +16,8 @@ import logging import glob # Import salt libs -import salt.utils # Can be removed when is_true and compare_dicts are moved +import salt.utils # Can be removed when is_true is moved +import salt.utils.data import salt.utils.files import salt.utils.path import salt.utils.pkg @@ -329,7 +330,7 @@ def upgrade(refresh=True): python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( @@ -424,7 +425,7 @@ def install(name=None, refresh=False, fromrepo=None, new = list_pkgs() _rehash() - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) def remove(name=None, pkgs=None, recursive=True, **kwargs): @@ -478,7 +479,7 @@ def remove(name=None, pkgs=None, recursive=True, **kwargs): __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - return salt.utils.compare_dicts(old, new) + return salt.utils.data.compare_dicts(old, new) def list_repos(): diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index 0136390d7f..4d30a5a785 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -41,8 +41,9 @@ from salt.ext.six.moves import configparser # pylint: enable=import-error,redefined-builtin # Import Salt libs -import salt.utils +import salt.utils # Can be removed once alias_function, is_true, and fnmatch_multiple are moved import salt.utils.args +import salt.utils.data import salt.utils.decorators.path import salt.utils.files import salt.utils.itertools @@ -1680,7 +1681,7 @@ def install(name=None, __context__.pop('pkg.list_pkgs', None) new = list_pkgs(versions_as_list=False, attr=diff_attr) if not downloadonly else list_downloaded() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) for pkgname, _ in to_reinstall: if pkgname not in ret or pkgname in old: @@ -1872,7 +1873,7 @@ def upgrade(name=None, python_shell=False) __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if result['retcode'] != 0: raise CommandExecutionError( @@ -1953,7 +1954,7 @@ def remove(name=None, pkgs=None, **kwargs): # pylint: disable=W0613 __context__.pop('pkg.list_pkgs', None) new = list_pkgs() - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -2144,7 +2145,7 @@ def unhold(name=None, pkgs=None, sources=None, **kwargs): # pylint: disable=W06 targets = [] if pkgs: - for pkg in salt.utils.repack_dictlist(pkgs): + for pkg in salt.utils.data.repack_dictlist(pkgs): targets.append(pkg) elif sources: for source in sources: diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py index 66658ba1f6..0e0a72f7d4 100644 --- a/salt/modules/zypper.py +++ b/salt/modules/zypper.py @@ -32,7 +32,8 @@ from xml.dom import minidom as dom from xml.parsers.expat import ExpatError # Import salt libs -import salt.utils +import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils.data import salt.utils.event import salt.utils.files import salt.utils.path @@ -1203,7 +1204,7 @@ def install(name=None, if isinstance(pkg_data, six.string_types): new[pkg_name] = pkg_data.split(',')[-1] - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if errors: raise CommandExecutionError( @@ -1317,7 +1318,7 @@ def upgrade(refresh=True, # (affects only kernel packages at this point) for pkg in new: new[pkg] = new[pkg].split(',')[-1] - ret = salt.utils.compare_dicts(old, new) + ret = salt.utils.data.compare_dicts(old, new) if __zypper__.exit_code not in __zypper__.SUCCESS_EXIT_CODES: result = { @@ -1360,7 +1361,7 @@ def _uninstall(name=None, pkgs=None): targets = targets[500:] __context__.pop('pkg.list_pkgs', None) - ret = salt.utils.compare_dicts(old, list_pkgs()) + ret = salt.utils.data.compare_dicts(old, list_pkgs()) if errors: raise CommandExecutionError( diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py index e6da1d8c80..0848d76309 100644 --- a/salt/pillar/__init__.py +++ b/salt/pillar/__init__.py @@ -21,16 +21,20 @@ import salt.fileclient import salt.minion import salt.crypt import salt.transport -import salt.utils.url +import salt.utils.args import salt.utils.cache import salt.utils.crypt +import salt.utils.data import salt.utils.dictupdate -import salt.utils.args +import salt.utils.url from salt.exceptions import SaltClientError from salt.template import compile_template -from salt.utils.dictupdate import merge from salt.utils.odict import OrderedDict from salt.version import __version__ +# Even though dictupdate is imported, invoking salt.utils.dictupdate.merge here +# causes an UnboundLocalError. This should be investigated and fixed, but until +# then, leave the import directly below this comment intact. +from salt.utils.dictupdate import merge # Import 3rd-party libs from salt.ext import six @@ -905,11 +909,12 @@ class Pillar(object): ext = None # Bring in CLI pillar data if self.pillar_override: - pillar = merge(pillar, - self.pillar_override, - self.merge_strategy, - self.opts.get('renderer', 'yaml'), - self.opts.get('pillar_merge_lists', False)) + pillar = merge( + pillar, + self.pillar_override, + self.merge_strategy, + self.opts.get('renderer', 'yaml'), + self.opts.get('pillar_merge_lists', False)) for run in self.opts['ext_pillar']: if not isinstance(run, dict): @@ -961,11 +966,12 @@ class Pillar(object): self.rend = salt.loader.render(self.opts, self.functions) matches = self.top_matches(top) pillar, errors = self.render_pillar(matches, errors=errors) - pillar = merge(self.opts['pillar'], - pillar, - self.merge_strategy, - self.opts.get('renderer', 'yaml'), - self.opts.get('pillar_merge_lists', False)) + pillar = merge( + self.opts['pillar'], + pillar, + self.merge_strategy, + self.opts.get('renderer', 'yaml'), + self.opts.get('pillar_merge_lists', False)) else: matches = self.top_matches(top) pillar, errors = self.render_pillar(matches) @@ -988,11 +994,12 @@ class Pillar(object): pillar['_errors'] = errors if self.pillar_override: - pillar = merge(pillar, - self.pillar_override, - self.merge_strategy, - self.opts.get('renderer', 'yaml'), - self.opts.get('pillar_merge_lists', False)) + pillar = merge( + pillar, + self.pillar_override, + self.merge_strategy, + self.opts.get('renderer', 'yaml'), + self.opts.get('pillar_merge_lists', False)) decrypt_errors = self.decrypt_pillar(pillar) if decrypt_errors: @@ -1008,11 +1015,11 @@ class Pillar(object): decrypt_pillar = self.opts['decrypt_pillar'] if not isinstance(decrypt_pillar, dict): decrypt_pillar = \ - salt.utils.repack_dictlist(self.opts['decrypt_pillar']) + salt.utils.data.repack_dictlist(self.opts['decrypt_pillar']) if not decrypt_pillar: errors.append('decrypt_pillar config option is malformed') for key, rend in six.iteritems(decrypt_pillar): - ptr = salt.utils.traverse_dict( + ptr = salt.utils.data.traverse_dict( pillar, key, default=None, @@ -1044,7 +1051,7 @@ class Pillar(object): # parent is the pillar dict itself. ptr = pillar else: - ptr = salt.utils.traverse_dict( + ptr = salt.utils.data.traverse_dict( pillar, parent, default=None, diff --git a/salt/pillar/makostack.py b/salt/pillar/makostack.py index 83494e4492..4724da2bed 100644 --- a/salt/pillar/makostack.py +++ b/salt/pillar/makostack.py @@ -407,13 +407,13 @@ def __virtual__(): def ext_pillar(minion_id, pillar, *args, **kwargs): - import salt.utils + import salt.utils.data stack = {} stack_config_files = list(args) traverse = { - 'pillar': partial(salt.utils.traverse_dict_and_list, pillar), - 'grains': partial(salt.utils.traverse_dict_and_list, __grains__), - 'opts': partial(salt.utils.traverse_dict_and_list, __opts__), + 'pillar': partial(salt.utils.data.traverse_dict_and_list, pillar), + 'grains': partial(salt.utils.data.traverse_dict_and_list, __grains__), + 'opts': partial(salt.utils.data.traverse_dict_and_list, __opts__), } for matcher, matchs in six.iteritems(kwargs): t, matcher = matcher.split(':', 1) diff --git a/salt/pillar/stack.py b/salt/pillar/stack.py index 6165f90d36..9c9367812d 100644 --- a/salt/pillar/stack.py +++ b/salt/pillar/stack.py @@ -387,7 +387,8 @@ from jinja2 import FileSystemLoader, Environment # Import Salt libs from salt.ext import six -import salt.utils +import salt.utils.data +import salt.utils.jinja log = logging.getLogger(__name__) @@ -398,9 +399,9 @@ def ext_pillar(minion_id, pillar, *args, **kwargs): stack = {} stack_config_files = list(args) traverse = { - 'pillar': partial(salt.utils.traverse_dict_and_list, pillar), - 'grains': partial(salt.utils.traverse_dict_and_list, __grains__), - 'opts': partial(salt.utils.traverse_dict_and_list, __opts__), + 'pillar': partial(salt.utils.data.traverse_dict_and_list, pillar), + 'grains': partial(salt.utils.data.traverse_dict_and_list, __grains__), + 'opts': partial(salt.utils.data.traverse_dict_and_list, __opts__), } for matcher, matchs in six.iteritems(kwargs): t, matcher = matcher.split(':', 1) @@ -438,7 +439,7 @@ def _process_stack_cfg(cfg, stack, minion_id, pillar): "__salt__": __salt__, "__grains__": __grains__, "__stack__": { - 'traverse': salt.utils.traverse_dict_and_list + 'traverse': salt.utils.data.traverse_dict_and_list }, "minion_id": minion_id, "pillar": pillar, diff --git a/salt/roster/cache.py b/salt/roster/cache.py index bdb1cc8171..3ae1c857c5 100644 --- a/salt/roster/cache.py +++ b/salt/roster/cache.py @@ -100,6 +100,7 @@ import logging import re # Salt libs +import salt.utils.data import salt.utils.minions import salt.utils.versions import salt.cache @@ -208,7 +209,7 @@ def _data_lookup(ref, lookup): res = [] for data_key in lookup: - data = salt.utils.traverse_dict_and_list(ref, data_key, None) + data = salt.utils.data.traverse_dict_and_list(ref, data_key, None) # log.debug('Fetched {0} in {1}: {2}'.format(data_key, ref, data)) if data: res.append(data) diff --git a/salt/runners/config.py b/salt/runners/config.py index 1a4085bbe6..99c784adbf 100644 --- a/salt/runners/config.py +++ b/salt/runners/config.py @@ -3,10 +3,9 @@ This runner is designed to mirror the execution module config.py, but for master settings ''' -from __future__ import absolute_import -from __future__ import print_function +from __future__ import absolute_import, print_function -import salt.utils +import salt.utils.data import salt.utils.sdb @@ -34,7 +33,7 @@ def get(key, default='', delimiter=':'): salt-run config.get file_roots:base salt-run config.get file_roots,base delimiter=',' ''' - ret = salt.utils.traverse_dict_and_list(__opts__, key, default='_|-', delimiter=delimiter) + ret = salt.utils.data.traverse_dict_and_list(__opts__, key, default='_|-', delimiter=delimiter) if ret == '_|-': return default else: diff --git a/salt/sdb/yaml.py b/salt/sdb/yaml.py index 028feb5ff2..d6b98fb283 100644 --- a/salt/sdb/yaml.py +++ b/salt/sdb/yaml.py @@ -41,7 +41,7 @@ import logging import salt.exceptions import salt.loader -import salt.utils +import salt.utils.data import salt.utils.files import salt.utils.dictupdate @@ -64,7 +64,7 @@ def get(key, profile=None): # pylint: disable=W0613 Get a value from the REST interface ''' data = _get_values(profile) - return salt.utils.traverse_dict_and_list(data, key, None) + return salt.utils.data.traverse_dict_and_list(data, key, None) def _get_values(profile=None): diff --git a/salt/serializers/python.py b/salt/serializers/python.py index ef1ff2aa33..9fab278cce 100644 --- a/salt/serializers/python.py +++ b/salt/serializers/python.py @@ -16,7 +16,7 @@ except ImportError: import json import pprint -import salt.utils +import salt.utils.data __all__ = ['serialize', 'available'] @@ -38,7 +38,7 @@ def serialize(obj, **options): return pprint.pformat( json.loads( json.dumps(obj), - object_hook=salt.utils.decode_dict + object_hook=salt.utils.data.decode_dict ), **options ) diff --git a/salt/states/boto_cloudtrail.py b/salt/states/boto_cloudtrail.py index 589c1d6a5b..249e10ad2f 100644 --- a/salt/states/boto_cloudtrail.py +++ b/salt/states/boto_cloudtrail.py @@ -60,7 +60,7 @@ import os.path # Import Salt Libs from salt.ext import six -import salt.utils +import salt.utils.data log = logging.getLogger(__name__) @@ -247,7 +247,7 @@ def present(name, Name, r = __salt__['boto_cloudtrail.list_tags'](Name=Name, region=region, key=key, keyid=keyid, profile=profile) _describe['Tags'] = r.get('tags', {}) - tagchange = salt.utils.compare_dicts(_describe['Tags'], Tags) + tagchange = salt.utils.data.compare_dicts(_describe['Tags'], Tags) if bool(tagchange): need_update = True ret['changes'].setdefault('new', {})['Tags'] = Tags diff --git a/salt/states/boto_ec2.py b/salt/states/boto_ec2.py index 1cacd624d9..589f3d6a90 100644 --- a/salt/states/boto_ec2.py +++ b/salt/states/boto_ec2.py @@ -59,7 +59,7 @@ from time import time, sleep # Import salt libs from salt.ext import six from salt.ext.six.moves import range # pylint: disable=import-error,no-name-in-module,redefined-builtin -import salt.utils +import salt.utils.data import salt.utils.dictupdate as dictupdate from salt.exceptions import SaltInvocationError, CommandExecutionError @@ -237,7 +237,7 @@ def eni_present( A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. ''' - if not salt.utils.exactly_one((subnet_id, subnet_name)): + if not salt.utils.data.exactly_one((subnet_id, subnet_name)): raise SaltInvocationError('One (but not both) of subnet_id or ' 'subnet_name must be provided.') if not groups: @@ -765,10 +765,10 @@ def instance_present(name, instance_name=None, instance_id=None, image_id=None, running_states = ('pending', 'rebooting', 'running', 'stopping', 'stopped') changed_attrs = {} - if not salt.utils.exactly_one((image_id, image_name)): + if not salt.utils.data.exactly_one((image_id, image_name)): raise SaltInvocationError('Exactly one of image_id OR ' 'image_name must be provided.') - if (public_ip or allocation_id or allocate_eip) and not salt.utils.exactly_one((public_ip, allocation_id, allocate_eip)): + if (public_ip or allocation_id or allocate_eip) and not salt.utils.data.exactly_one((public_ip, allocation_id, allocate_eip)): raise SaltInvocationError('At most one of public_ip, allocation_id OR ' 'allocate_eip may be provided.') @@ -1157,7 +1157,7 @@ def volume_absent(name, volume_name=None, volume_id=None, instance_name=None, filters = {} running_states = ('pending', 'rebooting', 'running', 'stopping', 'stopped') - if not salt.utils.exactly_one((volume_name, volume_id, instance_name, instance_id)): + if not salt.utils.data.exactly_one((volume_name, volume_id, instance_name, instance_id)): raise SaltInvocationError("Exactly one of 'volume_name', 'volume_id', " "'instance_name', or 'instance_id' must be provided.") if (instance_name or instance_id) and not device: @@ -1380,10 +1380,10 @@ def volume_present(name, volume_name=None, volume_id=None, instance_name=None, new_dict = {} running_states = ('running', 'stopped') - if not salt.utils.exactly_one((volume_name, volume_id)): + if not salt.utils.data.exactly_one((volume_name, volume_id)): raise SaltInvocationError("Exactly one of 'volume_name', 'volume_id', " " must be provided.") - if not salt.utils.exactly_one((instance_name, instance_id)): + if not salt.utils.data.exactly_one((instance_name, instance_id)): raise SaltInvocationError("Exactly one of 'instance_name', or 'instance_id'" " must be provided.") if device is None: @@ -1525,7 +1525,7 @@ def private_ips_present(name, network_interface_name=None, network_interface_id= dict with region, key and keyid. ''' - if not salt.utils.exactly_one((network_interface_name, network_interface_id)): + if not salt.utils.data.exactly_one((network_interface_name, network_interface_id)): raise SaltInvocationError("Exactly one of 'network_interface_name', " "'network_interface_id' must be provided") @@ -1646,7 +1646,7 @@ def private_ips_absent(name, network_interface_name=None, network_interface_id=N dict with region, key and keyid. ''' - if not salt.utils.exactly_one((network_interface_name, network_interface_id)): + if not salt.utils.data.exactly_one((network_interface_name, network_interface_id)): raise SaltInvocationError("Exactly one of 'network_interface_name', " "'network_interface_id' must be provided") diff --git a/salt/states/boto_iam.py b/salt/states/boto_iam.py index f0f6f4402b..9889d4a569 100644 --- a/salt/states/boto_iam.py +++ b/salt/states/boto_iam.py @@ -138,7 +138,7 @@ import json import os # Import Salt Libs -import salt.utils +import salt.utils.data import salt.utils.files import salt.utils.odict as odict import salt.utils.dictupdate as dictupdate @@ -1535,7 +1535,7 @@ def policy_present(name, policy_document, path=None, description=None, if isinstance(policy_document, six.string_types): policy_document = json.loads(policy_document) - r = salt.utils.compare_dicts(describeDict, policy_document) + r = salt.utils.data.compare_dicts(describeDict, policy_document) if bool(r): if __opts__['test']: diff --git a/salt/states/boto_iot.py b/salt/states/boto_iot.py index 6900c92471..b8d8f73255 100644 --- a/salt/states/boto_iot.py +++ b/salt/states/boto_iot.py @@ -81,7 +81,7 @@ import time import json # Import Salt Libs -import salt.utils +import salt.utils.data from salt.ext.six import string_types log = logging.getLogger(__name__) @@ -371,7 +371,7 @@ def policy_present(name, policyName, policyDocument, if isinstance(policyDocument, string_types): policyDocument = json.loads(policyDocument) - r = salt.utils.compare_dicts(describeDict, policyDocument) + r = salt.utils.data.compare_dicts(describeDict, policyDocument) if bool(r): if __opts__['test']: msg = 'Policy {0} set to be modified.'.format(policyName) diff --git a/salt/states/boto_lambda.py b/salt/states/boto_lambda.py index e190a74d59..48729a17e3 100644 --- a/salt/states/boto_lambda.py +++ b/salt/states/boto_lambda.py @@ -70,8 +70,8 @@ import json # Import Salt Libs from salt.ext import six +import salt.utils.data import salt.utils.dictupdate as dictupdate -import salt.utils import salt.utils.files from salt.exceptions import SaltInvocationError @@ -463,7 +463,7 @@ def _function_permissions_present(FunctionName, Permissions, if curr_permissions is None: curr_permissions = {} need_update = False - diffs = salt.utils.compare_dicts(curr_permissions, Permissions or {}) + diffs = salt.utils.data.compare_dicts(curr_permissions, Permissions or {}) if bool(diffs): ret['comment'] = os.linesep.join( [ret['comment'], 'Function permissions to be modified']) diff --git a/salt/states/boto_rds.py b/salt/states/boto_rds.py index c35eea5848..ca035cbfaa 100644 --- a/salt/states/boto_rds.py +++ b/salt/states/boto_rds.py @@ -77,7 +77,7 @@ import os # Import Salt Libs from salt.exceptions import SaltInvocationError -import salt.utils +import salt.utils.data log = logging.getLogger(__name__) @@ -464,7 +464,7 @@ def subnet_group_present(name, description, subnet_ids=None, subnet_names=None, A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. ''' - if not salt.utils.exactly_one((subnet_ids, subnet_names)): + if not salt.utils.data.exactly_one((subnet_ids, subnet_names)): raise SaltInvocationError('One (but not both) of subnet_ids or ' 'subnet_names must be provided.') diff --git a/salt/states/chocolatey.py b/salt/states/chocolatey.py index 141d5e7d59..2888e547a6 100644 --- a/salt/states/chocolatey.py +++ b/salt/states/chocolatey.py @@ -9,7 +9,8 @@ Manage Chocolatey package installs from __future__ import absolute_import # Import Salt libs -import salt.utils +import salt.utils.data +import salt.utils.versions from salt.exceptions import SaltInvocationError @@ -111,7 +112,7 @@ def installed(name, version=None, source=None, force=False, pre_versions=False, installed_version = version_info[full_name]['installed'][0] if version: - if salt.utils.compare_versions( + if salt.utils.versions.compare( ver1=installed_version, oper="==", ver2=version): if force: ret['changes'] = { @@ -181,7 +182,7 @@ def installed(name, version=None, source=None, force=False, pre_versions=False, # Get list of installed packages after 'chocolatey.install' post_install = __salt__['chocolatey.list'](local_only=True) - ret['changes'] = salt.utils.compare_dicts(pre_install, post_install) + ret['changes'] = salt.utils.data.compare_dicts(pre_install, post_install) return ret @@ -258,6 +259,6 @@ def uninstalled(name, version=None, uninstall_args=None, override_args=False): # Get list of installed packages after 'chocolatey.uninstall' post_uninstall = __salt__['chocolatey.list'](local_only=True) - ret['changes'] = salt.utils.compare_dicts(pre_uninstall, post_uninstall) + ret['changes'] = salt.utils.data.compare_dicts(pre_uninstall, post_uninstall) return ret diff --git a/salt/states/docker_network.py b/salt/states/docker_network.py index d5d57afc6b..8baf535133 100644 --- a/salt/states/docker_network.py +++ b/salt/states/docker_network.py @@ -35,7 +35,7 @@ import logging # Import salt libs from salt.ext import six -import salt.utils +import salt.utils.data # Enable proper logging log = logging.getLogger(__name__) # pylint: disable=invalid-name @@ -122,8 +122,8 @@ def present(name, 'result': False, 'comment': ''} - if salt.utils.is_dictlist(driver_opts): - driver_opts = salt.utils.repack_dictlist(driver_opts) + if salt.utils.data.is_dictlist(driver_opts): + driver_opts = salt.utils.data.repack_dictlist(driver_opts) # If any containers are specified, get details of each one, we need the Id and Name fields later if containers is not None: diff --git a/salt/states/docker_volume.py b/salt/states/docker_volume.py index 209b3839ac..baf9a664b1 100644 --- a/salt/states/docker_volume.py +++ b/salt/states/docker_volume.py @@ -34,7 +34,7 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils +import salt.utils.data # Enable proper logging log = logging.getLogger(__name__) # pylint: disable=invalid-name @@ -134,8 +134,8 @@ def present(name, driver=None, driver_opts=None, force=False): 'changes': {}, 'result': False, 'comment': ''} - if salt.utils.is_dictlist(driver_opts): - driver_opts = salt.utils.repack_dictlist(driver_opts) + if salt.utils.data.is_dictlist(driver_opts): + driver_opts = salt.utils.data.repack_dictlist(driver_opts) volume = _find_volume(name) if not volume: if __opts__['test']: diff --git a/salt/states/logadm.py b/salt/states/logadm.py index 0a1af8f1bd..6b1a2394c8 100644 --- a/salt/states/logadm.py +++ b/salt/states/logadm.py @@ -21,8 +21,8 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils import salt.utils.args +import salt.utils.data log = logging.getLogger(__name__) @@ -96,7 +96,7 @@ def rotate(name, **kwargs): new_config = __salt__['logadm.list_conf']() ret['comment'] = 'Log configuration {}'.format('updated' if kwargs['log_file'] in old_config else 'added') if kwargs['log_file'] in old_config: - for key, val in salt.utils.compare_dicts(old_config[kwargs['log_file']], new_config[kwargs['log_file']]).items(): + for key, val in salt.utils.data.compare_dicts(old_config[kwargs['log_file']], new_config[kwargs['log_file']]).items(): ret['changes'][key] = val['new'] else: ret['changes'] = new_config[kwargs['log_file']] diff --git a/salt/states/pdbedit.py b/salt/states/pdbedit.py index 9b4c7b0b98..69e57c6251 100644 --- a/salt/states/pdbedit.py +++ b/salt/states/pdbedit.py @@ -27,7 +27,7 @@ from __future__ import absolute_import import logging # Import Salt libs -import salt.utils +import salt.utils.data log = logging.getLogger(__name__) @@ -135,7 +135,7 @@ def managed(name, **kwargs): if res[name] in ['created']: ret['changes'] = res elif res[name] in ['updated']: - ret['changes'][name] = salt.utils.compare_dicts( + ret['changes'][name] = salt.utils.data.compare_dicts( saved, __salt__['pdbedit.list'](hashes=True)[name], ) diff --git a/salt/states/ports.py b/salt/states/ports.py index 1dbf2ed2c1..fae3d1eb59 100644 --- a/salt/states/ports.py +++ b/salt/states/ports.py @@ -22,7 +22,7 @@ import logging import sys # Import salt libs -import salt.utils +import salt.utils.data from salt.exceptions import SaltInvocationError, CommandExecutionError from salt.modules.freebsdports import _normalize, _options_file_exists @@ -46,7 +46,7 @@ def _repack_options(options): return dict( [ (str(x), _normalize(y)) - for x, y in six.iteritems(salt.utils.repack_dictlist(options)) + for x, y in six.iteritems(salt.utils.data.repack_dictlist(options)) ] ) diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index 8d1eaf6ffb..256662b84f 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -34,7 +34,7 @@ import time import salt.syspaths import salt.exceptions import salt.output -import salt.utils +import salt.utils.data import salt.utils.event import salt.utils.versions from salt.ext import six @@ -881,7 +881,7 @@ def parallel_runners(name, runners): if runner_config is None: runner_config = {} else: - runner_config = salt.utils.repack_dictlist(runner_config) + runner_config = salt.utils.data.repack_dictlist(runner_config) if 'name' not in runner_config: runner_config['name'] = runner_id runners[runner_id] = runner_config diff --git a/salt/states/smartos.py b/salt/states/smartos.py index 15d440b753..23ea7b267d 100644 --- a/salt/states/smartos.py +++ b/salt/states/smartos.py @@ -89,10 +89,9 @@ import logging import os # Import Salt libs -import salt.utils -import salt.utils.files import salt.utils.atomicfile -from salt.utils.odict import OrderedDict +import salt.utils.data +import salt.utils.files log = logging.getLogger(__name__) @@ -141,7 +140,7 @@ def _write_config(config): try: with salt.utils.atomicfile.atomic_open('/usbkey/config', 'w') as config_file: config_file.write("#\n# This file was generated by salt\n#\n") - for prop in OrderedDict(sorted(config.items())): + for prop in salt.utils.odict.OrderedDict(sorted(config.items())): if ' ' in str(config[prop]): if not config[prop].startswith('"') or not config[prop].endswith('"'): config[prop] = '"{0}"'.format(config[prop]) @@ -160,7 +159,7 @@ def _parse_vmconfig(config, instances): vmconfig = None if isinstance(config, (salt.utils.odict.OrderedDict)): - vmconfig = OrderedDict() + vmconfig = salt.utils.odict.OrderedDict() for prop in config: if prop not in instances: vmconfig[prop] = config[prop] @@ -190,8 +189,8 @@ def _get_instance_changes(current, state): state_keys = set(state.keys()) # compare configs - changed = salt.utils.compare_dicts(current, state) - for change in salt.utils.compare_dicts(current, state): + changed = salt.utils.data.compare_dicts(current, state) + for change in salt.utils.data.compare_dicts(current, state): if change in changed and changed[change]['old'] == "": del changed[change] if change in changed and changed[change]['new'] == "": diff --git a/salt/states/win_dism.py b/salt/states/win_dism.py index c7575f1b10..43e20488f0 100644 --- a/salt/states/win_dism.py +++ b/salt/states/win_dism.py @@ -20,7 +20,7 @@ import logging import os # Import Salt libs -import salt.utils +import salt.utils.data import salt.utils.platform log = logging.getLogger(__name__) @@ -91,7 +91,7 @@ def capability_installed(name, ret['result'] = False new = __salt__['dism.installed_capabilities']() - changes = salt.utils.compare_lists(old, new) + changes = salt.utils.data.compare_lists(old, new) if changes: ret['comment'] = 'Installed {0}'.format(name) @@ -147,7 +147,7 @@ def capability_removed(name, image=None, restart=False): ret['result'] = False new = __salt__['dism.installed_capabilities']() - changes = salt.utils.compare_lists(old, new) + changes = salt.utils.data.compare_lists(old, new) if changes: ret['comment'] = 'Removed {0}'.format(name) @@ -218,7 +218,7 @@ def feature_installed(name, ret['result'] = False new = __salt__['dism.installed_features']() - changes = salt.utils.compare_lists(old, new) + changes = salt.utils.data.compare_lists(old, new) if changes: ret['comment'] = 'Installed {0}'.format(name) @@ -278,7 +278,7 @@ def feature_removed(name, remove_payload=False, image=None, restart=False): ret['result'] = False new = __salt__['dism.installed_features']() - changes = salt.utils.compare_lists(old, new) + changes = salt.utils.data.compare_lists(old, new) if changes: ret['comment'] = 'Removed {0}'.format(name) @@ -355,7 +355,7 @@ def package_installed(name, ret['result'] = False new = __salt__['dism.installed_packages']() - changes = salt.utils.compare_lists(old, new) + changes = salt.utils.data.compare_lists(old, new) if changes: ret['comment'] = 'Installed {0}'.format(name) @@ -433,7 +433,7 @@ def package_removed(name, image=None, restart=False): ret['result'] = False new = __salt__['dism.installed_packages']() - changes = salt.utils.compare_lists(old, new) + changes = salt.utils.data.compare_lists(old, new) if changes: ret['comment'] = 'Removed {0}'.format(name) diff --git a/salt/states/win_network.py b/salt/states/win_network.py index c68a5d0a00..8329f997cf 100644 --- a/salt/states/win_network.py +++ b/salt/states/win_network.py @@ -65,7 +65,7 @@ from __future__ import absolute_import import logging # Import Salt libs -import salt.utils +import salt.utils.data import salt.utils.platform import salt.utils.validate.net from salt.ext.six.moves import range @@ -386,7 +386,7 @@ def managed(name, ) new = __salt__['ip.get_interface'](name) - ret['changes'] = salt.utils.compare_dicts(old, new) + ret['changes'] = salt.utils.data.compare_dicts(old, new) if _changes(new, dns_proto, dns_servers, ip_proto, ip_addrs, gateway): ret['result'] = False ret['comment'] = ('Failed to set desired configuration settings ' diff --git a/salt/states/win_servermanager.py b/salt/states/win_servermanager.py index b50a180f35..17b2607337 100644 --- a/salt/states/win_servermanager.py +++ b/salt/states/win_servermanager.py @@ -11,7 +11,7 @@ remove roles/features. from __future__ import absolute_import # Import salt modules -import salt.utils +import salt.utils.data import salt.utils.versions @@ -194,7 +194,7 @@ def installed(name, # Get the changes new = __salt__['win_servermanager.list_installed']() - ret['changes'] = salt.utils.compare_dicts(old, new) + ret['changes'] = salt.utils.data.compare_dicts(old, new) return ret @@ -325,6 +325,6 @@ def removed(name, features=None, remove_payload=False, restart=False): # Get the changes new = __salt__['win_servermanager.list_installed']() - ret['changes'] = salt.utils.compare_dicts(old, new) + ret['changes'] = salt.utils.data.compare_dicts(old, new) return ret diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 2284f5d19d..6410456675 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -50,7 +50,6 @@ try: except ImportError: HAS_CPROFILE = False -# Import 3rd-party libs try: import Crypto.Random HAS_CRYPTO = True @@ -933,164 +932,6 @@ def get_values_of_matching_keys(pattern_dict, user_name): return ret -def subdict_match(data, - expr, - delimiter=DEFAULT_TARGET_DELIM, - regex_match=False, - exact_match=False): - ''' - Check for a match in a dictionary using a delimiter character to denote - levels of subdicts, and also allowing the delimiter character to be - matched. Thus, 'foo:bar:baz' will match data['foo'] == 'bar:baz' and - data['foo']['bar'] == 'baz'. The former would take priority over the - latter. - ''' - def _match(target, pattern, regex_match=False, exact_match=False): - if regex_match: - try: - return re.match(pattern.lower(), str(target).lower()) - except Exception: - log.error('Invalid regex \'{0}\' in match'.format(pattern)) - return False - elif exact_match: - return str(target).lower() == pattern.lower() - else: - return fnmatch.fnmatch(str(target).lower(), pattern.lower()) - - def _dict_match(target, pattern, regex_match=False, exact_match=False): - wildcard = pattern.startswith('*:') - if wildcard: - pattern = pattern[2:] - - if pattern == '*': - # We are just checking that the key exists - return True - elif pattern in target: - # We might want to search for a key - return True - elif subdict_match(target, - pattern, - regex_match=regex_match, - exact_match=exact_match): - return True - if wildcard: - for key in target: - if _match(key, - pattern, - regex_match=regex_match, - exact_match=exact_match): - return True - if isinstance(target[key], dict): - if _dict_match(target[key], - pattern, - regex_match=regex_match, - exact_match=exact_match): - return True - elif isinstance(target[key], list): - for item in target[key]: - if _match(item, - pattern, - regex_match=regex_match, - exact_match=exact_match): - return True - return False - - for idx in range(1, expr.count(delimiter) + 1): - splits = expr.split(delimiter) - key = delimiter.join(splits[:idx]) - matchstr = delimiter.join(splits[idx:]) - log.debug('Attempting to match \'{0}\' in \'{1}\' using delimiter ' - '\'{2}\''.format(matchstr, key, delimiter)) - match = traverse_dict_and_list(data, key, {}, delimiter=delimiter) - if match == {}: - continue - if isinstance(match, dict): - if _dict_match(match, - matchstr, - regex_match=regex_match, - exact_match=exact_match): - return True - continue - if isinstance(match, list): - # We are matching a single component to a single list member - for member in match: - if isinstance(member, dict): - if _dict_match(member, - matchstr, - regex_match=regex_match, - exact_match=exact_match): - return True - if _match(member, - matchstr, - regex_match=regex_match, - exact_match=exact_match): - return True - continue - if _match(match, - matchstr, - regex_match=regex_match, - exact_match=exact_match): - return True - return False - - -def traverse_dict(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): - ''' - Traverse a dict using a colon-delimited (or otherwise delimited, using the - 'delimiter' param) target string. The target 'foo:bar:baz' will return - data['foo']['bar']['baz'] if this value exists, and will otherwise return - the dict in the default argument. - ''' - try: - for each in key.split(delimiter): - data = data[each] - except (KeyError, IndexError, TypeError): - # Encountered a non-indexable value in the middle of traversing - return default - return data - - -def traverse_dict_and_list(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): - ''' - Traverse a dict or list using a colon-delimited (or otherwise delimited, - using the 'delimiter' param) target string. The target 'foo:bar:0' will - return data['foo']['bar'][0] if this value exists, and will otherwise - return the dict in the default argument. - Function will automatically determine the target type. - The target 'foo:bar:0' will return data['foo']['bar'][0] if data like - {'foo':{'bar':['baz']}} , if data like {'foo':{'bar':{'0':'baz'}}} - then return data['foo']['bar']['0'] - ''' - for each in key.split(delimiter): - if isinstance(data, list): - try: - idx = int(each) - except ValueError: - embed_match = False - # Index was not numeric, lets look at any embedded dicts - for embedded in (x for x in data if isinstance(x, dict)): - try: - data = embedded[each] - embed_match = True - break - except KeyError: - pass - if not embed_match: - # No embedded dicts matched, return the default - return default - else: - try: - data = data[idx] - except IndexError: - return default - else: - try: - data = data[each] - except (KeyError, TypeError): - return default - return data - - def sanitize_win_path_string(winpath): ''' Remove illegal path characters for windows @@ -1228,24 +1069,6 @@ def is_true(value=None): return bool(value) -@jinja_filter('exactly_n_true') -def exactly_n(l, n=1): - ''' - Tests that exactly N items in an iterable are "truthy" (neither None, - False, nor 0). - ''' - i = iter(l) - return all(any(i) for j in range(n)) and not any(i) - - -@jinja_filter('exactly_one_true') -def exactly_one(l): - ''' - Check if only one item is not None, False, or 0 in an iterable. - ''' - return exactly_n(l) - - def option(value, default='', opts=None, pillar=None): ''' Pass in a generic option and receive the value that will be assigned @@ -1496,84 +1319,9 @@ def date_format(date=None, format="%Y-%m-%d"): return date_cast(date).strftime(format) -@jinja_filter('compare_dicts') -def compare_dicts(old=None, new=None): - ''' - Compare before and after results from various salt functions, returning a - dict describing the changes that were made. - ''' - ret = {} - for key in set((new or {})).union((old or {})): - if key not in old: - # New key - ret[key] = {'old': '', - 'new': new[key]} - elif key not in new: - # Key removed - ret[key] = {'new': '', - 'old': old[key]} - elif new[key] != old[key]: - # Key modified - ret[key] = {'old': old[key], - 'new': new[key]} - return ret - - -@jinja_filter('compare_lists') -def compare_lists(old=None, new=None): - ''' - Compare before and after results from various salt functions, returning a - dict describing the changes that were made - ''' - ret = dict() - for item in new: - if item not in old: - ret['new'] = item - for item in old: - if item not in new: - ret['old'] = item - return ret - - -@jinja_filter('json_decode_list') -def decode_list(data): - ''' - JSON decodes as unicode, Jinja needs bytes... - ''' - rv = [] - for item in data: - if isinstance(item, six.text_type) and six.PY2: - item = item.encode('utf-8') - elif isinstance(item, list): - item = decode_list(item) - elif isinstance(item, dict): - item = decode_dict(item) - rv.append(item) - return rv - - -@jinja_filter('json_decode_dict') -def decode_dict(data): - ''' - JSON decodes as unicode, Jinja needs bytes... - ''' - rv = {} - for key, value in six.iteritems(data): - if isinstance(key, six.text_type) and six.PY2: - key = key.encode('utf-8') - if isinstance(value, six.text_type) and six.PY2: - value = value.encode('utf-8') - elif isinstance(value, list): - value = decode_list(value) - elif isinstance(value, dict): - value = decode_dict(value) - rv[key] = value - return rv - - def find_json(raw): ''' - Pass in a raw string and load the json when is starts. This allows for a + Pass in a raw string and load the json when it starts. This allows for a string to start with garbage and end with json but be cleanly loaded ''' ret = {} @@ -1615,93 +1363,6 @@ def is_bin_file(path): return False -def is_dictlist(data): - ''' - Returns True if data is a list of one-element dicts (as found in many SLS - schemas), otherwise returns False - ''' - if isinstance(data, list): - for element in data: - if isinstance(element, dict): - if len(element) != 1: - return False - else: - return False - return True - return False - - -def repack_dictlist(data, - strict=False, - recurse=False, - key_cb=None, - val_cb=None): - ''' - Takes a list of one-element dicts (as found in many SLS schemas) and - repacks into a single dictionary. - ''' - if isinstance(data, six.string_types): - try: - import yaml - data = yaml.safe_load(data) - except yaml.parser.ParserError as err: - log.error(err) - return {} - - if key_cb is None: - key_cb = lambda x: x - if val_cb is None: - val_cb = lambda x, y: y - - valid_non_dict = (six.string_types, six.integer_types, float) - if isinstance(data, list): - for element in data: - if isinstance(element, valid_non_dict): - continue - elif isinstance(element, dict): - if len(element) != 1: - log.error( - 'Invalid input for repack_dictlist: key/value pairs ' - 'must contain only one element (data passed: %s).', - element - ) - return {} - else: - log.error( - 'Invalid input for repack_dictlist: element %s is ' - 'not a string/dict/numeric value', element - ) - return {} - else: - log.error( - 'Invalid input for repack_dictlist, data passed is not a list ' - '(%s)', data - ) - return {} - - ret = {} - for element in data: - if isinstance(element, valid_non_dict): - ret[key_cb(element)] = None - else: - key = next(iter(element)) - val = element[key] - if is_dictlist(val): - if recurse: - ret[key_cb(key)] = repack_dictlist(val, recurse=recurse) - elif strict: - log.error( - 'Invalid input for repack_dictlist: nested dictlist ' - 'found, but recurse is set to False' - ) - return {} - else: - ret[key_cb(key)] = val_cb(key, val) - else: - ret[key_cb(key)] = val_cb(key, val) - return ret - - def total_seconds(td): ''' Takes a timedelta and returns the total number of seconds @@ -1830,67 +1491,6 @@ def simple_types_filter(data): return data -@jinja_filter('substring_in_list') -def substr_in_list(string_to_search_for, list_to_search): - ''' - Return a boolean value that indicates whether or not a given - string is present in any of the strings which comprise a list - ''' - return any(string_to_search_for in s for s in list_to_search) - - -def filter_by(lookup_dict, - lookup, - traverse, - merge=None, - default='default', - base=None): - ''' - ''' - ret = None - # Default value would be an empty list if lookup not found - val = traverse_dict_and_list(traverse, lookup, []) - - # Iterate over the list of values to match against patterns in the - # lookup_dict keys - for each in val if isinstance(val, list) else [val]: - for key in lookup_dict: - test_key = key if isinstance(key, six.string_types) else str(key) - test_each = each if isinstance(each, six.string_types) else str(each) - if fnmatch.fnmatchcase(test_each, test_key): - ret = lookup_dict[key] - break - if ret is not None: - break - - if ret is None: - ret = lookup_dict.get(default, None) - - if base and base in lookup_dict: - base_values = lookup_dict[base] - if ret is None: - ret = base_values - - elif isinstance(base_values, collections.Mapping): - if not isinstance(ret, collections.Mapping): - raise SaltException( - 'filter_by default and look-up values must both be ' - 'dictionaries.') - ret = salt.utils.dictupdate.update(copy.deepcopy(base_values), ret) - - if merge: - if not isinstance(merge, collections.Mapping): - raise SaltException( - 'filter_by merge argument must be a dictionary.') - - if ret is None: - ret = merge - else: - salt.utils.dictupdate.update(ret, copy.deepcopy(merge)) - - return ret - - def fnmatch_multiple(candidates, pattern): ''' Convenience function which runs fnmatch.fnmatch() on each element of passed @@ -3160,3 +2760,271 @@ def get_gid(group=None): 'Salt Oxygen. This warning will be removed in Salt Neon.' ) return salt.utils.user.get_gid(group) + + +def traverse_dict(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): + ''' + Traverse a dict using a colon-delimited (or otherwise delimited, using the + 'delimiter' param) target string. The target 'foo:bar:baz' will return + data['foo']['bar']['baz'] if this value exists, and will otherwise return + the dict in the default argument. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.traverse_dict\' detected. This function ' + 'has been moved to \'salt.utils.data.traverse_dict\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.traverse_dict(data, key, default, delimiter) + + +def traverse_dict_and_list(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): + ''' + Traverse a dict or list using a colon-delimited (or otherwise delimited, + using the 'delimiter' param) target string. The target 'foo:bar:0' will + return data['foo']['bar'][0] if this value exists, and will otherwise + return the dict in the default argument. + Function will automatically determine the target type. + The target 'foo:bar:0' will return data['foo']['bar'][0] if data like + {'foo':{'bar':['baz']}} , if data like {'foo':{'bar':{'0':'baz'}}} + then return data['foo']['bar']['0'] + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.traverse_dict_and_list\' detected. This function ' + 'has been moved to \'salt.utils.data.traverse_dict_and_list\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.traverse_dict_and_list(data, key, default, delimiter) + + +def filter_by(lookup_dict, + lookup, + traverse, + merge=None, + default='default', + base=None): + ''' + Common code to filter data structures like grains and pillar + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.filter_by\' detected. This function ' + 'has been moved to \'salt.utils.data.filter_by\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.filter_by( + lookup_dict, lookup, traverse, merge, default, base) + + +def subdict_match(data, + expr, + delimiter=DEFAULT_TARGET_DELIM, + regex_match=False, + exact_match=False): + ''' + Check for a match in a dictionary using a delimiter character to denote + levels of subdicts, and also allowing the delimiter character to be + matched. Thus, 'foo:bar:baz' will match data['foo'] == 'bar:baz' and + data['foo']['bar'] == 'baz'. The former would take priority over the + latter. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.subdict_match\' detected. This function ' + 'has been moved to \'salt.utils.data.subdict_match\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.subdict_match( + data, expr, delimiter, regex_match, exact_match) + + +def substr_in_list(string_to_search_for, list_to_search): + ''' + Return a boolean value that indicates whether or not a given + string is present in any of the strings which comprise a list + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.substr_in_list\' detected. This function ' + 'has been moved to \'salt.utils.data.substr_in_list\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.substr_in_list(string_to_search_for, list_to_search) + + +def is_dictlist(data): + ''' + Returns True if data is a list of one-element dicts (as found in many SLS + schemas), otherwise returns False + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.is_dictlist\' detected. This function ' + 'has been moved to \'salt.utils.data.is_dictlist\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.is_dictlist(data) + + +def repack_dictlist(data, + strict=False, + recurse=False, + key_cb=None, + val_cb=None): + ''' + Takes a list of one-element dicts (as found in many SLS schemas) and + repacks into a single dictionary. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.is_dictlist\' detected. This function ' + 'has been moved to \'salt.utils.data.is_dictlist\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.repack_dictlist(data, strict, recurse, key_cb, val_cb) + + +def compare_dicts(old=None, new=None): + ''' + Compare before and after results from various salt functions, returning a + dict describing the changes that were made. + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.compare_dicts\' detected. This function ' + 'has been moved to \'salt.utils.data.compare_dicts\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.compare_dicts(old, new) + + +def compare_lists(old=None, new=None): + ''' + Compare before and after results from various salt functions, returning a + dict describing the changes that were made + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.compare_lists\' detected. This function ' + 'has been moved to \'salt.utils.data.compare_lists\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.compare_lists(old, new) + + +def decode_dict(data): + ''' + JSON decodes as unicode, Jinja needs bytes... + + .. deprecated:: Oxygen + ''' + + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.compare_dicts\' detected. This function ' + 'has been moved to \'salt.utils.data.compare_dicts\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.decode_dict(data) + + +def decode_list(data): + ''' + JSON decodes as unicode, Jinja needs bytes... + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.decode_list\' detected. This function ' + 'has been moved to \'salt.utils.data.decode_list\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.decode_list(data) + + +def exactly_n(l, n=1): + ''' + Tests that exactly N items in an iterable are "truthy" (neither None, + False, nor 0). + + .. deprecated:: Oxygen + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.exactly_n\' detected. This function ' + 'has been moved to \'salt.utils.data.exactly_n\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.exactly_n(l, n) + + +def exactly_one(l): + ''' + Check if only one item is not None, False, or 0 in an iterable. + ''' + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.exactly_one\' detected. This function ' + 'has been moved to \'salt.utils.data.exactly_one\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.exactly_one(l) diff --git a/salt/utils/boto.py b/salt/utils/boto.py index 86b09a8e7d..ba2c43a3b4 100644 --- a/salt/utils/boto.py +++ b/salt/utils/boto.py @@ -272,7 +272,7 @@ def assign_funcs(modname, service, module=None, pack=None): setattr(mod, '_get_conn', get_connection_func(service, module=module)) setattr(mod, '_cache_id', cache_id_func(service)) - # TODO: Remove this and import salt.utils.exactly_one into boto_* modules instead + # TODO: Remove this and import salt.utils.data.exactly_one into boto_* modules instead # Leaving this way for now so boto modules can be back ported setattr(mod, '_exactly_one', exactly_one) diff --git a/salt/utils/boto3.py b/salt/utils/boto3.py index 866ab7ed17..1f2e025c4c 100644 --- a/salt/utils/boto3.py +++ b/salt/utils/boto3.py @@ -308,7 +308,7 @@ def assign_funcs(modname, service, module=None, setattr(mod, get_conn_funcname, get_connection_func(service, module=module)) setattr(mod, cache_id_funcname, cache_id_func(service)) - # TODO: Remove this and import salt.utils.exactly_one into boto_* modules instead + # TODO: Remove this and import salt.utils.data.exactly_one into boto_* modules instead # Leaving this way for now so boto modules can be back ported if exactly_one_funcname is not None: setattr(mod, exactly_one_funcname, exactly_one) diff --git a/salt/utils/crypt.py b/salt/utils/crypt.py index 51003b939e..10d5e800ea 100644 --- a/salt/utils/crypt.py +++ b/salt/utils/crypt.py @@ -11,6 +11,12 @@ log = logging.getLogger(__name__) import salt.loader from salt.exceptions import SaltInvocationError +try: + import Crypto.Random + HAS_CRYPTO = True +except ImportError: + HAS_CRYPTO = False + def decrypt(data, rend, @@ -86,3 +92,17 @@ def decrypt(data, ) return rend_func(data, translate_newlines=translate_newlines) + + +def reinit_crypto(): + ''' + When a fork arises, pycrypto needs to reinit + From its doc:: + + Caveat: For the random number generator to work correctly, + you must call Random.atfork() in both the parent and + child processes after using os.fork() + + ''' + if HAS_CRYPTO: + Crypto.Random.atfork() diff --git a/salt/utils/data.py b/salt/utils/data.py new file mode 100644 index 0000000000..f20173badf --- /dev/null +++ b/salt/utils/data.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Import Python libs +import collections +import copy +import fnmatch +import logging +import re +import yaml + +# Import Salt libs +import salt.utils.dictupdate +from salt.defaults import DEFAULT_TARGET_DELIM +from salt.exceptions import SaltException +from salt.utils.decorators.jinja import jinja_filter + +# Import 3rd-party libs +from salt.ext import six +from salt.ext.six.moves import range # pylint: disable=redefined-builtin + +log = logging.getLogger(__name__) + + +@jinja_filter('compare_dicts') +def compare_dicts(old=None, new=None): + ''' + Compare before and after results from various salt functions, returning a + dict describing the changes that were made. + ''' + ret = {} + for key in set((new or {})).union((old or {})): + if key not in old: + # New key + ret[key] = {'old': '', + 'new': new[key]} + elif key not in new: + # Key removed + ret[key] = {'new': '', + 'old': old[key]} + elif new[key] != old[key]: + # Key modified + ret[key] = {'old': old[key], + 'new': new[key]} + return ret + + +@jinja_filter('compare_lists') +def compare_lists(old=None, new=None): + ''' + Compare before and after results from various salt functions, returning a + dict describing the changes that were made + ''' + ret = dict() + for item in new: + if item not in old: + ret['new'] = item + for item in old: + if item not in new: + ret['old'] = item + return ret + + +@jinja_filter('json_decode_dict') +def decode_dict(data): + ''' + JSON decodes as unicode, Jinja needs bytes... + ''' + rv = {} + for key, value in six.iteritems(data): + if isinstance(key, six.text_type) and six.PY2: + key = key.encode('utf-8') + if isinstance(value, six.text_type) and six.PY2: + value = value.encode('utf-8') + elif isinstance(value, list): + value = decode_list(value) + elif isinstance(value, dict): + value = decode_dict(value) + rv[key] = value + return rv + + +@jinja_filter('json_decode_list') +def decode_list(data): + ''' + JSON decodes as unicode, Jinja needs bytes... + ''' + rv = [] + for item in data: + if isinstance(item, six.text_type) and six.PY2: + item = item.encode('utf-8') + elif isinstance(item, list): + item = decode_list(item) + elif isinstance(item, dict): + item = decode_dict(item) + rv.append(item) + return rv + + +@jinja_filter('exactly_n_true') +def exactly_n(l, n=1): + ''' + Tests that exactly N items in an iterable are "truthy" (neither None, + False, nor 0). + ''' + i = iter(l) + return all(any(i) for j in range(n)) and not any(i) + + +@jinja_filter('exactly_one_true') +def exactly_one(l): + ''' + Check if only one item is not None, False, or 0 in an iterable. + ''' + return exactly_n(l) + + +def traverse_dict(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): + ''' + Traverse a dict using a colon-delimited (or otherwise delimited, using the + 'delimiter' param) target string. The target 'foo:bar:baz' will return + data['foo']['bar']['baz'] if this value exists, and will otherwise return + the dict in the default argument. + ''' + try: + for each in key.split(delimiter): + data = data[each] + except (KeyError, IndexError, TypeError): + # Encountered a non-indexable value in the middle of traversing + return default + return data + + +def filter_by(lookup_dict, + lookup, + traverse, + merge=None, + default='default', + base=None): + ''' + Common code to filter data structures like grains and pillar + ''' + ret = None + # Default value would be an empty list if lookup not found + val = traverse_dict_and_list(traverse, lookup, []) + + # Iterate over the list of values to match against patterns in the + # lookup_dict keys + for each in val if isinstance(val, list) else [val]: + for key in lookup_dict: + test_key = key if isinstance(key, six.string_types) else str(key) + test_each = each if isinstance(each, six.string_types) else str(each) + if fnmatch.fnmatchcase(test_each, test_key): + ret = lookup_dict[key] + break + if ret is not None: + break + + if ret is None: + ret = lookup_dict.get(default, None) + + if base and base in lookup_dict: + base_values = lookup_dict[base] + if ret is None: + ret = base_values + + elif isinstance(base_values, collections.Mapping): + if not isinstance(ret, collections.Mapping): + raise SaltException( + 'filter_by default and look-up values must both be ' + 'dictionaries.') + ret = salt.utils.dictupdate.update(copy.deepcopy(base_values), ret) + + if merge: + if not isinstance(merge, collections.Mapping): + raise SaltException( + 'filter_by merge argument must be a dictionary.') + + if ret is None: + ret = merge + else: + salt.utils.dictupdate.update(ret, copy.deepcopy(merge)) + + return ret + + +def traverse_dict_and_list(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): + ''' + Traverse a dict or list using a colon-delimited (or otherwise delimited, + using the 'delimiter' param) target string. The target 'foo:bar:0' will + return data['foo']['bar'][0] if this value exists, and will otherwise + return the dict in the default argument. + Function will automatically determine the target type. + The target 'foo:bar:0' will return data['foo']['bar'][0] if data like + {'foo':{'bar':['baz']}} , if data like {'foo':{'bar':{'0':'baz'}}} + then return data['foo']['bar']['0'] + ''' + for each in key.split(delimiter): + if isinstance(data, list): + try: + idx = int(each) + except ValueError: + embed_match = False + # Index was not numeric, lets look at any embedded dicts + for embedded in (x for x in data if isinstance(x, dict)): + try: + data = embedded[each] + embed_match = True + break + except KeyError: + pass + if not embed_match: + # No embedded dicts matched, return the default + return default + else: + try: + data = data[idx] + except IndexError: + return default + else: + try: + data = data[each] + except (KeyError, TypeError): + return default + return data + + +def subdict_match(data, + expr, + delimiter=DEFAULT_TARGET_DELIM, + regex_match=False, + exact_match=False): + ''' + Check for a match in a dictionary using a delimiter character to denote + levels of subdicts, and also allowing the delimiter character to be + matched. Thus, 'foo:bar:baz' will match data['foo'] == 'bar:baz' and + data['foo']['bar'] == 'baz'. The former would take priority over the + latter. + ''' + def _match(target, pattern, regex_match=False, exact_match=False): + if regex_match: + try: + return re.match(pattern.lower(), str(target).lower()) + except Exception: + log.error('Invalid regex \'{0}\' in match'.format(pattern)) + return False + elif exact_match: + return str(target).lower() == pattern.lower() + else: + return fnmatch.fnmatch(str(target).lower(), pattern.lower()) + + def _dict_match(target, pattern, regex_match=False, exact_match=False): + wildcard = pattern.startswith('*:') + if wildcard: + pattern = pattern[2:] + + if pattern == '*': + # We are just checking that the key exists + return True + elif pattern in target: + # We might want to search for a key + return True + elif subdict_match(target, + pattern, + regex_match=regex_match, + exact_match=exact_match): + return True + if wildcard: + for key in target: + if _match(key, + pattern, + regex_match=regex_match, + exact_match=exact_match): + return True + if isinstance(target[key], dict): + if _dict_match(target[key], + pattern, + regex_match=regex_match, + exact_match=exact_match): + return True + elif isinstance(target[key], list): + for item in target[key]: + if _match(item, + pattern, + regex_match=regex_match, + exact_match=exact_match): + return True + return False + + for idx in range(1, expr.count(delimiter) + 1): + splits = expr.split(delimiter) + key = delimiter.join(splits[:idx]) + matchstr = delimiter.join(splits[idx:]) + log.debug('Attempting to match \'{0}\' in \'{1}\' using delimiter ' + '\'{2}\''.format(matchstr, key, delimiter)) + match = traverse_dict_and_list(data, key, {}, delimiter=delimiter) + if match == {}: + continue + if isinstance(match, dict): + if _dict_match(match, + matchstr, + regex_match=regex_match, + exact_match=exact_match): + return True + continue + if isinstance(match, list): + # We are matching a single component to a single list member + for member in match: + if isinstance(member, dict): + if _dict_match(member, + matchstr, + regex_match=regex_match, + exact_match=exact_match): + return True + if _match(member, + matchstr, + regex_match=regex_match, + exact_match=exact_match): + return True + continue + if _match(match, + matchstr, + regex_match=regex_match, + exact_match=exact_match): + return True + return False + + +@jinja_filter('substring_in_list') +def substr_in_list(string_to_search_for, list_to_search): + ''' + Return a boolean value that indicates whether or not a given + string is present in any of the strings which comprise a list + ''' + return any(string_to_search_for in s for s in list_to_search) + + +def is_dictlist(data): + ''' + Returns True if data is a list of one-element dicts (as found in many SLS + schemas), otherwise returns False + ''' + if isinstance(data, list): + for element in data: + if isinstance(element, dict): + if len(element) != 1: + return False + else: + return False + return True + return False + + +def repack_dictlist(data, + strict=False, + recurse=False, + key_cb=None, + val_cb=None): + ''' + Takes a list of one-element dicts (as found in many SLS schemas) and + repacks into a single dictionary. + ''' + if isinstance(data, six.string_types): + try: + data = yaml.safe_load(data) + except yaml.parser.ParserError as err: + log.error(err) + return {} + + if key_cb is None: + key_cb = lambda x: x + if val_cb is None: + val_cb = lambda x, y: y + + valid_non_dict = (six.string_types, six.integer_types, float) + if isinstance(data, list): + for element in data: + if isinstance(element, valid_non_dict): + continue + elif isinstance(element, dict): + if len(element) != 1: + log.error( + 'Invalid input for repack_dictlist: key/value pairs ' + 'must contain only one element (data passed: %s).', + element + ) + return {} + else: + log.error( + 'Invalid input for repack_dictlist: element %s is ' + 'not a string/dict/numeric value', element + ) + return {} + else: + log.error( + 'Invalid input for repack_dictlist, data passed is not a list ' + '(%s)', data + ) + return {} + + ret = {} + for element in data: + if isinstance(element, valid_non_dict): + ret[key_cb(element)] = None + else: + key = next(iter(element)) + val = element[key] + if is_dictlist(val): + if recurse: + ret[key_cb(key)] = repack_dictlist(val, recurse=recurse) + elif strict: + log.error( + 'Invalid input for repack_dictlist: nested dictlist ' + 'found, but recurse is set to False' + ) + return {} + else: + ret[key_cb(key)] = val_cb(key, val) + else: + ret[key_cb(key)] = val_cb(key, val) + return ret diff --git a/salt/utils/docker/__init__.py b/salt/utils/docker/__init__.py index 530066f6ed..3c85fe60f9 100644 --- a/salt/utils/docker/__init__.py +++ b/salt/utils/docker/__init__.py @@ -12,8 +12,8 @@ import logging import os # Import Salt libs -import salt.utils import salt.utils.args +import salt.utils.data import salt.utils.docker.translate from salt.exceptions import CommandExecutionError, SaltInvocationError from salt.utils.args import get_function_argspec as _argspec @@ -204,8 +204,8 @@ def translate_input(**kwargs): if real_key in skip_translate: continue - if salt.utils.is_dictlist(kwargs[key]): - kwargs[key] = salt.utils.repack_dictlist(kwargs[key]) + if salt.utils.data.is_dictlist(kwargs[key]): + kwargs[key] = salt.utils.data.repack_dictlist(kwargs[key]) try: func = getattr(salt.utils.docker.translate, real_key) diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py index b38cce6857..e4310a1519 100644 --- a/salt/utils/gitfs.py +++ b/salt/utils/gitfs.py @@ -22,6 +22,7 @@ from datetime import datetime # Import salt libs import salt.utils # Can be removed once check_whitelist_blacklist, get_hash, is_bin_file, repack_dictlist are moved import salt.utils.configparser +import salt.utils.data import salt.utils.files import salt.utils.gzip_util import salt.utils.itertools @@ -183,7 +184,7 @@ class GitProvider(object): override_params, cache_root, role='gitfs'): self.opts = opts self.role = role - self.global_saltenv = salt.utils.repack_dictlist( + self.global_saltenv = salt.utils.data.repack_dictlist( self.opts.get('{0}_saltenv'.format(self.role), []), strict=True, recurse=True, @@ -220,7 +221,7 @@ class GitProvider(object): self.id = next(iter(remote)) self.get_url() - per_remote_conf = salt.utils.repack_dictlist( + per_remote_conf = salt.utils.data.repack_dictlist( remote[self.id], strict=True, recurse=True, diff --git a/salt/utils/minions.py b/salt/utils/minions.py index 4657c70210..ab30e31d1c 100644 --- a/salt/utils/minions.py +++ b/salt/utils/minions.py @@ -14,6 +14,7 @@ import logging # Import salt libs import salt.payload import salt.utils +import salt.utils.data import salt.utils.files import salt.utils.network import salt.utils.versions @@ -289,11 +290,11 @@ class CkMinions(object): minions.remove(id_) continue search_results = mdata.get(search_type) - if not salt.utils.subdict_match(search_results, - expr, - delimiter=delimiter, - regex_match=regex_match, - exact_match=exact_match): + if not salt.utils.data.subdict_match(search_results, + expr, + delimiter=delimiter, + regex_match=regex_match, + exact_match=exact_match): minions.remove(id_) minions = list(minions) return {'minions': minions, diff --git a/salt/utils/reactor.py b/salt/utils/reactor.py index de1581c510..6dfd6d0a39 100644 --- a/salt/utils/reactor.py +++ b/salt/utils/reactor.py @@ -12,6 +12,7 @@ import salt.runner import salt.state import salt.utils import salt.utils.cache +import salt.utils.data import salt.utils.event import salt.utils.files import salt.utils.process @@ -364,7 +365,7 @@ class ReactWrap(object): ) if low['state'] == 'caller' \ and isinstance(reactor_args, list) \ - and not salt.utils.is_dictlist(reactor_args): + and not salt.utils.data.is_dictlist(reactor_args): # Legacy 'caller' reactors were already using the 'args' # param, but only supported a list of positional arguments. # If low['args'] is a list but is *not* a dictlist, then @@ -377,7 +378,7 @@ class ReactWrap(object): kwargs['arg'] = () kwargs['kwarg'] = reactor_args if not isinstance(kwargs['kwarg'], dict): - kwargs['kwarg'] = salt.utils.repack_dictlist(kwargs['kwarg']) + kwargs['kwarg'] = salt.utils.data.repack_dictlist(kwargs['kwarg']) if not kwargs['kwarg']: log.error( 'Reactor \'%s\' failed to execute %s \'%s\': ' diff --git a/tests/unit/utils/test_reactor.py b/tests/unit/utils/test_reactor.py index b0a10d581f..564c2f2b6e 100644 --- a/tests/unit/utils/test_reactor.py +++ b/tests/unit/utils/test_reactor.py @@ -9,7 +9,7 @@ import textwrap import yaml import salt.loader -import salt.utils +import salt.utils.data import salt.utils.reactor as reactor from tests.support.unit import TestCase, skipIf @@ -393,7 +393,7 @@ class TestReactor(TestCase, AdaptedConfigurationTestCaseMixin): reactor_config = yaml.safe_load(REACTOR_CONFIG) cls.opts.update(reactor_config) cls.reactor = reactor.Reactor(cls.opts) - cls.reaction_map = salt.utils.repack_dictlist(reactor_config['reactor']) + cls.reaction_map = salt.utils.data.repack_dictlist(reactor_config['reactor']) renderers = salt.loader.render(cls.opts, {}) cls.render_pipe = [(renderers[x], '') for x in ('jinja', 'yaml')] diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index f13e9cdf73..08620470bc 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -16,6 +16,7 @@ from tests.support.mock import ( # Import Salt libs import salt.utils +import salt.utils.data import salt.utils.jid import salt.utils.yamlencoding import salt.utils.zeromq @@ -126,21 +127,21 @@ class UtilsTestCase(TestCase): test_three_level_dict = {'a': {'b': {'c': 'v'}}} self.assertTrue( - salt.utils.subdict_match( + salt.utils.data.subdict_match( test_two_level_dict, 'foo:bar:baz' ) ) # In test_two_level_comb_dict, 'foo:bar' corresponds to 'baz:woz', not # 'baz'. This match should return False. self.assertFalse( - salt.utils.subdict_match( + salt.utils.data.subdict_match( test_two_level_comb_dict, 'foo:bar:baz' ) ) # This tests matching with the delimiter in the value part (in other # words, that the path 'foo:bar' corresponds to the string 'baz:woz'). self.assertTrue( - salt.utils.subdict_match( + salt.utils.data.subdict_match( test_two_level_comb_dict, 'foo:bar:baz:woz' ) ) @@ -148,7 +149,7 @@ class UtilsTestCase(TestCase): # to 'baz:woz:wiz', or if there was more deep nesting. But it does not, # so this should return False. self.assertFalse( - salt.utils.subdict_match( + salt.utils.data.subdict_match( test_two_level_comb_dict, 'foo:bar:baz:woz:wiz' ) ) @@ -156,11 +157,11 @@ class UtilsTestCase(TestCase): # value part 'ghi' should be successfully matched as it is a member of # the list corresponding to key path 'abc'. It is somewhat a # duplication of a test within test_traverse_dict_and_list, but - # salt.utils.subdict_match() does more than just invoke + # salt.utils.data.subdict_match() does more than just invoke # salt.utils.traverse_list_and_dict() so this particular assertion is a # sanity check. self.assertTrue( - salt.utils.subdict_match( + salt.utils.data.subdict_match( test_two_level_dict_and_list, 'abc:ghi' ) ) @@ -168,25 +169,25 @@ class UtilsTestCase(TestCase): # list, embedded in a dict. This is a rather absurd case, but it # confirms that match recursion works properly. self.assertTrue( - salt.utils.subdict_match( + salt.utils.data.subdict_match( test_two_level_dict_and_list, 'abc:lorem:ipsum:dolor:sit' ) ) # Test four level dict match for reference self.assertTrue( - salt.utils.subdict_match( + salt.utils.data.subdict_match( test_three_level_dict, 'a:b:c:v' ) ) self.assertFalse( # Test regression in 2015.8 where 'a:c:v' would match 'a:b:c:v' - salt.utils.subdict_match( + salt.utils.data.subdict_match( test_three_level_dict, 'a:c:v' ) ) # Test wildcard match self.assertTrue( - salt.utils.subdict_match( + salt.utils.data.subdict_match( test_three_level_dict, 'a:*:c:v' ) ) @@ -196,13 +197,13 @@ class UtilsTestCase(TestCase): self.assertDictEqual( {'not_found': 'nope'}, - salt.utils.traverse_dict( + salt.utils.data.traverse_dict( test_two_level_dict, 'foo:bar:baz', {'not_found': 'nope'} ) ) self.assertEqual( 'baz', - salt.utils.traverse_dict( + salt.utils.data.traverse_dict( test_two_level_dict, 'foo:bar', {'not_found': 'not_found'} ) ) @@ -213,40 +214,40 @@ class UtilsTestCase(TestCase): 'foo': ['bar', 'baz', {'lorem': {'ipsum': [{'dolor': 'sit'}]}}] } - # Check traversing too far: salt.utils.traverse_dict_and_list() returns + # Check traversing too far: salt.utils.data.traverse_dict_and_list() returns # the value corresponding to a given key path, and baz is a value # corresponding to the key path foo:bar. self.assertDictEqual( {'not_found': 'nope'}, - salt.utils.traverse_dict_and_list( + salt.utils.data.traverse_dict_and_list( test_two_level_dict, 'foo:bar:baz', {'not_found': 'nope'} ) ) # Now check to ensure that foo:bar corresponds to baz self.assertEqual( 'baz', - salt.utils.traverse_dict_and_list( + salt.utils.data.traverse_dict_and_list( test_two_level_dict, 'foo:bar', {'not_found': 'not_found'} ) ) # Check traversing too far self.assertDictEqual( {'not_found': 'nope'}, - salt.utils.traverse_dict_and_list( + salt.utils.data.traverse_dict_and_list( test_two_level_dict_and_list, 'foo:bar', {'not_found': 'nope'} ) ) # Check index 1 (2nd element) of list corresponding to path 'foo' self.assertEqual( 'baz', - salt.utils.traverse_dict_and_list( + salt.utils.data.traverse_dict_and_list( test_two_level_dict_and_list, 'foo:1', {'not_found': 'not_found'} ) ) # Traverse a couple times into dicts embedded in lists self.assertEqual( 'sit', - salt.utils.traverse_dict_and_list( + salt.utils.data.traverse_dict_and_list( test_two_level_dict_and_list, 'foo:lorem:ipsum:dolor', {'not_found': 'not_found'} @@ -345,17 +346,17 @@ class UtilsTestCase(TestCase): self.assertRaises(TypeError, salt.utils.yamlencoding.yaml_encode, testobj) def test_compare_dicts(self): - ret = salt.utils.compare_dicts(old={'foo': 'bar'}, new={'foo': 'bar'}) + ret = salt.utils.data.compare_dicts(old={'foo': 'bar'}, new={'foo': 'bar'}) self.assertEqual(ret, {}) - ret = salt.utils.compare_dicts(old={'foo': 'bar'}, new={'foo': 'woz'}) + ret = salt.utils.data.compare_dicts(old={'foo': 'bar'}, new={'foo': 'woz'}) expected_ret = {'foo': {'new': 'woz', 'old': 'bar'}} self.assertDictEqual(ret, expected_ret) def test_decode_list(self): test_data = [u'unicode_str', [u'unicode_item_in_list', 'second_item_in_list'], {'dict_key': u'dict_val'}] expected_ret = ['unicode_str', ['unicode_item_in_list', 'second_item_in_list'], {'dict_key': 'dict_val'}] - ret = salt.utils.decode_list(test_data) + ret = salt.utils.data.decode_list(test_data) self.assertEqual(ret, expected_ret) def test_decode_dict(self): @@ -365,7 +366,7 @@ class UtilsTestCase(TestCase): expected_ret = {'test_unicode_key': 'test_unicode_val', 'test_list_key': ['list_1', 'unicode_list_two'], 'test_dict_key': {'test_sub_dict_key': 'test_sub_dict_val'}} - ret = salt.utils.decode_dict(test_data) + ret = salt.utils.data.decode_dict(test_data) self.assertDictEqual(ret, expected_ret) def test_find_json(self): @@ -419,16 +420,16 @@ class UtilsTestCase(TestCase): expected_ret = {'dict_key_1': 'dict_val_1', 'dict_key_2': 'dict_val_2', 'dict_key_3': 'dict_val_3'} - ret = salt.utils.repack_dictlist(list_of_one_element_dicts) + ret = salt.utils.data.repack_dictlist(list_of_one_element_dicts) self.assertDictEqual(ret, expected_ret) # Try with yaml yaml_key_val_pair = '- key1: val1' - ret = salt.utils.repack_dictlist(yaml_key_val_pair) + ret = salt.utils.data.repack_dictlist(yaml_key_val_pair) self.assertDictEqual(ret, {'key1': 'val1'}) # Make sure we handle non-yaml junk data - ret = salt.utils.repack_dictlist(LOREM_IPSUM) + ret = salt.utils.data.repack_dictlist(LOREM_IPSUM) self.assertDictEqual(ret, {}) @skipIf(NO_MOCK, NO_MOCK_REASON) From ab8c224e0765e2970e499f77a3ba2f1445ee022a Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 10 Oct 2017 09:41:04 -0500 Subject: [PATCH 561/633] Remove docstrings in salt.utils convenience functions Having the duplicate docstrings makes CodeClimate cry. --- salt/utils/__init__.py | 502 ----------------------------------------- 1 file changed, 502 deletions(-) diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 6410456675..1e33813e01 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -1518,12 +1518,6 @@ def fnmatch_multiple(candidates, pattern): # # These are deprecated and will be removed in Neon. def to_bytes(s, encoding=None): - ''' - Given bytes, bytearray, str, or unicode (python 2), return bytes (str for - python 2) - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1537,11 +1531,6 @@ def to_bytes(s, encoding=None): def to_str(s, encoding=None): - ''' - Given str, bytes, bytearray, or unicode (py2), return str - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1555,11 +1544,6 @@ def to_str(s, encoding=None): def to_unicode(s, encoding=None): - ''' - Given str or unicode, return unicode (str for python 3) - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1573,14 +1557,6 @@ def to_unicode(s, encoding=None): def str_to_num(text): - ''' - Convert a string to a number. - Returns an integer if the string represents an integer, a floating - point number if the string is a real number, or the string unchanged - otherwise. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1594,12 +1570,6 @@ def str_to_num(text): def is_quoted(value): - ''' - Return a single or double quote, if a string is wrapped in extra quotes. - Otherwise return an empty string. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1613,11 +1583,6 @@ def is_quoted(value): def dequote(value): - ''' - Remove extra quotes around a string. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1631,11 +1596,6 @@ def dequote(value): def is_hex(value): - ''' - Returns True if value is a hexidecimal string, otherwise returns False - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1649,11 +1609,6 @@ def is_hex(value): def is_bin_str(data): - ''' - Detects if the passed string of data is binary or text - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1667,9 +1622,6 @@ def is_bin_str(data): def rand_string(size=32): - ''' - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1683,11 +1635,6 @@ def rand_string(size=32): def contains_whitespace(text): - ''' - Returns True if there are any whitespace characters in the string - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.stringutils @@ -1701,15 +1648,6 @@ def contains_whitespace(text): def clean_kwargs(**kwargs): - ''' - Return a dict without any of the __pub* keys (or any other keys starting - with a dunder) from the kwargs dict passed into the execution module - functions. These keys are useful for tracking what was used to invoke - the function call, but they may not be desirable to have if passing the - kwargs forward wholesale. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.args @@ -1723,11 +1661,6 @@ def clean_kwargs(**kwargs): def invalid_kwargs(invalid_kwargs, raise_exc=True): - ''' - Raise a SaltInvocationError if invalid_kwargs is non-empty - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.args @@ -1741,11 +1674,6 @@ def invalid_kwargs(invalid_kwargs, raise_exc=True): def shlex_split(s, **kwargs): - ''' - Only split if variable is a string - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.args @@ -1759,12 +1687,6 @@ def shlex_split(s, **kwargs): def arg_lookup(fun, aspec=None): - ''' - Return a dict containing the arguments and default arguments to the - function. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.args @@ -1778,12 +1700,6 @@ def arg_lookup(fun, aspec=None): def argspec_report(functions, module=''): - ''' - Pass in a functions dict as it is returned from the loader and return the - argspec function signatures - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.args @@ -1797,11 +1713,6 @@ def argspec_report(functions, module=''): def which(exe=None): - ''' - Python clone of /usr/bin/which - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.path @@ -1815,11 +1726,6 @@ def which(exe=None): def which_bin(exes): - ''' - Scan over some possible executables and return the first one that is found - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.path @@ -1833,18 +1739,6 @@ def which_bin(exes): def path_join(*parts, **kwargs): - ''' - This functions tries to solve some issues when joining multiple absolute - paths on both *nix and windows platforms. - - See tests/unit/utils/test_path.py for some examples on what's being - talked about here. - - The "use_posixpath" kwarg can be be used to force joining using poxixpath, - which is useful for Salt fileserver paths on Windows masters. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.path @@ -1858,11 +1752,6 @@ def path_join(*parts, **kwargs): def rand_str(size=9999999999, hash_type=None): - ''' - Return a hash of a randomized data from random.SystemRandom() - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.hashutils @@ -1876,11 +1765,6 @@ def rand_str(size=9999999999, hash_type=None): def is_windows(): - ''' - Simple function to return if a host is Windows or not - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -1894,15 +1778,6 @@ def is_windows(): def is_proxy(): - ''' - Return True if this minion is a proxy minion. - Leverages the fact that is_linux() and is_windows - both return False for proxies. - TODO: Need to extend this for proxies that might run on - other Unices - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -1916,12 +1791,6 @@ def is_proxy(): def is_linux(): - ''' - Simple function to return if a host is Linux or not. - Note for a proxy minion, we need to return something else - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -1935,11 +1804,6 @@ def is_linux(): def is_darwin(): - ''' - Simple function to return if a host is Darwin (macOS) or not - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -1953,11 +1817,6 @@ def is_darwin(): def is_sunos(): - ''' - Simple function to return if host is SunOS or not - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -1971,11 +1830,6 @@ def is_sunos(): def is_smartos(): - ''' - Simple function to return if host is SmartOS (Illumos) or not - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -1989,11 +1843,6 @@ def is_smartos(): def is_smartos_globalzone(): - ''' - Function to return if host is SmartOS (Illumos) global zone or not - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -2007,11 +1856,6 @@ def is_smartos_globalzone(): def is_smartos_zone(): - ''' - Function to return if host is SmartOS (Illumos) and not the gz - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -2025,11 +1869,6 @@ def is_smartos_zone(): def is_freebsd(): - ''' - Simple function to return if host is FreeBSD or not - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -2043,11 +1882,6 @@ def is_freebsd(): def is_netbsd(): - ''' - Simple function to return if host is NetBSD or not - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -2061,11 +1895,6 @@ def is_netbsd(): def is_openbsd(): - ''' - Simple function to return if host is OpenBSD or not - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -2079,11 +1908,6 @@ def is_openbsd(): def is_aix(): - ''' - Simple function to return if host is AIX or not - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.platform @@ -2097,11 +1921,6 @@ def is_aix(): def safe_rm(tgt): - ''' - Safely remove a file - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.files @@ -2116,11 +1935,6 @@ def safe_rm(tgt): @jinja_filter('is_empty') def is_empty(filename): - ''' - Is a file empty? - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.files @@ -2134,19 +1948,6 @@ def is_empty(filename): def fopen(*args, **kwargs): - ''' - Wrapper around open() built-in to set CLOEXEC on the fd. - - This flag specifies that the file descriptor should be closed when an exec - function is invoked; - When a file descriptor is allocated (as with open or dup), this bit is - initially cleared on the new file descriptor, meaning that descriptor will - survive into the new program after exec. - - NB! We still have small race condition between open and fcntl. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.files @@ -2161,11 +1962,6 @@ def fopen(*args, **kwargs): @contextlib.contextmanager def flopen(*args, **kwargs): - ''' - Shortcut for fopen with lock and context manager - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.files @@ -2180,25 +1976,6 @@ def flopen(*args, **kwargs): @contextlib.contextmanager def fpopen(*args, **kwargs): - ''' - Shortcut for fopen with extra uid, gid and mode options. - - Supported optional Keyword Arguments: - - mode: explicit mode to set. Mode is anything os.chmod - would accept as input for mode. Works only on unix/unix - like systems. - - uid: the uid to set, if not set, or it is None or -1 no changes are - made. Same applies if the path is already owned by this - uid. Must be int. Works only on unix/unix like systems. - - gid: the gid to set, if not set, or it is None or -1 no changes are - made. Same applies if the path is already owned by this - gid. Must be int. Works only on unix/unix like systems. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.files @@ -2212,12 +1989,6 @@ def fpopen(*args, **kwargs): def rm_rf(path): - ''' - Platform-independent recursive delete. Includes code from - http://stackoverflow.com/a/2656405 - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.files @@ -2231,14 +2002,6 @@ def rm_rf(path): def mkstemp(*args, **kwargs): - ''' - Helper function which does exactly what `tempfile.mkstemp()` does but - accepts another argument, `close_fd`, which, by default, is true and closes - the fd before returning the file path. Something commonly done throughout - Salt's code. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.files @@ -2253,14 +2016,6 @@ def mkstemp(*args, **kwargs): @jinja_filter('is_text_file') def istextfile(fp_, blocksize=512): - ''' - Uses heuristics to guess whether the given file is text or binary, - by reading a single block of bytes from the file. - If more than 30% of the chars in the block are non-text, or there - are NUL ('\x00') bytes in the block, assume this is a binary file. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.files @@ -2274,19 +2029,6 @@ def istextfile(fp_, blocksize=512): def str_version_to_evr(verstring): - ''' - Split the package version string into epoch, version and release. - Return this as tuple. - - The epoch is always not empty. The version and the release can be an empty - string if such a component could not be found in the version string. - - "2:1.0-1.2" => ('2', '1.0', '1.2) - "1.0" => ('0', '1.0', '') - "" => ('0', '', '') - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.pkg.rpm @@ -2300,20 +2042,6 @@ def str_version_to_evr(verstring): def parse_docstring(docstring): - ''' - Parse a docstring into its parts. - - Currently only parses dependencies, can be extended to parse whatever is - needed. - - Parses into a dictionary: - { - 'full': full docstring, - 'deps': list of dependencies (empty list if none) - } - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.doc @@ -2327,12 +2055,6 @@ def parse_docstring(docstring): def compare_versions(ver1='', oper='==', ver2='', cmp_func=None, ignore_epoch=False): - ''' - Compares two version numbers. Accepts a custom function to perform the - cmp-style version comparison, otherwise uses version_cmp(). - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions salt.utils.versions.warn_until( @@ -2349,15 +2071,6 @@ def compare_versions(ver1='', oper='==', ver2='', cmp_func=None, ignore_epoch=Fa def version_cmp(pkg1, pkg2, ignore_epoch=False): - ''' - Compares two version strings using salt.utils.versions.LooseVersion. This - is a fallback for providers which don't have a version comparison utility - built into them. Return -1 if version1 < version2, 0 if version1 == - version2, and 1 if version1 > version2. Return None if there was a problem - making the comparison. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions salt.utils.versions.warn_until( @@ -2377,14 +2090,6 @@ def warn_until(version, stacklevel=None, _version_info_=None, _dont_call_warnings=False): - ''' - Helper function to raise a warning, by default, a ``DeprecationWarning``, - until the provided ``version``, after which, a ``RuntimeError`` will - be raised to remind the developers to remove the warning because the - target version has been reached. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions salt.utils.versions.warn_until( @@ -2407,20 +2112,6 @@ def kwargs_warn_until(kwargs, stacklevel=None, _version_info_=None, _dont_call_warnings=False): - ''' - Helper function to raise a warning (by default, a ``DeprecationWarning``) - when unhandled keyword arguments are passed to function, until the - provided ``version_info``, after which, a ``RuntimeError`` will be raised - to remind the developers to remove the ``**kwargs`` because the target - version has been reached. - This function is used to help deprecate unused legacy ``**kwargs`` that - were added to function parameters lists to preserve backwards compatibility - when removing a parameter. See - :ref:`the deprecation development docs ` - for the modern strategy for deprecating a function parameter. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions salt.utils.versions.warn_until( @@ -2439,11 +2130,6 @@ def kwargs_warn_until(kwargs, def get_color_theme(theme): - ''' - Return the color theme to use - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.color import salt.utils.versions @@ -2458,19 +2144,6 @@ def get_color_theme(theme): def get_colors(use=True, theme=None): - ''' - Return the colors as an easy to use dict. Pass `False` to deactivate all - colors by setting them to empty strings. Pass a string containing only the - name of a single color to be used in place of all colors. Examples: - - .. code-block:: python - - colors = get_colors() # enable all colors - no_colors = get_colors(False) # disable all colors - red_colors = get_colors('RED') # set all colors to red - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.color import salt.utils.versions @@ -2485,11 +2158,6 @@ def get_colors(use=True, theme=None): def gen_state_tag(low): - ''' - Generate the running dict tag string from the low data structure - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.state @@ -2503,11 +2171,6 @@ def gen_state_tag(low): def search_onfail_requisites(sid, highstate): - ''' - For a particular low chunk, search relevant onfail related states - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.state @@ -2521,20 +2184,6 @@ def search_onfail_requisites(sid, highstate): def check_onfail_requisites(state_id, state_result, running, highstate): - ''' - When a state fail and is part of a highstate, check - if there is onfail requisites. - When we find onfail requisites, we will consider the state failed - only if at least one of those onfail requisites also failed - - Returns: - - True: if onfail handlers suceeded - False: if one on those handler failed - None: if the state does not have onfail requisites - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.state @@ -2550,12 +2199,6 @@ def check_onfail_requisites(state_id, state_result, running, highstate): def check_state_result(running, recurse=False, highstate=None): - ''' - Check the total return value of the run and determine if the running - dict has any issues - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.state @@ -2571,11 +2214,6 @@ def check_state_result(running, recurse=False, highstate=None): def get_user(): - ''' - Returns the current user - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2589,13 +2227,6 @@ def get_user(): def get_uid(user=None): - ''' - Get the uid for a given user name. If no user given, the current euid will - be returned. If the user does not exist, None will be returned. On systems - which do not support pwd or os.geteuid, None will be returned. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2609,12 +2240,6 @@ def get_uid(user=None): def get_specific_user(): - ''' - Get a user name for publishing. If you find the user is "root" attempt to be - more specific by checking if Salt is being run as root via sudo. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2628,12 +2253,6 @@ def get_specific_user(): def chugid(runas): - ''' - Change the current process to belong to the specified user (and the groups - to which it belongs) - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2647,12 +2266,6 @@ def chugid(runas): def chugid_and_umask(runas, umask): - ''' - Helper method for for subprocess.Popen to initialise uid/gid and umask - for the new process. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2666,12 +2279,6 @@ def chugid_and_umask(runas, umask): def get_default_group(user): - ''' - Returns the specified user's default group. If the user doesn't exist, a - KeyError will be raised. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2685,12 +2292,6 @@ def get_default_group(user): def get_group_list(user, include_default=True): - ''' - Returns a list of all of the system group names of which the user - is a member. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2704,13 +2305,6 @@ def get_group_list(user, include_default=True): def get_group_dict(user=None, include_default=True): - ''' - Returns a dict of all of the system groups as keys, and group ids - as values, of which the user is a member. - E.g.: {'staff': 501, 'sudo': 27} - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2724,12 +2318,6 @@ def get_group_dict(user=None, include_default=True): def get_gid_list(user, include_default=True): - ''' - Returns a list of all of the system group IDs of which the user - is a member. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2743,13 +2331,6 @@ def get_gid_list(user, include_default=True): def get_gid(group=None): - ''' - Get the gid for a given group name. If no group given, the current egid - will be returned. If the group does not exist, None will be returned. On - systems which do not support grp or os.getegid it will return None. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.user @@ -2763,14 +2344,6 @@ def get_gid(group=None): def traverse_dict(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): - ''' - Traverse a dict using a colon-delimited (or otherwise delimited, using the - 'delimiter' param) target string. The target 'foo:bar:baz' will return - data['foo']['bar']['baz'] if this value exists, and will otherwise return - the dict in the default argument. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2784,18 +2357,6 @@ def traverse_dict(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): def traverse_dict_and_list(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): - ''' - Traverse a dict or list using a colon-delimited (or otherwise delimited, - using the 'delimiter' param) target string. The target 'foo:bar:0' will - return data['foo']['bar'][0] if this value exists, and will otherwise - return the dict in the default argument. - Function will automatically determine the target type. - The target 'foo:bar:0' will return data['foo']['bar'][0] if data like - {'foo':{'bar':['baz']}} , if data like {'foo':{'bar':{'0':'baz'}}} - then return data['foo']['bar']['0'] - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2814,11 +2375,6 @@ def filter_by(lookup_dict, merge=None, default='default', base=None): - ''' - Common code to filter data structures like grains and pillar - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2837,15 +2393,6 @@ def subdict_match(data, delimiter=DEFAULT_TARGET_DELIM, regex_match=False, exact_match=False): - ''' - Check for a match in a dictionary using a delimiter character to denote - levels of subdicts, and also allowing the delimiter character to be - matched. Thus, 'foo:bar:baz' will match data['foo'] == 'bar:baz' and - data['foo']['bar'] == 'baz'. The former would take priority over the - latter. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2860,12 +2407,6 @@ def subdict_match(data, def substr_in_list(string_to_search_for, list_to_search): - ''' - Return a boolean value that indicates whether or not a given - string is present in any of the strings which comprise a list - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2879,12 +2420,6 @@ def substr_in_list(string_to_search_for, list_to_search): def is_dictlist(data): - ''' - Returns True if data is a list of one-element dicts (as found in many SLS - schemas), otherwise returns False - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2902,12 +2437,6 @@ def repack_dictlist(data, recurse=False, key_cb=None, val_cb=None): - ''' - Takes a list of one-element dicts (as found in many SLS schemas) and - repacks into a single dictionary. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2921,12 +2450,6 @@ def repack_dictlist(data, def compare_dicts(old=None, new=None): - ''' - Compare before and after results from various salt functions, returning a - dict describing the changes that were made. - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2940,12 +2463,6 @@ def compare_dicts(old=None, new=None): def compare_lists(old=None, new=None): - ''' - Compare before and after results from various salt functions, returning a - dict describing the changes that were made - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2959,11 +2476,6 @@ def compare_lists(old=None, new=None): def decode_dict(data): - ''' - JSON decodes as unicode, Jinja needs bytes... - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions @@ -2978,11 +2490,6 @@ def decode_dict(data): def decode_list(data): - ''' - JSON decodes as unicode, Jinja needs bytes... - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -2996,12 +2503,6 @@ def decode_list(data): def exactly_n(l, n=1): - ''' - Tests that exactly N items in an iterable are "truthy" (neither None, - False, nor 0). - - .. deprecated:: Oxygen - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data @@ -3015,9 +2516,6 @@ def exactly_n(l, n=1): def exactly_one(l): - ''' - Check if only one item is not None, False, or 0 in an iterable. - ''' # Late import to avoid circular import. import salt.utils.versions import salt.utils.data From 02986140b87abde2c5f1c1b7abbb33bbadc06821 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Wed, 11 Oct 2017 04:41:20 -0600 Subject: [PATCH 562/633] utility support for vagrant-cloud --- salt/modules/vagrant.py | 24 +++++++++++++++++------- salt/utils/cloud.py | 5 +++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index f94fd01c82..357362d204 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -54,7 +54,6 @@ def __virtual__(): ''' run Vagrant commands if possible ''' - # noinspection PyUnresolvedReferences if salt.utils.path.which('vagrant') is None: return False, 'The vagrant module could not be loaded: vagrant command not found' return __virtualname__ @@ -297,6 +296,11 @@ def vm_state(name='', cwd=None): 'provider': _, # the Vagrant VM provider 'name': _} # salt_id name + Known bug: if there are multiple machines in your Vagrantfile, and you request + the status of the ``primary`` machine, which you defined by leaving the ``machine`` + parameter blank, then you may receive the status of all of them. + Please specify the actual machine name for each VM if there are more than one. + ''' if name: @@ -320,7 +324,7 @@ def vm_state(name='', cwd=None): datum = {'machine': tokens[0], 'state': ' '.join(tokens[1:-1]), 'provider': tokens[-1].lstrip('(').rstrip(')'), - 'name': name or get_machine_id(tokens[0], cwd) + 'name': get_machine_id(tokens[0], cwd) } info.append(datum) except IndexError: @@ -364,7 +368,7 @@ def init(name, # Salt_id for created VM # passed-in keyword arguments overwrite vm dictionary values vm_['cwd'] = cwd or vm_.get('cwd') if not vm_['cwd']: - raise SaltInvocationError('Path to Vagrantfile must be defined by \'cwd\' argument') + raise SaltInvocationError('Path to Vagrantfile must be defined by "cwd" argument') vm_['machine'] = machine or vm_.get('machine', machine) vm_['runas'] = runas or vm_.get('runas', runas) vm_['vagrant_provider'] = vagrant_provider or vm_.get('vagrant_provider', '') @@ -422,7 +426,7 @@ def shutdown(name): ''' Send a soft shutdown (vagrant halt) signal to the named vm. - This does the same thing as vagrant.stop. Other VM control + This does the same thing as vagrant.stop. Other-VM control modules use "stop" and "shutdown" to differentiate between hard and soft shutdowns. @@ -475,20 +479,26 @@ def pause(name): return ret == 0 -def reboot(name): +def reboot(name, **kwargs): ''' Reboot a VM. (vagrant reload) + keyword argument: + + - provision: (False) also re-run the Vagrant provisioning scripts. + CLI Example: .. code-block:: bash - salt vagrant.reboot + salt vagrant.reboot provision=True ''' vm_ = get_vm_info(name) machine = vm_['machine'] + prov = kwargs.get('provision', False) + provision = '--provision' if prov else '' - cmd = 'vagrant reload {}'.format(machine) + cmd = 'vagrant reload {} {}'.format(machine, provision) ret = __salt__['cmd.retcode'](cmd, runas=vm_.get('runas'), cwd=vm_.get('cwd')) diff --git a/salt/utils/cloud.py b/salt/utils/cloud.py index b013c48f30..93be5715ae 100644 --- a/salt/utils/cloud.py +++ b/salt/utils/cloud.py @@ -397,13 +397,14 @@ def bootstrap(vm_, opts=None): # NOTE: deploy_kwargs is also used to pass inline_script variable content # to run_inline_script function + host = salt.config.get_cloud_config_value('ssh_host', vm_, opts) deploy_kwargs = { 'opts': opts, - 'host': vm_['ssh_host'], + 'host': host, 'port': salt.config.get_cloud_config_value( 'ssh_port', vm_, opts, default=22 ), - 'salt_host': vm_.get('salt_host', vm_['ssh_host']), + 'salt_host': vm_.get('salt_host', host), 'username': ssh_username, 'script': deploy_script_code, 'inline_script': inline_script_config, From 5b1069809c475a990e063ab7fc40b987b5bb7ee3 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Wed, 11 Oct 2017 04:47:16 -0600 Subject: [PATCH 563/633] vagrant states --- salt/states/vagrant.py | 368 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 salt/states/vagrant.py diff --git a/salt/states/vagrant.py b/salt/states/vagrant.py new file mode 100644 index 0000000000..f3f6b097b0 --- /dev/null +++ b/salt/states/vagrant.py @@ -0,0 +1,368 @@ +# -*- coding: utf-8 -*- +r''' +.. index:: Vagrant state function + +Manage Vagrant VMs +================== + +Manange execution of Vagrant virtual machines on Salt minions. + +Vagrant_ is a tool for building and managing virtual machine environments. +It can use various providers, such as VirtualBox_, Docker_, or VMware_, to run its VMs. +Vagrant provides some of the functionality of a light-weight hypervisor. +The combination of Salt modules, Vagrant running on the host, and a +virtual machine provider, gives hypervisor-like functionality for +developers who use Vagrant to quickly define their virtual environments. + +.. _Vagrant: http://www.vagrantup.com/ +.. _VirtualBox: https://www.virtualbox.org/ +.. _Docker: https://www.docker.io/ +.. _VMWare: https://www.vmware.com/ + + .. versionadded:: Oxygen + +The configuration of each virtual machine is defined in a file named +``Vagrantfile`` which must exist on the VM host machine. +The essential parameters which must be defined to start a Vagrant VM +are the directory where the ``Vagrantfile`` is located \(argument ``cwd:``\), +and the username which will own the ``Vagrant box`` created for the VM \( +argument ``vagrant_runas:``\). + +A single ``Vagrantfile`` may define one or more virtual machines. +Use the ``machine`` argument to chose among them. The default (blank) +value will select the ``primary`` (or only) machine in the Vagrantfile. + +\[NOTE:\] Each virtual machine host must have the following: + +- a working salt-minion +- a Salt sdb database configured for ``vagrant_sdb_data``. +- Vagrant installed and the ``vagrant`` command working +- a suitable VM provider + +.. code-block:: yaml + + # EXAMPLE: + # file /etc/salt/minion.d/vagrant_sdb.conf on the host computer + # -- this sdb database is required by the Vagrant module -- + vagrant_sdb_data: # The sdb database must have this name. + driver: sqlite3 # Let's use SQLite to store the data ... + database: /var/cache/salt/vagrant.sqlite # ... in this file ... + table: sdb # ... using this table name. + create_table: True # if not present + +''' +from __future__ import absolute_import + +# Import Python libs +import fnmatch + +# Import Salt libs +import salt.utils.args +from salt.exceptions import CommandExecutionError, SaltInvocationError +import salt.ext.six as six + +__virtualname__ = 'vagrant' + + +def __virtual__(): + ''' + Only if vagrant module is available. + + :return: + ''' + + if 'vagrant.version' in __salt__: + return __virtualname__ + return False + + +def _vagrant_call(node, function, section, comment, status_when_done=None,**kwargs): + ''' + Helper to call the vagrant functions. Wildcards supported. + + :param node: The Salt-id or wildcard + :param function: the vagrant submodule to call + :param section: the name for the state call. + :param comment: what the state reply should say + :param status_when_done: the Vagrant status expected for this state + :return: the dictionary for the state reply + ''' + ret = {'name': node, 'changes': {}, 'result': True, 'comment': ''} + + targeted_nodes = [] + if isinstance(node, six.string_types): + try: # use shortcut if a single node name + if __salt__['vagrant.get_vm_info'](node): + targeted_nodes = [node] + except (SaltInvocationError): + pass + + if not targeted_nodes: # the shortcut failed, do this the hard way + all_domains = __salt__['vagrant.list_domains']() + targeted_nodes = fnmatch.filter(all_domains, node) + changed_nodes = [] + ignored_nodes = [] + for node in targeted_nodes: + if status_when_done: + try: + present_state = __salt__['vagrant.vm_state'](node)[0] + if present_state['state'] == status_when_done: + continue # no change is needed + except (IndexError, SaltInvocationError, CommandExecutionError): + pass + try: + response = __salt__['vagrant.{0}'.format(function)](node, **kwargs) + if isinstance(response, dict): + response = response['name'] + changed_nodes.append({'node': node, function: response}) + except (SaltInvocationError, CommandExecutionError) as err: + ignored_nodes.append({'node': node, 'issue': str(err)}) + if not changed_nodes: + ret['result'] = True + ret['comment'] = 'No changes seen' + if ignored_nodes: + ret['changes'] = {'ignored': ignored_nodes} + else: + ret['changes'] = {section: changed_nodes} + ret['comment'] = comment + + return ret + + +def running(name, **kwargs): + r''' + Defines and starts a new VM with specified arguments, or restart a + VM (or group of VMs). (Runs ``vagrant up``.) + + :param name: the Salt_id node name you wish your VM to have. + + If ``name`` contains a "?" or "*" then it will re-start a group of VMs + which have been paused or stopped. + + Each machine must be initially started individually using this function + or the vagrant.init execution module call. + + \[NOTE:\] Keyword arguments are silently ignored when re-starting an existing VM. + + Possible keyword arguments: + + - cwd: The directory (path) containing the Vagrantfile + - machine: ('') the name of the machine (in the Vagrantfile) if not default + - vagrant_runas: ('root') the username who owns the vagrantbox file + - vagrant_provider: the provider to run the VM (usually 'virtualbox') + - vm: ({}) a dictionary containing these or other keyword arguments + + .. code-block:: yaml + + node_name: + vagrant.running + + .. code-block:: yaml + + node_name: + vagrant.running: + - cwd: /projects/my_project + - vagrant_runas: my_username + - machine: machine1 + + ''' + if '*' in name or '?' in name: + + return _vagrant_call(name, 'start', 'restarted', + "Machine has been restarted", "running") + + else: + + ret = {'name': name, + 'changes': {}, + 'result': True, + 'comment': '{0} is already running'.format(name) + } + + try: + info = __salt__['vagrant.vm_state'](name) + if info[0]['state'] != 'running': + __salt__['vagrant.start'](name) + ret['changes'][name] = 'Machine started' + ret['comment'] = 'Node {0} started'.format(name) + except (SaltInvocationError, CommandExecutionError): + # there was no viable existing machine to start + ret, kwargs = _find_init_change(name, ret, **kwargs) + kwargs['start'] = True + __salt__['vagrant.init'](name, **kwargs) + ret['changes'][name] = 'Node defined and started' + ret['comment'] = 'Node {0} defined and started'.format(name) + + return ret + + +def _find_init_change(name, ret, **kwargs): + ''' + look for changes from any previous init of machine. + + :return: modified ret and kwargs + ''' + kwargs = salt.utils.args.clean_kwargs(**kwargs) + if 'vm' in kwargs: + kwargs.update(kwargs.pop('vm')) + # the state processing eats 'runas' so we rename + kwargs['runas'] = kwargs.pop('vagrant_runas', '') + try: + vm_ = __salt__['vagrant.get_vm_info'](name) + except SaltInvocationError: + vm_ = {} + for key, value in kwargs.items(): + ret['changes'][key] = {'old': None, 'new': value} + if vm_: # test for changed values + for key in vm_: + value = vm_[key] or '' # supply a blank if value is None + if key != 'name': # will be missing in kwargs + new = kwargs.get(key, '') + if new != value: + if key == 'machine' and new == '': + continue # we don't know the default machine name + ret['changes'][key] = {'old': value, 'new': new} + return ret, kwargs + + +def initialized(name, **kwargs): + r''' + Defines a new VM with specified arguments, but does not start it. + + :param name: the Salt_id node name you wish your VM to have. + + Each machine must be initialized individually using this function + or the "vagrant.running" function, or the vagrant.init execution module call. + + This command will not change the state of a running or paused machine. + + Possible keyword arguments: + + - cwd: The directory (path) containing the Vagrantfile + - machine: ('') the name of the machine (in the Vagrantfile) if not default + - vagrant_runas: ('root') the username who owns the vagrantbox file + - vagrant_provider: the provider to run the VM (usually 'virtualbox') + - vm: ({}) a dictionary containing these or other keyword arguments + + .. code-block:: yaml + + node_name1: + vagrant.initialized + - cwd: /projects/my_project + - vagrant_runas: my_username + - machine: machine1 + + node_name2: + vagrant.initialized + - cwd: /projects/my_project + - vagrant_runas: my_username + - machine: machine2 + + start_nodes: + vagrant.start: + - name: node_name? + ''' + ret = {'name': name, + 'changes': {}, + 'result': True, + 'comment': 'The VM is already correctly defined' + } + + # define a machine to start later + ret, kwargs = _find_init_change(name, ret, **kwargs) + + if ret['changes'] == {}: + return ret + + kwargs['start'] = False + __salt__['vagrant.init'](name, **kwargs) + ret['changes'][name] = 'Node initialized' + ret['comment'] = 'Node {0} defined but not started.'.format(name) + + return ret + +def stopped(name): + ''' + Stops a VM (or VMs) by shutting it (them) down nicely. (Runs ``vagrant halt``) + + :param name: May be a Salt_id node, or a POSIX-style wildcard string. + + .. code-block:: yaml + + node_name: + vagrant.stopped + ''' + + return _vagrant_call(name, 'shutdown', 'stopped', + 'Machine has been shut down', 'poweroff') + + +def powered_off(name): + ''' + Stops a VM (or VMs) by power off. (Runs ``vagrant halt``.) + + This method is provided for compatibility with other VM-control + state modules. For Vagrant, the action is identical with ``stopped``. + + :param name: May be a Salt_id node or a POSIX-style wildcard string. + + .. code-block:: yaml + + node_name: + vagrant.unpowered + ''' + + return _vagrant_call(name, 'stop', 'unpowered', + 'Machine has been powered off', 'poweroff') + + +def destroyed(name): + ''' + Stops a VM (or VMs) and removes all refences to it (them). (Runs ``vagrant destroy``.) + + Subsequent re-use of the same machine will requere another operation of ``vagrant.running`` + or a call to the ``vagrant.init`` execution module. + + :param name: May be a Salt_id node or a POSIX-style wildcard string. + + .. code-block:: yaml + + node_name: + vagrant.destroyed + ''' + + return _vagrant_call(name, 'destroy', 'destroyed', + 'Machine has been removed') + + +def paused(name): + ''' + Stores the state of a VM (or VMs) for fast restart. (Runs ``vagrant suspend``.) + + :param name: May be a Salt_id node or a POSIX-style wildcard string. + + .. code-block:: yaml + + node_name: + vagrant.paused + ''' + + return _vagrant_call(name, 'pause', 'paused', + 'Machine has been suspended', 'saved') + + +def rebooted(name): + ''' + Reboots a running, paused, or stopped VM (or VMs). (Runs ``vagrant reload``.) + + The will re-run the provisioning + + :param name: May be a Salt_id node or a POSIX-style wildcard string. + + .. code-block:: yaml + + node_name: + vagrant.reloaded + ''' + + return _vagrant_call(name, 'reboot', 'rebooted', 'Machine has been reloaded') From 41298e9c5741d39e71ec4b0e99ee426a8608119e Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Wed, 11 Oct 2017 04:48:09 -0600 Subject: [PATCH 564/633] vagrant cloud driver --- salt/cloud/__init__.py | 2 +- salt/cloud/clouds/vagrant.py | 339 +++++++++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 salt/cloud/clouds/vagrant.py diff --git a/salt/cloud/__init__.py b/salt/cloud/__init__.py index 0372f91f63..e5d05cab1c 100644 --- a/salt/cloud/__init__.py +++ b/salt/cloud/__init__.py @@ -1416,7 +1416,7 @@ class Cloud(object): if name in vms: prov = vms[name]['provider'] driv = vms[name]['driver'] - msg = six.u('{0} already exists under {1}:{2}').format( + msg = u'{0} already exists under {1}:{2}'.format( name, prov, driv ) log.error(msg) diff --git a/salt/cloud/clouds/vagrant.py b/salt/cloud/clouds/vagrant.py new file mode 100644 index 0000000000..462ec6ed01 --- /dev/null +++ b/salt/cloud/clouds/vagrant.py @@ -0,0 +1,339 @@ +# -*- coding: utf-8 -*- +''' +Vagrant Cloud Driver +==================== + +The Vagrant cloud is designed to "vagrant up" a virtual machine as a +Salt minion. + +Use of this module requires some configuration in cloud profile and provider +files as described in the +:ref:`Gettting Started with Vagrant ` documentation. + +.. versionadded:: Oxygen + + +''' + +# Import python libs +from __future__ import absolute_import +import logging +import os +import tempfile + +# Import salt libs +import salt.utils +import salt.config as config +import salt.client +import salt.ext.six as six +if six.PY3: + import ipaddress +else: + import salt.ext.ipaddress as ipaddress +from salt.exceptions import SaltCloudException, SaltCloudSystemExit + +# Get logging started +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Needs no special configuration + ''' + return True + + +def avail_locations(call=None): + r''' + This function returns a list of locations available. + + CLI Example: + + .. code-block:: bash + + salt-cloud --list-locations my-cloud-provider + + # \[ vagrant will always returns an empty dictionary \] + + ''' + + return {} + + +def avail_images(call=None): + '''This function returns a list of images available for this cloud provider. + vagrant will return a list of profiles. + salt-cloud --list-images my-cloud-provider + ''' + vm_ = get_configured_provider() + return {'Profiles': [profile for profile in vm_['profiles']]} + + +def avail_sizes(call=None): + r''' + This function returns a list of sizes available for this cloud provider. + + CLI Example: + + .. code-block:: bash + + salt-cloud --list-sizes my-cloud-provider + + # \[ vagrant always returns an empty dictionary \] + + ''' + return {} + + +def list_nodes(call=None): + ''' + List the nodes which have salt-cloud:driver:vagrant grains. + + CLI Example: + + .. code-block:: bash + + salt-cloud -Q + ''' + nodes = _list_nodes(call) + return _build_required_items(nodes) + + +def _build_required_items(nodes): + ret = {} + for name, grains in nodes.items(): + if grains: + private_ips = [] + public_ips = [] + ips = grains['ipv4'] + grains['ipv6'] + for adrs in ips: + ip_ = ipaddress.ip_address(adrs) + if not ip_.is_loopback: + if ip_.is_private: + private_ips.append(adrs) + else: + public_ips.append(adrs) + + ret[name] = { + 'id': grains['id'], + 'image': grains['salt-cloud']['profile'], + 'private_ips': private_ips, + 'public_ips': public_ips, + 'size': '', + 'state': 'running' + } + + return ret + + +def list_nodes_full(call=None): + ''' + List the nodes, ask all 'vagrant' minions, return dict of grains (enhanced). + + CLI Example: + + .. code-block:: bash + + salt-call -F + ''' + ret = _list_nodes(call) + + for key, grains in ret.items(): # clean up some hyperverbose grains -- everything is too much + try: + del grains['cpu_flags'], grains['disks'], grains['pythonpath'], grains['dns'], grains['gpus'] + except KeyError: + pass # ignore absence of things we are eliminating + except TypeError: + del ret[key] # eliminate all reference to unexpected (None) values. + + reqs = _build_required_items(ret) + for name in ret: + ret[name].update(reqs[name]) + return ret + + +def _list_nodes(call=None): + ''' + List the nodes, ask all 'vagrant' minions, return dict of grains. + ''' + local = salt.client.LocalClient() + ret = local.cmd('salt-cloud:driver:vagrant', 'grains.items', '', tgt_type='grain') + return ret + + +def list_nodes_select(call=None): + ''' + Return a list of the minions that have salt-cloud grains, with + select fields. + ''' + return salt.utils.cloud.list_nodes_select( + list_nodes_full('function'), __opts__['query.selection'], call, + ) + + +def show_instance(name, call=None): + ''' + List the a single node, return dict of grains. + ''' + local = salt.client.LocalClient() + ret = local.cmd(name, 'grains.items', '') + reqs = _build_required_items(ret) + ret[name].update(reqs[name]) + return ret + + +def _get_my_info(name): + local = salt.client.LocalClient() + return local.cmd(name, 'grains.get', ['salt-cloud']) + + +def create(vm_): + ''' + Provision a single machine + + CLI Example: + + .. code-block:: bash + salt-cloud -p my_profile new_node_1 + + ''' + name = vm_['name'] + machine = config.get_cloud_config_value( + 'machine', vm_, __opts__, default='') + vm_['machine'] = machine + host = config.get_cloud_config_value( + 'host', vm_, __opts__, default=NotImplemented) + vm_['cwd'] = config.get_cloud_config_value( + 'cwd', vm_, __opts__, default='/') + vm_['runas'] = config.get_cloud_config_value( + 'vagrant_runas', vm_, __opts__, default=os.getenv('SUDO_USER')) + vm_['timeout'] = config.get_cloud_config_value( + 'vagrant_up_timeout', vm_, __opts__, default=300) + vm_['vagrant_provider'] = config.get_cloud_config_value( + 'vagrant_provider', vm_, __opts__, default='') + vm_['grains'] = {'salt-cloud:vagrant': {'host': host, 'machine': machine}} + + log.info('sending \'vagrant.init %s machine=%s\' command to %s', name, machine, host) + + local = salt.client.LocalClient() + ret = local.cmd(host, 'vagrant.init', [name], kwarg={'vm': vm_, 'start': True}) + log.info('response ==> %s', ret[host]) + + network_mask = config.get_cloud_config_value( + 'network_mask', vm_, __opts__, default='') + if 'ssh_host' not in vm_: + local = salt.client.LocalClient() + ret = local.cmd(host, + 'vagrant.get_ssh_config', + [name], + kwarg={'network_mask': network_mask, + 'get_private_key': True})[host] + with tempfile.NamedTemporaryFile() as pks: + if 'private_key' not in vm_ and ret.get('private_key', False): + pks.write(ret['private_key']) + pks.flush() + log.debug('wrote private key to %s', pks.name) + vm_['key_filename'] = pks.name + if 'ssh_host' not in vm_: + vm_.setdefault('ssh_username', ret['ssh_username']) + if ret.get('ip_address'): + vm_['ssh_host'] = ret['ip_address'] + else: # if probe failed or not used, use Vagrant's reported ssh info + vm_['ssh_host'] = ret['ssh_host'] + vm_.setdefault('ssh_port', ret['ssh_port']) + + log.info('Provisioning machine %s as node %s using ssh %s', + machine, name, vm_['ssh_host']) + ret = __utils__['cloud.bootstrap'](vm_, __opts__) + return ret + + +def get_configured_provider(): + ''' + Return the first configured instance. + ''' + ret = config.is_provider_configured( + __opts__, + __active_provider_name__ or 'vagrant', + '' + ) + return ret + + +# noinspection PyTypeChecker +def destroy(name, call=None): + ''' + Destroy a node. + + CLI Example: + + .. code-block:: bash + + salt-cloud --destroy mymachine + ''' + if call == 'function': + raise SaltCloudSystemExit( + 'The destroy action must be called with -d, --destroy, ' + '-a, or --action.' + ) + + opts = __opts__ + + __utils__['cloud.fire_event']( + 'event', + 'destroying instance', + 'salt/cloud/{0}/destroying'.format(name), + args={'name': name}, + sock_dir=opts['sock_dir'], + transport=opts['transport'] + ) + my_info = _get_my_info(name) + profile_name = my_info[name]['profile'] + profile = opts['profiles'][profile_name] + host = profile['host'] + local = salt.client.LocalClient() + ret = local.cmd(host, 'vagrant.destroy', [name]) + + if ret[host]: + __utils__['cloud.fire_event']( + 'event', + 'destroyed instance', + 'salt/cloud/{0}/destroyed'.format(name), + args={'name': name}, + sock_dir=opts['sock_dir'], + transport=opts['transport'] + ) + + if opts.get('update_cachedir', False) is True: + __utils__['cloud.delete_minion_cachedir']( + name, __active_provider_name__.split(':')[0], opts) + + return {'Destroyed': '{0} was destroyed.'.format(name)} + else: + return {'Error': 'Error destroying {}'.format(name)} + + +# noinspection PyTypeChecker +def reboot(name, call=None): + ''' + Reboot a vagrant minion. + + name + The name of the VM to reboot. + + CLI Example: + + .. code-block:: bash + + salt-cloud -a reboot vm_name + ''' + if call != 'action': + raise SaltCloudException( + 'The reboot action must be called with -a or --action.' + ) + my_info = _get_my_info(name) + profile_name = my_info[name]['profile'] + profile = __opts__['profiles'][profile_name] + host = profile['host'] + local = salt.client.LocalClient() + return local.cmd(host, 'vagrant.reboot', [name]) From 4cdb04b409c0e24ab48d4e2bc41f3df296dcc233 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Wed, 11 Oct 2017 04:49:47 -0600 Subject: [PATCH 565/633] documentation for vagrant cloud & state --- doc/ref/clouds/all/index.rst | 1 + .../clouds/all/salt.cloud.clouds.vagrant.rst | 6 + doc/ref/states/all/index.rst | 1 + doc/ref/states/all/salt.states.vagrant.rst | 6 + doc/topics/cloud/config.rst | 11 + doc/topics/cloud/features.rst | 282 +++++++++--------- doc/topics/cloud/index.rst | 1 + doc/topics/cloud/saltify.rst | 2 + doc/topics/cloud/vagrant.rst | 268 +++++++++++++++++ 9 files changed, 440 insertions(+), 138 deletions(-) create mode 100644 doc/ref/clouds/all/salt.cloud.clouds.vagrant.rst create mode 100644 doc/ref/states/all/salt.states.vagrant.rst create mode 100644 doc/topics/cloud/vagrant.rst diff --git a/doc/ref/clouds/all/index.rst b/doc/ref/clouds/all/index.rst index 15fb4b1ae3..3ec40b0ed7 100644 --- a/doc/ref/clouds/all/index.rst +++ b/doc/ref/clouds/all/index.rst @@ -34,6 +34,7 @@ Full list of Salt Cloud modules scaleway softlayer softlayer_hw + vagrant virtualbox vmware vultrpy diff --git a/doc/ref/clouds/all/salt.cloud.clouds.vagrant.rst b/doc/ref/clouds/all/salt.cloud.clouds.vagrant.rst new file mode 100644 index 0000000000..ba3dcbe2d7 --- /dev/null +++ b/doc/ref/clouds/all/salt.cloud.clouds.vagrant.rst @@ -0,0 +1,6 @@ +========================= +salt.cloud.clouds.vagrant +========================= + +.. automodule:: salt.cloud.clouds.vagrant + :members: diff --git a/doc/ref/states/all/index.rst b/doc/ref/states/all/index.rst index 0b681ace7e..2fb3491b76 100644 --- a/doc/ref/states/all/index.rst +++ b/doc/ref/states/all/index.rst @@ -267,6 +267,7 @@ state modules tuned uptime user + vagrant vault vbox_guest victorops diff --git a/doc/ref/states/all/salt.states.vagrant.rst b/doc/ref/states/all/salt.states.vagrant.rst new file mode 100644 index 0000000000..5d5b6e9f9c --- /dev/null +++ b/doc/ref/states/all/salt.states.vagrant.rst @@ -0,0 +1,6 @@ +=================== +salt.states.vagrant +=================== + +.. automodule:: salt.states.vagrant + :members: \ No newline at end of file diff --git a/doc/topics/cloud/config.rst b/doc/topics/cloud/config.rst index 173ea4e692..e934a047d0 100644 --- a/doc/topics/cloud/config.rst +++ b/doc/topics/cloud/config.rst @@ -540,6 +540,17 @@ machines which are already installed, but not Salted. For more information about this driver and for configuration examples, please see the :ref:`Gettting Started with Saltify ` documentation. +.. _config_vagrant: + +Vagrant +------- + +The Vagrant driver is a new, experimental driver for controlling a VagrantBox +virtual machine, and installing Salt on it. The target host machine must be a +working salt minion, which is controlled via the salt master using salt-api. +For more information, see +:ref:`Getting Started With Vagrant `. + Extending Profiles and Cloud Providers Configuration ==================================================== diff --git a/doc/topics/cloud/features.rst b/doc/topics/cloud/features.rst index b067dc9a30..f270198dee 100644 --- a/doc/topics/cloud/features.rst +++ b/doc/topics/cloud/features.rst @@ -38,26 +38,30 @@ These are features that are available for almost every cloud host. .. container:: scrollable - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - | |AWS |CloudStack|Digital|EC2|GoGrid|JoyEnt|Linode|OpenStack|Parallels|Rackspace|Saltify|Softlayer|Softlayer|Aliyun| - | |(Legacy)| |Ocean | | | | | | |(Legacy) | | |Hardware | | - +=======================+========+==========+=======+===+======+======+======+=========+=========+=========+=======+=========+=========+======+ - |Query |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes | |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |Full Query |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes | |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |Selective Query |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes | |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |List Sizes |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes | |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |List Images |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes | |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |List Locations |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes | |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |create |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |destroy |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes | |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+-------+---------+---------+------+ + | |AWS |CloudStack|Digital|EC2|GoGrid|JoyEnt|Linode|OpenStack|Parallels|Rackspace|Saltify|Vagrant|Softlayer|Softlayer|Aliyun| + | |(Legacy)| |Ocean | | | | | | |(Legacy) | | | |Hardware | | + +=======================+========+==========+=======+===+======+======+======+=========+=========+=========+=======+=======+=========+=========+======+ + |Query |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |[1] |[1] |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+-------+---------+---------+------+ + |Full Query |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |[1] |[1] |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+-------+---------+---------+------+ + |Selective Query |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |[1] |[1] |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+-------+---------+---------+------+ + |List Sizes |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |[2] |[2] |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+-------+---------+---------+------+ + |List Images |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+-------+---------+---------+------+ + |List Locations |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |[2] |[2] |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+-------+---------+---------+------+ + |create |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |Yes |[1] |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+-------+---------+---------+------+ + |destroy |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |[1] |[1] |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+-------+---------+---------+------+ + +[1] Yes, if salt-api is enabled. + +[2] Always returns `{}`. Actions ======= @@ -70,46 +74,46 @@ instance name to be passed in. For example: .. container:: scrollable - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |Actions |AWS |CloudStack|Digital|EC2|GoGrid|JoyEnt|Linode|OpenStack|Parallels|Rackspace|Saltify|Softlayer|Softlayer|Aliyun| - | |(Legacy)| |Ocean | | | | | | |(Legacy) | | |Hardware | | - +=======================+========+==========+=======+===+======+======+======+=========+=========+=========+=======+=========+=========+======+ - |attach_volume | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |create_attach_volumes |Yes | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |del_tags |Yes | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |delvol_on_destroy | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |detach_volume | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |disable_term_protect |Yes | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |enable_term_protect |Yes | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_tags |Yes | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |keepvol_on_destroy | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |list_keypairs | | |Yes | | | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |rename |Yes | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |set_tags |Yes | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |show_delvol_on_destroy | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |show_instance | | |Yes |Yes| | |Yes | |Yes | | |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |show_term_protect | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |start |Yes | | |Yes| |Yes |Yes | |Yes | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |stop |Yes | | |Yes| |Yes |Yes | |Yes | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |take_action | | | | | |Yes | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |Actions |AWS |CloudStack|Digital|EC2|GoGrid|JoyEnt|Linode|OpenStack|Parallels|Rackspace|Saltify&|Softlayer|Softlayer|Aliyun| + | |(Legacy)| |Ocean | | | | | | |(Legacy) | Vagrant| |Hardware | | + +=======================+========+==========+=======+===+======+======+======+=========+=========+=========+========+=========+=========+======+ + |attach_volume | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |create_attach_volumes |Yes | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |del_tags |Yes | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |delvol_on_destroy | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |detach_volume | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |disable_term_protect |Yes | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |enable_term_protect |Yes | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_tags |Yes | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |keepvol_on_destroy | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |list_keypairs | | |Yes | | | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |rename |Yes | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |set_tags |Yes | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |show_delvol_on_destroy | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |show_instance | | |Yes |Yes| | |Yes | |Yes | | |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |show_term_protect | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |start |Yes | | |Yes| |Yes |Yes | |Yes | | | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |stop |Yes | | |Yes| |Yes |Yes | |Yes | | | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |take_action | | | | | |Yes | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ Functions ========= @@ -122,81 +126,83 @@ require the name of the provider to be passed in. For example: .. container:: scrollable - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |Functions |AWS |CloudStack|Digital|EC2|GoGrid|JoyEnt|Linode|OpenStack|Parallels|Rackspace|Saltify|Softlayer|Softlayer|Aliyun| - | |(Legacy)| |Ocean | | | | | | |(Legacy) | | |Hardware | | - +=======================+========+==========+=======+===+======+======+======+=========+=========+=========+=======+=========+=========+======+ - |block_device_mappings |Yes | | | | | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |create_keypair | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |create_volume | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |delete_key | | | | | |Yes | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |delete_keypair | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |delete_volume | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_image | | |Yes | | |Yes | | |Yes | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_ip | |Yes | | | | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_key | |Yes | | | | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_keyid | | |Yes | | | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_keypair | |Yes | | | | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_networkid | |Yes | | | | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_node | | | | | |Yes | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_password | |Yes | | | | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_size | | |Yes | | |Yes | | | | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_spot_config | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |get_subnetid | | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |iam_profile |Yes | | |Yes| | | | | | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |import_key | | | | | |Yes | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |key_list | | | | | |Yes | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |keyname |Yes | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |list_availability_zones| | | |Yes| | | | | | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |list_custom_images | | | | | | | | | | | |Yes | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |list_keys | | | | | |Yes | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |list_nodes |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |list_nodes_full |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |list_nodes_select |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |list_vlans | | | | | | | | | | | |Yes |Yes | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |rackconnect | | | | | | | |Yes | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |reboot | | | |Yes| |Yes | | | | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |reformat_node | | | | | |Yes | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |securitygroup |Yes | | |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |securitygroupid | | | |Yes| | | | | | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |show_image | | | |Yes| | | | |Yes | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |show_key | | | | | |Yes | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |show_keypair | | |Yes |Yes| | | | | | | | | | | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ - |show_volume | | | |Yes| | | | | | | | | |Yes | - +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+-------+---------+---------+------+ + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |Functions |AWS |CloudStack|Digital|EC2|GoGrid|JoyEnt|Linode|OpenStack|Parallels|Rackspace|Saltify&|Softlayer|Softlayer|Aliyun| + | |(Legacy)| |Ocean | | | | | | |(Legacy) | Vagrant| |Hardware | | + +=======================+========+==========+=======+===+======+======+======+=========+=========+=========+========+=========+=========+======+ + |block_device_mappings |Yes | | | | | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |create_keypair | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |create_volume | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |delete_key | | | | | |Yes | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |delete_keypair | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |delete_volume | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_image | | |Yes | | |Yes | | |Yes | | | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_ip | |Yes | | | | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_key | |Yes | | | | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_keyid | | |Yes | | | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_keypair | |Yes | | | | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_networkid | |Yes | | | | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_node | | | | | |Yes | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_password | |Yes | | | | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_size | | |Yes | | |Yes | | | | | | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_spot_config | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |get_subnetid | | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |iam_profile |Yes | | |Yes| | | | | | | | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |import_key | | | | | |Yes | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |key_list | | | | | |Yes | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |keyname |Yes | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |list_availability_zones| | | |Yes| | | | | | | | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |list_custom_images | | | | | | | | | | | |Yes | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |list_keys | | | | | |Yes | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |list_nodes |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |list_nodes_full |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |list_nodes_select |Yes |Yes |Yes |Yes|Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |list_vlans | | | | | | | | | | | |Yes |Yes | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |rackconnect | | | | | | | |Yes | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |reboot | | | |Yes| |Yes | | | | |[1] | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |reformat_node | | | | | |Yes | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |securitygroup |Yes | | |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |securitygroupid | | | |Yes| | | | | | | | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |show_image | | | |Yes| | | | |Yes | | | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |show_key | | | | | |Yes | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |show_keypair | | |Yes |Yes| | | | | | | | | | | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + |show_volume | | | |Yes| | | | | | | | | |Yes | + +-----------------------+--------+----------+-------+---+------+------+------+---------+---------+---------+--------+---------+---------+------+ + +[1] Yes, if salt-api is enabled. diff --git a/doc/topics/cloud/index.rst b/doc/topics/cloud/index.rst index eff8e2aa8f..d95ca09269 100644 --- a/doc/topics/cloud/index.rst +++ b/doc/topics/cloud/index.rst @@ -129,6 +129,7 @@ Cloud Provider Specifics Getting Started With Scaleway Getting Started With Saltify Getting Started With SoftLayer + Getting Started With Vagrant Getting Started With Vexxhost Getting Started With Virtualbox Getting Started With VMware diff --git a/doc/topics/cloud/saltify.rst b/doc/topics/cloud/saltify.rst index dda9801522..8b59da5b0e 100644 --- a/doc/topics/cloud/saltify.rst +++ b/doc/topics/cloud/saltify.rst @@ -183,6 +183,8 @@ simple installation. ssh_username: vagrant # a user name which has passwordless sudo password: vagrant # on your target machine provider: my_saltify_provider + shutdown_on_destroy: true # halt the target on "salt-cloud -d" command + .. code-block:: yaml diff --git a/doc/topics/cloud/vagrant.rst b/doc/topics/cloud/vagrant.rst new file mode 100644 index 0000000000..8a67dfc818 --- /dev/null +++ b/doc/topics/cloud/vagrant.rst @@ -0,0 +1,268 @@ +.. _getting-started-with-vagrant: + +============================ +Getting Started With Vagrant +============================ + +The Vagrant driver is a new, experimental driver for spinning up a VagrantBox +virtual machine, and installing Salt on it. + +Dependencies +============ +The Vagrant driver itself has no external dependencies. + +The machine which will host the VagrantBox must be an already existing minion +of the cloud server's Salt master. +It must have Vagrant_ installed, and a Vagrant-compatible virtual machine engine, +such as VirtualBox_. +(Note: The Vagrant driver does not depend on the salt-cloud VirtualBox driver in any way.) + +.. _Vagrant: https://www.vagrantup.com/ +.. _VirtualBox: https://www.virtualbox.org/ + +\[Caution: The version of Vagrant packaged for ``apt install`` in Ubuntu 16.04 will not connect a bridged +network adapter correctly. Use a version downloaded directly from the web site.\] + +Include the Vagrant guest editions plugin: +``vagrant plugin install vagrant-vbguest``. + +Configuration +============= + +Configuration of the client virtual machine (using VirtualBox, VMware, etc) +will be done by Vagrant as specified in the Vagrantfile on the host machine. + +Salt-cloud will push the commands to install and provision a salt minion on +the virtual machine, so you need not (perhaps **should** not) provision salt +in your Vagrantfile, in most cases. + +If, however, your cloud master cannot open an ssh connection to the child VM, +you may **need** to let Vagrant provision the VM with Salt, and use some other +method (such as passing a pillar dictionary to the VM) to pass the master's +IP address to the VM. The VM can then attempt to reach the salt master in the +usual way for non-cloud minions. Specify the profile configuration argument +as ``deploy: False`` to prevent the cloud master from trying. + +.. code-block:: yaml + + # Note: This example is for /etc/salt/cloud.providers file or any file in + # the /etc/salt/cloud.providers.d/ directory. + + my-vagrant-config: + minion: + master: 111.222.333.444 + provider: vagrant + + +Because the Vagrant driver needs a place to store the mapping between the +node name you use for Salt commands and the Vagrantfile which controls the VM, +you must configure your salt minion as a Salt smb server. +(See `host provisioning example`_ below.) + +Profiles +======== + +Vagrant requires a profile to be configured for each machine that needs Salt +installed. The initial profile can be set up at ``/etc/salt/cloud.profiles`` +or in the ``/etc/salt/cloud.profiles.d/`` directory. + +Each profile requires a ``vagrantfile`` parameter. If the Vagrantfile has +definitions for `multiple machines`_ then you need a ``machine`` parameter, + +.. _`multiple machines`: https://www.vagrantup.com/docs/multi-machine/ + +Salt-cloud uses ssh to provision the minion. There must be a routable path +from the cloud master to the VM. Usually, you will want to use +a bridged network adapter for ssh. The address may not be known until +DHCP assigns it. If ``ssh_host`` is not defined, and ``target_network`` +is defined, the driver will attempt to read the address from the output +of an ``ifconfig`` command. Lacking either setting, +the driver will try to use the value Vagrant returns as its ``ssh_host``, +which will work only if the cloud master is running somewhere on the same host. + +The ``target_network`` setting should be used +to identify the IP network your bridged adapter is expected to appear on. +Use CIDR notation, like ``target_network: '2001:DB8::/32'`` +or ``target_network: '192.0.2.0/24'``. + +Profile configuration example: + +.. code-block:: yaml + + # /etc/salt/cloud.profiles.d/vagrant.conf + + vagrant-machine: + host: my-vhost # the Salt id of the virtual machine's host computer. + provider: my-vagrant-config + cwd: /srv/machines # the path to your Virtualbox file. + vagrant_runas: my-username # the username who defined the Vagrantbox on the host + # vagrant_up_timeout: 300 # (seconds) timeout for cmd.run of the "vagrant up" command + # vagrant_provider: '' # option for "vagrant up" like: "--provider vmware_fusion" + # ssh_host: None # "None" means try to find the routable ip address from "ifconfig" + # target_network: None # Expected CIDR address of your bridged network + # force_minion_config: false # Set "true" to re-purpose an existing VM + +The machine can now be created and configured with the following command: + +.. code-block:: bash + + salt-cloud -p vagrant-machine my-id + +This will create the machine specified by the cloud profile +``vagrant-machine``, and will give the machine the minion id of +``my-id``. If the cloud master is also the salt-master, its Salt +key will automatically be accepted on the master. + +Once a salt-minion has been successfully installed on the instance, connectivity +to it can be verified with Salt: + +.. code-block:: bash + + salt my-id test.ping + +.. _host provisioning example: + +Provisioning a Vagrant cloud host (example) +=========================================== + +In order to query or control minions it created, each host +minion needs to track the Salt node names associated with +any guest virtual machines on it. +It does that using a Salt sdb database. + +The Salt sdb is not configured by default. The following example shows a +simple installation. + +This example assumes: + +- you are on a large network using the 10.x.x.x IP address space +- your Salt master's Salt id is "bevymaster" +- it will also be your salt-cloud controller +- it is at hardware address 10.124.30.7 +- it is running a recent Debian family Linux (raspbian) +- your workstation is a Salt minion of bevymaster +- your workstation's minion id is "my_laptop" +- VirtualBox has been installed on "my_laptop" (apt install is okay) +- Vagrant was installed from vagrantup.com. (not the 16.04 Ubuntu apt) +- "my_laptop" has done "vagrant plugin install vagrant-vbguest" +- the VM you want to start is on "my_laptop" at "/home/my_username/Vagrantfile" + +.. code-block:: yaml + + # file /etc/salt/minion.d/vagrant_sdb.conf on host computer "my_laptop" + # -- this sdb database is required by the Vagrant module -- + vagrant_sdb_data: # The sdb database must have this name. + driver: sqlite3 # Let's use SQLite to store the data ... + database: /var/cache/salt/vagrant.sqlite # ... in this file ... + table: sdb # ... using this table name. + create_table: True # if not present + +Remember to re-start your minion after changing its configuration files... + + ``sudo systemctl restart salt-minion`` + +.. code-block:: ruby + + # -*- mode: ruby -*- + # file /home/my_username/Vagrantfile on host computer "my_laptop" + BEVY = "bevy1" + DOMAIN = BEVY + ".test" # .test is an ICANN reserved non-public TLD + + # must supply a list of names to avoid Vagrant asking for interactive input + def get_good_ifc() # try to find a working Ubuntu network adapter name + addr_infos = Socket.getifaddrs + addr_infos.each do |info| + a = info.addr + if a and a.ip? and not a.ip_address.start_with?("127.") + return info.name + end + end + return "eth0" # fall back to an old reliable name + end + + Vagrant.configure(2) do |config| + config.ssh.forward_agent = true # so you can use git ssh://... + + # add a bridged network interface. (try to detect name, then guess MacOS names, too) + interface_guesses = [get_good_ifc(), 'en0: Ethernet', 'en1: Wi-Fi (AirPort)'] + config.vm.network "public_network", bridge: interface_guesses + if ARGV[0] == "up" + puts "Trying bridge network using interfaces: #{interface_guesses}" + end + config.vm.provision "shell", inline: "ip address", run: "always" # make user feel good + + # . . . . . . . . . . . . Define machine QUAIL1 . . . . . . . . . . . . . . + config.vm.define "quail1", primary: true do |quail_config| + quail_config.vm.box = "boxesio/xenial64-standard" # a public VMware & Virtualbox box + quail_config.vm.hostname = "quail1." + DOMAIN # supply a name in our bevy + quail_config.vm.provider "virtualbox" do |v| + v.memory = 1024 # limit memory for the virtual box + v.cpus = 1 + v.linked_clone = true # make a soft copy of the base Vagrant box + v.customize ["modifyvm", :id, "--natnet1", "192.168.128.0/24"] # do not use 10.x network for NAT + end + end + end + +.. code-block:: yaml + + # file /etc/salt/cloud.profiles.d/my_vagrant_profiles.conf on bevymaster + q1: + host: my_laptop # the Salt id of your virtual machine host + machine: quail1 # a machine name in the Vagrantfile (if not primary) + vagrant_runas: my_username # owner of Vagrant box files on "my_laptop" + cwd: '/home/my_username' # the path (on "my_laptop") of the Vagrantfile + provider: my_vagrant_provider # name of entry in provider.conf file + target_network: '10.0.0.0/8' # VM external address will be somewhere here + +.. code-block:: yaml + + # file /etc/salt/cloud.providers.d/vagrant_provider.conf on bevymaster + my_vagrant_provider: + driver: vagrant + minion: + master: 10.124.30.7 # the hard address of the master + + +Create and use your new Salt minion +----------------------------------- + +- Typing on the Salt master computer ``bevymaster``, tell it to create a new minion named ``v1`` using profile ``q1``... + +.. code-block:: bash + + sudo salt-cloud -p q1 v1 + sudo salt v1 network.ip_addrs + [ you get a list of ip addresses, including the bridged one ] + +- logged in to your laptop (or some other computer known to github)... + + \[NOTE:\] if you are using MacOS, you need to type ``ssh-add -K`` after each boot, + unless you use one of the methods in `this gist`_. + +.. _this gist: https://github.com/jirsbek/SSH-keys-in-macOS-Sierra-keychain + +.. code-block:: bash + + ssh -A vagrant@< the bridged network address > + # [ or, if you are at /home/my_username/ on my_laptop ] + vagrant ssh quail1 + +- then typing on your new node "v1" (a.k.a. quail1.bevy1.test)... + +.. code-block:: bash + + password: vagrant + # [ stuff types out ... ] + + ls -al /vagrant + # [ should be shared /home/my_username from my_laptop ] + + # you can access other network facilities using the ssh authorization + # as recorded in your ~.ssh/ directory on my_laptop ... + + sudo apt update + sudo apt install git + git clone ssh://git@github.com/yourID/your_project + # etc... + From e9c7c69402613bf7494377eb30b74ebc3ae4a591 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Wed, 11 Oct 2017 05:02:26 -0600 Subject: [PATCH 566/633] documentation for vagrant cloud & state --- doc/topics/releases/oxygen.rst | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 3c99a58ba1..50638195d2 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -132,6 +132,56 @@ file. For example: These commands will run in sequence **before** the bootstrap script is executed. +New salt-cloud Grains +===================== + +When salt cloud creates a new minon, it will now add grain information +to the minion configuration file, identifying the resources originally used +to create it. + +The generated grain information will appear similar to: + +.. code-block:: yaml + grains: + salt-cloud: + driver: ec2 + provider: my_ec2:ec2 + profile: ec2-web +The generation of salt-cloud grains can be surpressed by the +option ``enable_cloud_grains: 'False'`` in the cloud configuration file. + +Upgraded Saltify Driver +======================= + +The salt-cloud Saltify driver is used to provision machines which +are not controlled by a dedicated cloud supervisor (such as typical hardware +machines) by pushing a salt-bootstrap command to them and accepting them on +the salt master. Creation of a node has been its only function and no other +salt-cloud commands were implemented. + +With this upgrade, it can use the salt-api to provide advanced control, +such as rebooting a machine, querying it along with conventional cloud minions, +and, ultimately, disconnecting it from its master. + +After disconnection from ("destroying" on) one master, a machine can be +re-purposed by connecting to ("creating" on) a subsequent master. + +New Vagrant Driver +================== + +The salt-cloud Vagrant driver brings virtual machines running in a limited +environment, such as a programmer's workstation, under salt-cloud control. +This can be useful for experimentation, instruction, or testing salt configurations. + +Using salt-api on the master, and a salt-minion running on the host computer, +the Vagrant driver can create (``vagrant up``), restart (``vagrant reload``), +and destroy (``vagrant destroy``) VMs, as controlled by salt-cloud profiles +which designate a ``Vagrantfile`` on the host machine. + +The master can be a very limited machine, such as a Raspberry Pi, or a small +VagrantBox VM. + + New pillar/master_tops module called saltclass ---------------------------------------------- From 1d4a6c394979f2067cc526e18e26c9bf40c5a8f2 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 11 Oct 2017 09:25:41 -0400 Subject: [PATCH 567/633] Lint: Fixup undefined variable errors --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 519f753401..344aa78d92 100755 --- a/setup.py +++ b/setup.py @@ -190,8 +190,9 @@ class WriteSaltVersion(Command): exit(1) if not self.distribution.with_salt_version: - salt_version = __saltstack_version__ + salt_version = __saltstack_version__ # pylint: disable=undefined-variable else: + from salt.version import SaltStackVersion salt_version = SaltStackVersion.parse(self.distribution.with_salt_version) # pylint: disable=E0602 From 471ff35c2fa090a96d6fa201e5663eb04f71542f Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 9 Oct 2017 17:57:48 +0200 Subject: [PATCH 568/633] Bugfix: always return a string "list" on unknown job target type. --- salt/returners/couchbase_return.py | 2 +- salt/returners/postgres_local_cache.py | 2 +- salt/runners/jobs.py | 2 +- salt/utils/jid.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/salt/returners/couchbase_return.py b/salt/returners/couchbase_return.py index 24c3a9105a..f5adecc2e7 100644 --- a/salt/returners/couchbase_return.py +++ b/salt/returners/couchbase_return.py @@ -309,7 +309,7 @@ def _format_job_instance(job): 'Arguments': list(job.get('arg', [])), # unlikely but safeguard from invalid returns 'Target': job.get('tgt', 'unknown-target'), - 'Target-type': job.get('tgt_type', []), + 'Target-type': job.get('tgt_type', 'list'), 'User': job.get('user', 'root')} if 'metadata' in job: diff --git a/salt/returners/postgres_local_cache.py b/salt/returners/postgres_local_cache.py index 422f8c77c7..28dc2f565c 100644 --- a/salt/returners/postgres_local_cache.py +++ b/salt/returners/postgres_local_cache.py @@ -180,7 +180,7 @@ def _format_job_instance(job): 'Arguments': json.loads(job.get('arg', '[]')), # unlikely but safeguard from invalid returns 'Target': job.get('tgt', 'unknown-target'), - 'Target-type': job.get('tgt_type', []), + 'Target-type': job.get('tgt_type', 'list'), 'User': job.get('user', 'root')} # TODO: Add Metadata support when it is merged from develop return ret diff --git a/salt/runners/jobs.py b/salt/runners/jobs.py index 82abd56eae..fae7942e38 100644 --- a/salt/runners/jobs.py +++ b/salt/runners/jobs.py @@ -542,7 +542,7 @@ def _format_job_instance(job): 'Arguments': list(job.get('arg', [])), # unlikely but safeguard from invalid returns 'Target': job.get('tgt', 'unknown-target'), - 'Target-type': job.get('tgt_type', []), + 'Target-type': job.get('tgt_type', 'list'), 'User': job.get('user', 'root')} if 'metadata' in job: diff --git a/salt/utils/jid.py b/salt/utils/jid.py index 3f4ef296a2..4dbf0d2c6f 100644 --- a/salt/utils/jid.py +++ b/salt/utils/jid.py @@ -65,7 +65,7 @@ def format_job_instance(job): 'Arguments': list(job.get('arg', [])), # unlikely but safeguard from invalid returns 'Target': job.get('tgt', 'unknown-target'), - 'Target-type': job.get('tgt_type', []), + 'Target-type': job.get('tgt_type', 'list'), 'User': job.get('user', 'root')} if 'metadata' in job: From 41c086faf7176a1ba2cd20bdbb5d79019a0172f4 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Thu, 5 Oct 2017 14:07:14 -0600 Subject: [PATCH 569/633] convert jinja context to dictionary For `show_full_context`, it is useful to have this as a serializable dictionary so that it can be dumped and actually parsed using `{{show_full_context|yaml(False)}}` --- doc/topics/jinja/index.rst | 2 +- salt/utils/jinja.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/topics/jinja/index.rst b/doc/topics/jinja/index.rst index d57fbcbb04..066676b08d 100644 --- a/doc/topics/jinja/index.rst +++ b/doc/topics/jinja/index.rst @@ -1727,7 +1727,7 @@ in the current Jinja context. .. code-block:: jinja - Context is: {{ show_full_context() }} + Context is: {{ show_full_context()|yaml(False) }} .. jinja_ref:: logs diff --git a/salt/utils/jinja.py b/salt/utils/jinja.py index 38896e2b81..ec24821e25 100644 --- a/salt/utils/jinja.py +++ b/salt/utils/jinja.py @@ -582,7 +582,7 @@ def symmetric_difference(lst1, lst2): @jinja2.contextfunction def show_full_context(ctx): - return ctx + return salt.utils.simple_types_filter({key: value for key, value in ctx.items()}) class SerializerExtension(Extension, object): From d3d76c79caf89ee51bd3a227be5ad65039aa36ea Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 11 Oct 2017 14:12:51 -0500 Subject: [PATCH 570/633] Fix bad copypasta in warning message --- salt/utils/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 1e33813e01..9d5b6e4de8 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -2482,8 +2482,8 @@ def decode_dict(data): import salt.utils.data salt.utils.versions.warn_until( 'Neon', - 'Use of \'salt.utils.compare_dicts\' detected. This function ' - 'has been moved to \'salt.utils.data.compare_dicts\' as of ' + 'Use of \'salt.utils.decode_dict\' detected. This function ' + 'has been moved to \'salt.utils.data.decode_dict\' as of ' 'Salt Oxygen. This warning will be removed in Salt Neon.' ) return salt.utils.data.decode_dict(data) From 554c685ce53bd43db9cfc1e88c6a52b8a250d49d Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 9 Oct 2017 14:26:22 -0500 Subject: [PATCH 571/633] Move 4 functions from salt.utils - salt.utils.reinit_crypto -> salt.utils.crypt.reinit_crypto - salt.utils.appendproctitle -> salt.utils.process.appendproctitle - salt.utils.daemonize -> salt.utils.process.daemonize - salt.utils.daemonize_if -> salt.utils.process.daemonize_if --- salt/client/mixins.py | 4 +- salt/cloud/__init__.py | 7 +- salt/crypt.py | 7 +- salt/daemons/flo/jobber.py | 3 +- salt/log/setup.py | 5 +- salt/master.py | 13 ++- salt/minion.py | 7 +- salt/modules/inspectlib/collector.py | 10 +- salt/scripts.py | 3 +- salt/transport/tcp.py | 3 +- salt/transport/zeromq.py | 5 +- salt/utils/__init__.py | 157 +++++++++------------------ salt/utils/event.py | 5 +- salt/utils/parsers.py | 3 +- salt/utils/process.py | 87 ++++++++++++++- salt/utils/reactor.py | 2 +- salt/utils/schedule.py | 5 +- salt/utils/vt.py | 6 +- tests/integration/__init__.py | 11 +- tests/unit/utils/test_process.py | 36 +++++- tests/unit/utils/test_utils.py | 20 +--- 21 files changed, 222 insertions(+), 177 deletions(-) diff --git a/salt/client/mixins.py b/salt/client/mixins.py index c16ec72f53..0df7cf7dd1 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -16,7 +16,7 @@ import copy as pycopy # Import Salt libs import salt.exceptions import salt.minion -import salt.utils # Can be removed once daemonize, format_call are moved +import salt.utils # Can be removed once format_call is moved import salt.utils.args import salt.utils.doc import salt.utils.error @@ -469,7 +469,7 @@ class AsyncClientMixin(object): # Shutdown the multiprocessing before daemonizing salt.log.setup.shutdown_multiprocessing_logging() - salt.utils.daemonize() + salt.utils.process.daemonize() # Reconfigure multiprocessing logging after daemonizing salt.log.setup.setup_multiprocessing_logging() diff --git a/salt/cloud/__init__.py b/salt/cloud/__init__.py index 0372f91f63..04b84a1759 100644 --- a/salt/cloud/__init__.py +++ b/salt/cloud/__init__.py @@ -33,6 +33,7 @@ import salt.utils import salt.utils.args import salt.utils.cloud import salt.utils.context +import salt.utils.crypt import salt.utils.dictupdate import salt.utils.files import salt.syspaths @@ -2300,7 +2301,7 @@ def create_multiprocessing(parallel_data, queue=None): This function will be called from another process when running a map in parallel mode. The result from the create is always a json object. ''' - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() parallel_data['opts']['output'] = 'json' cloud = Cloud(parallel_data['opts']) @@ -2332,7 +2333,7 @@ def destroy_multiprocessing(parallel_data, queue=None): This function will be called from another process when running a map in parallel mode. The result from the destroy is always a json object. ''' - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() parallel_data['opts']['output'] = 'json' clouds = salt.loader.clouds(parallel_data['opts']) @@ -2368,7 +2369,7 @@ def run_parallel_map_providers_query(data, queue=None): This function will be called from another process when building the providers map. ''' - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() cloud = Cloud(data['opts']) try: diff --git a/salt/crypt.py b/salt/crypt.py index 3d002a3847..764015e480 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -50,7 +50,8 @@ import salt.defaults.exitcodes import salt.payload import salt.transport.client import salt.transport.frame -import salt.utils # Can be removed when pem_finger, reinit_crypto are moved +import salt.utils # Can be removed when pem_finger is moved +import salt.utils.crypt import salt.utils.decorators import salt.utils.event import salt.utils.files @@ -113,7 +114,7 @@ def gen_keys(keydir, keyname, keysize, user=None, passphrase=None): priv = u'{0}.pem'.format(base) pub = u'{0}.pub'.format(base) - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() gen = RSA.generate(bits=keysize, e=65537) if os.path.isfile(priv): # Between first checking and the generation another process has made @@ -446,7 +447,7 @@ class AsyncAuth(object): self.io_loop = io_loop or tornado.ioloop.IOLoop.current() - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() key = self.__key(self.opts) # TODO: if we already have creds for this key, lets just re-use if key in AsyncAuth.creds_map: diff --git a/salt/daemons/flo/jobber.py b/salt/daemons/flo/jobber.py index da11b7e5c6..1f50b3227e 100644 --- a/salt/daemons/flo/jobber.py +++ b/salt/daemons/flo/jobber.py @@ -24,6 +24,7 @@ import salt.utils.args import salt.utils.data import salt.utils.files import salt.utils.kinds as kinds +import salt.utils.process import salt.utils.stringutils import salt.transport from raet import raeting, nacling @@ -273,7 +274,7 @@ class SaltRaetNixJobber(ioflo.base.deeding.Deed): data = msg['pub'] fn_ = os.path.join(self.proc_dir, data['jid']) self.opts['__ex_id'] = data['jid'] - salt.utils.daemonize_if(self.opts) + salt.utils.process.daemonize_if(self.opts) salt.transport.jobber_stack = stack = self._setup_jobber_stack() # set up return destination from source diff --git a/salt/log/setup.py b/salt/log/setup.py index 73361391ae..9dc1966b30 100644 --- a/salt/log/setup.py +++ b/salt/log/setup.py @@ -38,7 +38,6 @@ GARBAGE = logging.GARBAGE = 1 QUIET = logging.QUIET = 1000 # Import salt libs -import salt.utils # Can be removed once appendproctitle is moved from salt.textformat import TextFormat from salt.log.handlers import (TemporaryLoggingHandler, StreamHandler, @@ -998,7 +997,9 @@ def patch_python_logging_handlers(): def __process_multiprocessing_logging_queue(opts, queue): - salt.utils.appendproctitle('MultiprocessingLoggingQueue') + # Avoid circular import + import salt.utils.process + salt.utils.process.appendproctitle('MultiprocessingLoggingQueue') # Assign UID/GID of user to proc if set from salt.utils.verify import check_user diff --git a/salt/master.py b/salt/master.py index b7595ab1aa..298a7c9453 100644 --- a/salt/master.py +++ b/salt/master.py @@ -47,7 +47,7 @@ import tornado.gen # pylint: disable=F0401 # Import salt libs import salt.crypt -import salt.utils +import salt.utils # Can be removed once get_values_of_matching_keys is moved import salt.client import salt.payload import salt.pillar @@ -65,6 +65,7 @@ import salt.transport.server import salt.log.setup import salt.utils.args import salt.utils.atomicfile +import salt.utils.crypt import salt.utils.event import salt.utils.files import salt.utils.gitfs @@ -222,7 +223,7 @@ class Maintenance(salt.utils.process.SignalHandlingMultiprocessingProcess): This is where any data that needs to be cleanly maintained from the master is maintained. ''' - salt.utils.appendproctitle(u'Maintenance') + salt.utils.process.appendproctitle(u'Maintenance') # init things that need to be done after the process is forked self._post_fork_init() @@ -652,7 +653,7 @@ class Halite(salt.utils.process.SignalHandlingMultiprocessingProcess): ''' Fire up halite! ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) halite.start(self.hopts) @@ -912,13 +913,13 @@ class MWorker(salt.utils.process.SignalHandlingMultiprocessingProcess): ''' Start a Master Worker ''' - salt.utils.appendproctitle(self.name) + salt.utils.process.appendproctitle(self.name) self.clear_funcs = ClearFuncs( self.opts, self.key, ) self.aes_funcs = AESFuncs(self.opts) - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() self.__bind() @@ -2163,7 +2164,7 @@ class FloMWorker(MWorker): ''' Prepare the needed objects and socket for iteration within ioflo ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.crypt.appendproctitle(self.__class__.__name__) self.clear_funcs = salt.master.ClearFuncs( self.opts, self.key, diff --git a/salt/minion.py b/salt/minion.py index 885fb0541b..646d2a433b 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -98,6 +98,7 @@ import salt.utils.minion import salt.utils.minions import salt.utils.network import salt.utils.platform +import salt.utils.process import salt.utils.schedule import salt.utils.user import salt.utils.zeromq @@ -1470,12 +1471,12 @@ class Minion(MinionBase): # Shutdown the multiprocessing before daemonizing salt.log.setup.shutdown_multiprocessing_logging() - salt.utils.daemonize_if(opts) + salt.utils.process.daemonize_if(opts) # Reconfigure multiprocessing logging after daemonizing salt.log.setup.setup_multiprocessing_logging() - salt.utils.appendproctitle(u'{0}._thread_return {1}'.format(cls.__name__, data[u'jid'])) + salt.utils.process.appendproctitle(u'{0}._thread_return {1}'.format(cls.__name__, data[u'jid'])) sdata = {u'pid': os.getpid()} sdata.update(data) @@ -1654,7 +1655,7 @@ class Minion(MinionBase): This method should be used as a threading target, start the actual minion side execution. ''' - salt.utils.appendproctitle(u'{0}._thread_multi_return {1}'.format(cls.__name__, data[u'jid'])) + salt.utils.process.appendproctitle(u'{0}._thread_multi_return {1}'.format(cls.__name__, data[u'jid'])) multifunc_ordered = opts.get(u'multifunc_ordered', False) num_funcs = len(data[u'fun']) if multifunc_ordered: diff --git a/salt/modules/inspectlib/collector.py b/salt/modules/inspectlib/collector.py index d67b2519bb..74951391cb 100644 --- a/salt/modules/inspectlib/collector.py +++ b/salt/modules/inspectlib/collector.py @@ -28,7 +28,7 @@ from salt.modules.inspectlib import kiwiproc from salt.modules.inspectlib.entities import (AllowedDir, IgnoredDir, Package, PayloadFile, PackageCfgFile) -import salt.utils # Can be removed when reinit_crypto is moved +import salt.utils.crypt import salt.utils.files import salt.utils.fsutils import salt.utils.path @@ -505,10 +505,10 @@ if __name__ == '__main__': # Double-fork stuff try: if os.fork() > 0: - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() sys.exit(0) else: - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() except OSError as ex: sys.exit(1) @@ -518,12 +518,12 @@ if __name__ == '__main__': try: pid = os.fork() if pid > 0: - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() with salt.utils.files.fopen(os.path.join(pidfile, EnvLoader.PID_FILE), 'w') as fp_: fp_.write('{0}\n'.format(pid)) sys.exit(0) except OSError as ex: sys.exit(1) - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() main(dbfile, pidfile, mode) diff --git a/salt/scripts.py b/salt/scripts.py index acef4987f9..0e7da6a8ea 100644 --- a/salt/scripts.py +++ b/salt/scripts.py @@ -97,11 +97,12 @@ def minion_process(): Start a minion process ''' import salt.utils.platform + import salt.utils.process import salt.cli.daemons # salt_minion spawns this function in a new process - salt.utils.appendproctitle(u'KeepAlive') + salt.utils.process.appendproctitle(u'KeepAlive') def handle_hup(manager, sig, frame): manager.minion.reload() diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py index dcfd8ef685..ffc6871949 100644 --- a/salt/transport/tcp.py +++ b/salt/transport/tcp.py @@ -23,6 +23,7 @@ import salt.utils import salt.utils.async import salt.utils.event import salt.utils.platform +import salt.utils.process import salt.utils.verify import salt.payload import salt.exceptions @@ -1314,7 +1315,7 @@ class TCPPubServerChannel(salt.transport.server.PubServerChannel): ''' Bind to the interface specified in the configuration file ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) if log_queue is not None: salt.log.setup.set_multiprocessing_logging_queue(log_queue) diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py index 7bb5409baf..57ffebcc63 100644 --- a/salt/transport/zeromq.py +++ b/salt/transport/zeromq.py @@ -21,6 +21,7 @@ import salt.crypt import salt.utils import salt.utils.verify import salt.utils.event +import salt.utils.process import salt.utils.stringutils import salt.payload import salt.transport.client @@ -449,7 +450,7 @@ class ZeroMQReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt. Multiprocessing target for the zmq queue device ''' self.__setup_signals() - salt.utils.appendproctitle('MWorkerQueue') + salt.utils.process.appendproctitle('MWorkerQueue') self.context = zmq.Context(self.opts['worker_threads']) # Prepare the zeromq sockets self.uri = 'tcp://{interface}:{ret_port}'.format(**self.opts) @@ -694,7 +695,7 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel): ''' Bind to the interface specified in the configuration file ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) # Set up the context context = zmq.Context(1) # Prepare minion publish socket diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 1e33813e01..fe565efd69 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -50,12 +50,6 @@ try: except ImportError: HAS_CPROFILE = False -try: - import Crypto.Random - HAS_CRYPTO = True -except ImportError: - HAS_CRYPTO = False - try: import timelib HAS_TIMELIB = True @@ -74,12 +68,6 @@ try: except ImportError: HAS_WIN32API = False -try: - import setproctitle - HAS_SETPROCTITLE = True -except ImportError: - HAS_SETPROCTITLE = False - try: import ctypes import ctypes.util @@ -173,91 +161,6 @@ def get_master_key(key_user, opts, skip_perm_errors=False): return '' -def reinit_crypto(): - ''' - When a fork arises, pycrypto needs to reinit - From its doc:: - - Caveat: For the random number generator to work correctly, - you must call Random.atfork() in both the parent and - child processes after using os.fork() - - ''' - if HAS_CRYPTO: - Crypto.Random.atfork() - - -def daemonize(redirect_out=True): - ''' - Daemonize a process - ''' - # Late import to avoid circular import. - import salt.utils.files - - try: - pid = os.fork() - if pid > 0: - # exit first parent - reinit_crypto() - sys.exit(salt.defaults.exitcodes.EX_OK) - except OSError as exc: - log.error( - 'fork #1 failed: {0} ({1})'.format(exc.errno, exc.strerror) - ) - sys.exit(salt.defaults.exitcodes.EX_GENERIC) - - # decouple from parent environment - os.chdir('/') - # noinspection PyArgumentList - os.setsid() - os.umask(18) - - # do second fork - try: - pid = os.fork() - if pid > 0: - reinit_crypto() - sys.exit(salt.defaults.exitcodes.EX_OK) - except OSError as exc: - log.error( - 'fork #2 failed: {0} ({1})'.format( - exc.errno, exc.strerror - ) - ) - sys.exit(salt.defaults.exitcodes.EX_GENERIC) - - reinit_crypto() - - # A normal daemonization redirects the process output to /dev/null. - # Unfortunately when a python multiprocess is called the output is - # not cleanly redirected and the parent process dies when the - # multiprocessing process attempts to access stdout or err. - if redirect_out: - with salt.utils.files.fopen('/dev/null', 'r+') as dev_null: - # Redirect python stdin/out/err - # and the os stdin/out/err which can be different - os.dup2(dev_null.fileno(), sys.stdin.fileno()) - os.dup2(dev_null.fileno(), sys.stdout.fileno()) - os.dup2(dev_null.fileno(), sys.stderr.fileno()) - os.dup2(dev_null.fileno(), 0) - os.dup2(dev_null.fileno(), 1) - os.dup2(dev_null.fileno(), 2) - - -def daemonize_if(opts): - ''' - Daemonize a module function process if multiprocessing is True and the - process is not being called by salt-call - ''' - if 'salt-call' in sys.argv[0]: - return - if not opts.get('multiprocessing', True): - return - if sys.platform.startswith('win'): - return - daemonize(False) - - def profile_func(filename=None): ''' Decorator for adding profiling to a nested function in Salt @@ -1385,14 +1288,6 @@ def import_json(): continue -def appendproctitle(name): - ''' - Append "name" to the current process title - ''' - if HAS_SETPROCTITLE: - setproctitle.setproctitle(setproctitle.getproctitle() + ' ' + name) - - def human_size_to_bytes(human_size): ''' Convert human-readable units to bytes @@ -1517,6 +1412,58 @@ def fnmatch_multiple(candidates, pattern): # MOVED FUNCTIONS # # These are deprecated and will be removed in Neon. +def appendproctitle(name): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.process + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.appendproctitle\' detected. This function has been ' + 'moved to \'salt.utils.process.appendproctitle\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.process.appendproctitle(name) + + +def daemonize(redirect_out=True): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.process + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.daemonize\' detected. This function has been ' + 'moved to \'salt.utils.process.daemonize\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.process.daemonize(redirect_out) + + +def daemonize_if(opts): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.process + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.daemonize_if\' detected. This function has been ' + 'moved to \'salt.utils.process.daemonize_if\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.process.daemonize_if(opts) + + +def reinit_crypto(): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.crypt + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.reinit_crypto\' detected. This function has been ' + 'moved to \'salt.utils.crypt.reinit_crypto\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.crypt.reinit_crypto() + + def to_bytes(s, encoding=None): # Late import to avoid circular import. import salt.utils.versions diff --git a/salt/utils/event.py b/salt/utils/event.py index c7e8b457a3..e47161b631 100644 --- a/salt/utils/event.py +++ b/salt/utils/event.py @@ -72,7 +72,6 @@ import tornado.iostream # Import salt libs import salt.config import salt.payload -import salt.utils import salt.utils.async import salt.utils.cache import salt.utils.dicttrim @@ -1078,7 +1077,7 @@ class EventPublisher(salt.utils.process.SignalHandlingMultiprocessingProcess): ''' Bind the pub and pull sockets for events ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) self.io_loop = tornado.ioloop.IOLoop() with salt.utils.async.current_ioloop(self.io_loop): if self.opts['ipc_mode'] == 'tcp': @@ -1243,7 +1242,7 @@ class EventReturn(salt.utils.process.SignalHandlingMultiprocessingProcess): ''' Spin up the multiprocess event returner ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) self.event = get_event('master', opts=self.opts, listen=True) events = self.event.iter_events(full=True) self.event.fire_event({}, 'salt/event_listen/start') diff --git a/salt/utils/parsers.py b/salt/utils/parsers.py index 7b2fca742a..6be34f8c0d 100644 --- a/salt/utils/parsers.py +++ b/salt/utils/parsers.py @@ -36,6 +36,7 @@ import salt.utils.files import salt.utils.jid import salt.utils.kinds as kinds import salt.utils.platform +import salt.utils.process import salt.utils.user import salt.utils.xdg from salt.defaults import DEFAULT_TARGET_DELIM @@ -1004,7 +1005,7 @@ class DaemonMixIn(six.with_metaclass(MixInMeta, object)): log.shutdown_multiprocessing_logging_listener(daemonizing=True) # Late import so logging works correctly - salt.utils.daemonize() + salt.utils.process.daemonize() # Setup the multiprocessing log queue listener if enabled self._setup_mp_logging_listener() diff --git a/salt/utils/process.py b/salt/utils/process.py index 39c7e02145..f156145a4c 100644 --- a/salt/utils/process.py +++ b/salt/utils/process.py @@ -20,7 +20,6 @@ import socket # Import salt libs import salt.defaults.exitcodes -import salt.utils # Can be removed once appendproctitle is moved import salt.utils.files import salt.utils.path import salt.utils.platform @@ -43,6 +42,90 @@ try: except ImportError: pass +try: + import setproctitle + HAS_SETPROCTITLE = True +except ImportError: + HAS_SETPROCTITLE = False + + +def appendproctitle(name): + ''' + Append "name" to the current process title + ''' + if HAS_SETPROCTITLE: + setproctitle.setproctitle(setproctitle.getproctitle() + ' ' + name) + + +def daemonize(redirect_out=True): + ''' + Daemonize a process + ''' + # Avoid circular import + import salt.utils.crypt + try: + pid = os.fork() + if pid > 0: + # exit first parent + salt.utils.crypt.reinit_crypto() + sys.exit(salt.defaults.exitcodes.EX_OK) + except OSError as exc: + log.error( + 'fork #1 failed: {0} ({1})'.format(exc.errno, exc.strerror) + ) + sys.exit(salt.defaults.exitcodes.EX_GENERIC) + + # decouple from parent environment + os.chdir('/') + # noinspection PyArgumentList + os.setsid() + os.umask(18) + + # do second fork + try: + pid = os.fork() + if pid > 0: + salt.utils.crypt.reinit_crypto() + sys.exit(salt.defaults.exitcodes.EX_OK) + except OSError as exc: + log.error( + 'fork #2 failed: {0} ({1})'.format( + exc.errno, exc.strerror + ) + ) + sys.exit(salt.defaults.exitcodes.EX_GENERIC) + + salt.utils.crypt.reinit_crypto() + + # A normal daemonization redirects the process output to /dev/null. + # Unfortunately when a python multiprocess is called the output is + # not cleanly redirected and the parent process dies when the + # multiprocessing process attempts to access stdout or err. + if redirect_out: + with salt.utils.files.fopen('/dev/null', 'r+') as dev_null: + # Redirect python stdin/out/err + # and the os stdin/out/err which can be different + os.dup2(dev_null.fileno(), sys.stdin.fileno()) + os.dup2(dev_null.fileno(), sys.stdout.fileno()) + os.dup2(dev_null.fileno(), sys.stderr.fileno()) + os.dup2(dev_null.fileno(), 0) + os.dup2(dev_null.fileno(), 1) + os.dup2(dev_null.fileno(), 2) + + +def daemonize_if(opts): + ''' + Daemonize a module function process if multiprocessing is True and the + process is not being called by salt-call + ''' + if 'salt-call' in sys.argv[0]: + return + if not opts.get('multiprocessing', True): + return + if sys.platform.startswith('win'): + return + daemonize(False) + def systemd_notify_call(action): process = subprocess.Popen(['systemd-notify', action], stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -397,7 +480,7 @@ class ProcessManager(object): Load and start all available api modules ''' log.debug('Process Manager starting!') - salt.utils.appendproctitle(self.name) + appendproctitle(self.name) # make sure to kill the subprocesses if the parent is killed if signal.getsignal(signal.SIGTERM) is signal.SIG_DFL: diff --git a/salt/utils/reactor.py b/salt/utils/reactor.py index 6dfd6d0a39..5bc60b8bd4 100644 --- a/salt/utils/reactor.py +++ b/salt/utils/reactor.py @@ -232,7 +232,7 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat ''' Enter into the server loop ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) # instantiate some classes inside our new process self.event = salt.utils.event.get_event( diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index d29a3bf314..055c7fc0db 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -337,7 +337,6 @@ import copy # Import Salt libs import salt.config -import salt.utils # Can be removed once appendproctitle and daemonize_if are moved import salt.utils.args import salt.utils.error import salt.utils.event @@ -779,7 +778,7 @@ class Schedule(object): log.warning('schedule: The metadata parameter must be ' 'specified as a dictionary. Ignoring.') - salt.utils.appendproctitle('{0} {1}'.format(self.__class__.__name__, ret['jid'])) + salt.utils.process.appendproctitle('{0} {1}'.format(self.__class__.__name__, ret['jid'])) if not self.standalone: proc_fn = os.path.join( @@ -818,7 +817,7 @@ class Schedule(object): log_setup.setup_multiprocessing_logging() # Don't *BEFORE* to go into try to don't let it triple execute the finally section. - salt.utils.daemonize_if(self.opts) + salt.utils.process.daemonize_if(self.opts) # TODO: Make it readable! Splt to funcs, remove nested try-except-finally sections. try: diff --git a/salt/utils/vt.py b/salt/utils/vt.py index b3ffb6d239..bdb609f493 100644 --- a/salt/utils/vt.py +++ b/salt/utils/vt.py @@ -52,7 +52,7 @@ except ImportError: import resource # Import salt libs -import salt.utils +import salt.utils.crypt import salt.utils.stringutils from salt.ext.six import string_types from salt.log.setup import LOG_LEVELS @@ -493,7 +493,7 @@ class Terminal(object): # Close parent FDs os.close(stdout_parent_fd) os.close(stderr_parent_fd) - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() # ----- Make STDOUT the controlling PTY ---------------------> child_name = os.ttyname(stdout_child_fd) @@ -554,7 +554,7 @@ class Terminal(object): # <---- Duplicate Descriptors -------------------------------- else: # Parent. Close Child PTY's - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() os.close(stdout_child_fd) os.close(stderr_child_fd) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 92dcd8fedf..0ede993700 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -49,7 +49,6 @@ import salt.minion import salt.runner import salt.output import salt.version -import salt.utils # Can be removed once appendproctitle is moved import salt.utils.color import salt.utils.files import salt.utils.path @@ -261,7 +260,7 @@ class TestDaemon(object): def start_daemon(self, cls, opts, start_fun): def start(cls, opts, start_fun): - salt.utils.appendproctitle('{0}-{1}'.format(self.__class__.__name__, cls.__name__)) + salt.utils.process.appendproctitle('{0}-{1}'.format(self.__class__.__name__, cls.__name__)) daemon = cls(opts) getattr(daemon, start_fun)() process = multiprocessing.Process(target=start, @@ -1143,7 +1142,7 @@ class TestDaemon(object): ] def wait_for_minion_connections(self, targets, timeout): - salt.utils.appendproctitle('WaitForMinionConnections') + salt.utils.process.appendproctitle('WaitForMinionConnections') sys.stdout.write( ' {LIGHT_BLUE}*{ENDC} Waiting at most {0} for minions({1}) to ' 'connect back\n'.format( @@ -1286,13 +1285,13 @@ class TestDaemon(object): return True def sync_minion_states(self, targets, timeout=None): - salt.utils.appendproctitle('SyncMinionStates') + salt.utils.process.appendproctitle('SyncMinionStates') self.sync_minion_modules_('states', targets, timeout=timeout) def sync_minion_modules(self, targets, timeout=None): - salt.utils.appendproctitle('SyncMinionModules') + salt.utils.process.appendproctitle('SyncMinionModules') self.sync_minion_modules_('modules', targets, timeout=timeout) def sync_minion_grains(self, targets, timeout=None): - salt.utils.appendproctitle('SyncMinionGrains') + salt.utils.process.appendproctitle('SyncMinionGrains') self.sync_minion_modules_('grains', targets, timeout=timeout) diff --git a/tests/unit/utils/test_process.py b/tests/unit/utils/test_process.py index 25be0b7a1b..af17079697 100644 --- a/tests/unit/utils/test_process.py +++ b/tests/unit/utils/test_process.py @@ -10,9 +10,13 @@ import multiprocessing # Import Salt Testing libs from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + patch, + NO_MOCK, + NO_MOCK_REASON +) # Import salt libs -import salt.utils import salt.utils.process # Import 3rd-party libs @@ -27,7 +31,7 @@ class TestProcessManager(TestCase): Make sure that the process is alive 2s later ''' def spin(): - salt.utils.appendproctitle('test_basic') + salt.utils.process.appendproctitle('test_basic') while True: time.sleep(1) @@ -50,7 +54,7 @@ class TestProcessManager(TestCase): def test_kill(self): def spin(): - salt.utils.appendproctitle('test_kill') + salt.utils.process.appendproctitle('test_kill') while True: time.sleep(1) @@ -79,7 +83,7 @@ class TestProcessManager(TestCase): Make sure that the process is alive 2s later ''' def die(): - salt.utils.appendproctitle('test_restarting') + salt.utils.process.appendproctitle('test_restarting') process_manager = salt.utils.process.ProcessManager() process_manager.add_process(die) @@ -101,7 +105,7 @@ class TestProcessManager(TestCase): @skipIf(sys.version_info < (2, 7), 'Needs > Py 2.7 due to bug in stdlib') def test_counter(self): def incr(counter, num): - salt.utils.appendproctitle('test_counter') + salt.utils.process.appendproctitle('test_counter') for _ in range(0, num): counter.value += 1 counter = multiprocessing.Value('i', 0) @@ -162,3 +166,25 @@ class TestThreadPool(TestCase): self.assertEqual(counter.value, 0) # make sure the queue is still full self.assertEqual(pool._job_queue.qsize(), 1) + + +class TestProcess(TestCase): + + @skipIf(NO_MOCK, NO_MOCK_REASON) + def test_daemonize_if(self): + # pylint: disable=assignment-from-none + with patch('sys.argv', ['salt-call']): + ret = salt.utils.process.daemonize_if({}) + self.assertEqual(None, ret) + + ret = salt.utils.process.daemonize_if({'multiprocessing': False}) + self.assertEqual(None, ret) + + with patch('sys.platform', 'win'): + ret = salt.utils.process.daemonize_if({}) + self.assertEqual(None, ret) + + with patch('salt.utils.process.daemonize'): + salt.utils.process.daemonize_if({}) + self.assertTrue(salt.utils.process.daemonize.called) + # pylint: enable=assignment-from-none diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 08620470bc..60445c5b00 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -18,6 +18,7 @@ from tests.support.mock import ( import salt.utils import salt.utils.data import salt.utils.jid +import salt.utils.process import salt.utils.yamlencoding import salt.utils.zeromq from salt.exceptions import SaltSystemExit, CommandNotFoundError @@ -432,25 +433,6 @@ class UtilsTestCase(TestCase): ret = salt.utils.data.repack_dictlist(LOREM_IPSUM) self.assertDictEqual(ret, {}) - @skipIf(NO_MOCK, NO_MOCK_REASON) - def test_daemonize_if(self): - # pylint: disable=assignment-from-none - with patch('sys.argv', ['salt-call']): - ret = salt.utils.daemonize_if({}) - self.assertEqual(None, ret) - - ret = salt.utils.daemonize_if({'multiprocessing': False}) - self.assertEqual(None, ret) - - with patch('sys.platform', 'win'): - ret = salt.utils.daemonize_if({}) - self.assertEqual(None, ret) - - with patch('salt.utils.daemonize'): - salt.utils.daemonize_if({}) - self.assertTrue(salt.utils.daemonize.called) - # pylint: enable=assignment-from-none - @skipIf(NO_MOCK, NO_MOCK_REASON) def test_gen_jid(self): now = datetime.datetime(2002, 12, 25, 12, 00, 00, 00) From 377d6b6171957b5d7d31b8d1ba0d16332bfce0aa Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 11 Oct 2017 16:28:26 -0600 Subject: [PATCH 572/633] Fix some docs in the win_dacl state module --- salt/states/win_dacl.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/salt/states/win_dacl.py b/salt/states/win_dacl.py index d7db7ad61c..558f0ddd7d 100644 --- a/salt/states/win_dacl.py +++ b/salt/states/win_dacl.py @@ -33,7 +33,7 @@ Ensure an ACL does not exist .. code-block:: yaml - removeAcl: + removeAcl: win_dacl.absent: - name: HKEY_LOCAL_MACHINE\\SOFTWARE\\mykey - objectType: Registry @@ -50,11 +50,11 @@ Ensure an object is inheriting permissions .. code-block:: yaml - eInherit: - win_dacl.enableinheritance: - - name: HKEY_LOCAL_MACHINE\\SOFTWARE\\mykey - - objectType: Registry - - clear_existing_acl: True + eInherit: + win_dacl.enableinheritance: + - name: HKEY_LOCAL_MACHINE\\SOFTWARE\\mykey + - objectType: Registry + - clear_existing_acl: True Ensure an object is not inheriting permissions parameters: @@ -62,13 +62,13 @@ Ensure an object is not inheriting permissions objectType - Registry/File/Directory copy_inherited_acl - True/False - if inheritance is enabled, should the inherited permissions be copied to the ACL when inheritance is disabled - .. code-block:: yaml + .. code-block:: yaml - dInherit: - win_dacl.disableinheritance: - - name: HKEY_LOCAL_MACHINE\\SOFTWARE\\mykey - - objectType: Registry - - copy_inherited_acl: False + dInherit: + win_dacl.disableinheritance: + - name: HKEY_LOCAL_MACHINE\\SOFTWARE\\mykey + - objectType: Registry + - copy_inherited_acl: False ''' @@ -119,7 +119,7 @@ def present(name, objectType, user, permission, acetype, propagation): def absent(name, objectType, user, permission, acetype, propagation): ''' - Ensure a Linux ACL does not exist + Ensure an ACL does not exist ''' ret = {'name': name, 'result': True, From a7108ff00324d2e752f2cb4246a26e30f8f996bd Mon Sep 17 00:00:00 2001 From: Dennis Dmitriev Date: Tue, 10 Oct 2017 19:21:39 +0300 Subject: [PATCH 573/633] Fix issue with mutable 'virt:disk' object Images stored in 'virt:disk::image' , if exist, have higher priority than 'image' argument for 'virt' module. But if different nodes will use different images for the same disk during the run, then the image used for the first node will be re-used for all the rest because it is stored in the mutable object. Use copy.deepcopy to avoid such issue. --- salt/modules/virt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 3354493736..c34a7d7489 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -9,6 +9,7 @@ Work with virtual machines managed by libvirt # Import python libs from __future__ import absolute_import +import copy import os import re import sys @@ -553,7 +554,8 @@ def _disk_profile(profile, hypervisor, **kwargs): else: overlay = {} - disklist = __salt__['config.get']('virt:disk', {}).get(profile, default) + disklist = copy.deepcopy( + __salt__['config.get']('virt:disk', {}).get(profile, default)) for key, val in six.iteritems(overlay): for i, disks in enumerate(disklist): for disk in disks: From 067d4a5dd27471e9fb458c1ecf4fd24bd041057a Mon Sep 17 00:00:00 2001 From: Ken Koch Date: Wed, 11 Oct 2017 21:14:27 -0400 Subject: [PATCH 574/633] change tags to list based in config --- doc/topics/cloud/digitalocean.rst | 5 ++++- salt/cloud/clouds/digitalocean.py | 5 +---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/topics/cloud/digitalocean.rst b/doc/topics/cloud/digitalocean.rst index 129e1fcd3a..1c286a46a4 100644 --- a/doc/topics/cloud/digitalocean.rst +++ b/doc/topics/cloud/digitalocean.rst @@ -54,7 +54,10 @@ Set up an initial profile at ``/etc/salt/cloud.profiles`` or in the ipv6: True create_dns_record: True userdata_file: /etc/salt/cloud.userdata.d/setup - tags:tag1,tag2,tag3 + tags: + - tag1 + - tag2 + - tag3 Locations can be obtained using the ``--list-locations`` option for the ``salt-cloud`` command: diff --git a/salt/cloud/clouds/digitalocean.py b/salt/cloud/clouds/digitalocean.py index 0b9c088465..aee635b3e5 100644 --- a/salt/cloud/clouds/digitalocean.py +++ b/salt/cloud/clouds/digitalocean.py @@ -380,12 +380,9 @@ def create(vm_): raise SaltCloudConfigError("'ipv6' should be a boolean value.") kwargs['ipv6'] = ipv6 - tag_string = config.get_cloud_config_value( + kwargs['tags'] = config.get_cloud_config_value( 'tags', vm_, __opts__, search_global=False, default=False ) - if tag_string: - for tag in tag_string.split(','): - kwargs['tags'].append(tag) userdata_file = config.get_cloud_config_value( 'userdata_file', vm_, __opts__, search_global=False, default=None From eb2bfd047b14c24d92fc127d1036cda49f0e4527 Mon Sep 17 00:00:00 2001 From: Nasenbaer Date: Thu, 24 Nov 2016 15:37:23 +0100 Subject: [PATCH 575/633] Add missing delete_on_termination passthrough. Adapt docs. --- salt/states/boto_lc.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/salt/states/boto_lc.py b/salt/states/boto_lc.py index e21180fd15..cfa7bbbb07 100644 --- a/salt/states/boto_lc.py +++ b/salt/states/boto_lc.py @@ -122,6 +122,7 @@ def present( kernel_id=None, ramdisk_id=None, block_device_mappings=None, + delete_on_termination=None, instance_monitoring=False, spot_price=None, instance_profile_name=None, @@ -185,9 +186,10 @@ def present( Indicates what volume type to use. Valid values are standard, io1, gp2. Default is standard. - delete_on_termination - Indicates whether to delete the volume on instance termination (true) or - not (false). + delete_on_termination + Indicates whether to delete the volume on instance termination (true) or + not (false). Default is "None" which corresponds to not specified. + Amazon has different defaults for root and additional volumes. iops For Provisioned IOPS (SSD) volumes only. The number of I/O operations per @@ -268,6 +270,7 @@ def present( kernel_id=kernel_id, ramdisk_id=ramdisk_id, block_device_mappings=block_device_mappings, + delete_on_termination=delete_on_termination, instance_monitoring=instance_monitoring, spot_price=spot_price, instance_profile_name=instance_profile_name, From 9efd63526af4d6bbe3ab2a2e2d9b403e5b0d21e8 Mon Sep 17 00:00:00 2001 From: Nasenbaer Date: Thu, 5 Oct 2017 22:42:22 +0200 Subject: [PATCH 576/633] Adapted documentation of delete_on_termination parameter --- salt/states/boto_lc.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/salt/states/boto_lc.py b/salt/states/boto_lc.py index cfa7bbbb07..29925501db 100644 --- a/salt/states/boto_lc.py +++ b/salt/states/boto_lc.py @@ -186,10 +186,11 @@ def present( Indicates what volume type to use. Valid values are standard, io1, gp2. Default is standard. - delete_on_termination - Indicates whether to delete the volume on instance termination (true) or - not (false). Default is "None" which corresponds to not specified. - Amazon has different defaults for root and additional volumes. + delete_on_termination + Whether the volume should be explicitly marked for deletion when its instance is + terminated (True), or left around (False). If not provided, or None is explicitly passed, + the default AWS behaviour is used, which is True for ROOT volumes of instances, and + False for all others. iops For Provisioned IOPS (SSD) volumes only. The number of I/O operations per From 255aa94c6499646cfc0938822c7ba6dea006205c Mon Sep 17 00:00:00 2001 From: Nasenbaer Date: Mon, 17 Oct 2016 14:40:47 +0200 Subject: [PATCH 577/633] Activate jid_queue also for SingleMinions to workaround 0mq reconnection issues --- salt/minion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/minion.py b/salt/minion.py index 419d842be8..2cafd9ded6 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -935,7 +935,7 @@ class Minion(MinionBase): # Flag meaning minion has finished initialization including first connect to the master. # True means the Minion is fully functional and ready to handle events. self.ready = False - self.jid_queue = jid_queue + self.jid_queue = jid_queue or [] if io_loop is None: if HAS_ZMQ: From e555b6351bdff218e1edc33cbfdfdc71175db949 Mon Sep 17 00:00:00 2001 From: Ronald van Zantvoort Date: Thu, 12 Oct 2017 11:42:23 +0200 Subject: [PATCH 578/633] doc fixes for salt-ssh roster(s) opts --- conf/master | 6 ++++-- conf/suse/master | 6 ++++-- doc/ref/configuration/master.rst | 34 +++++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/conf/master b/conf/master index e39b0b5e3e..a2c7888989 100644 --- a/conf/master +++ b/conf/master @@ -464,11 +464,13 @@ ##### Salt-SSH Configuration ##### ########################################## +# Define the default salt-ssh roster module to use +#roster: flat -# Pass in an alternative location for the salt-ssh roster file +# Pass in an alternative location for the salt-ssh `flat` roster file #roster_file: /etc/salt/roster -# Define locations for roster files so they can be chosen when using Salt API. +# Define locations for `flat` roster files so they can be chosen when using Salt API. # An administrator can place roster files into these locations. Then when # calling Salt API, parameter 'roster_file' should contain a relative path to # these locations. That is, "roster_file=/foo/roster" will be resolved as diff --git a/conf/suse/master b/conf/suse/master index cdba8f7dac..2cf12fd933 100644 --- a/conf/suse/master +++ b/conf/suse/master @@ -438,11 +438,13 @@ syndic_user: salt ##### Salt-SSH Configuration ##### ########################################## +# Define the default salt-ssh roster module to use +#roster: flat -# Pass in an alternative location for the salt-ssh roster file +# Pass in an alternative location for the salt-ssh `flat` roster file #roster_file: /etc/salt/roster -# Define locations for roster files so they can be chosen when using Salt API. +# Define locations for `flat` roster files so they can be chosen when using Salt API. # An administrator can place roster files into these locations. Then when # calling Salt API, parameter 'roster_file' should contain a relative path to # these locations. That is, "roster_file=/foo/roster" will be resolved as diff --git a/doc/ref/configuration/master.rst b/doc/ref/configuration/master.rst index 8dc4f83ca1..21aac67e05 100644 --- a/doc/ref/configuration/master.rst +++ b/doc/ref/configuration/master.rst @@ -963,6 +963,19 @@ The TCP port for ``mworkers`` to connect to on the master. Salt-SSH Configuration ====================== +.. conf_master:: roster + +``roster`` +--------------- + +Default: ``flat`` + +Define the default salt-ssh roster module to use + +.. code-block:: yaml + + roster: cache + .. conf_master:: roster_file ``roster_file`` @@ -970,12 +983,31 @@ Salt-SSH Configuration Default: ``/etc/salt/roster`` -Pass in an alternative location for the salt-ssh roster file. +Pass in an alternative location for the salt-ssh `flat` roster file. .. code-block:: yaml roster_file: /root/roster +.. conf_master:: roster_file + +``rosters`` +--------------- + +Default: None + +Define locations for `flat` roster files so they can be chosen when using Salt API. +An administrator can place roster files into these locations. +Then when calling Salt API, parameter 'roster_file' should contain a relative path to these locations. +That is, "roster_file=/foo/roster" will be resolved as "/etc/salt/roster.d/foo/roster" etc. +This feature prevents passing insecure custom rosters through the Salt API. + +.. code-block:: yaml + + rosters: + - /etc/salt/roster.d + - /opt/salt/some/more/rosters + .. conf_master:: ssh_passwd ``ssh_passwd`` From 911ee5f36185a321bded58931faa8bea1bf7c570 Mon Sep 17 00:00:00 2001 From: Markus Wyrsch Date: Thu, 12 Oct 2017 13:43:27 +0200 Subject: [PATCH 579/633] Add option to eagerly scrub the disk --- doc/topics/cloud/vmware.rst | 3 +++ salt/cloud/clouds/vmware.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/topics/cloud/vmware.rst b/doc/topics/cloud/vmware.rst index 2b510528a2..e44701564d 100644 --- a/doc/topics/cloud/vmware.rst +++ b/doc/topics/cloud/vmware.rst @@ -283,6 +283,9 @@ Set up an initial profile at ``/etc/salt/cloud.profiles`` or thin_provision Specifies whether the disk should be thin provisioned or not. Default is ``thin_provision: False``. .. versionadded:: 2016.3.0 + eagerly_scrub + Specifies whether the disk should be rewrite with zeros during thick provisioning or not. + Default is ``eagerly_scrub: False``. controller Specify the SCSI controller label to which this disk should be attached. This should be specified only when creating both the specified SCSI diff --git a/salt/cloud/clouds/vmware.py b/salt/cloud/clouds/vmware.py index 98ab762230..9e0f6485b0 100644 --- a/salt/cloud/clouds/vmware.py +++ b/salt/cloud/clouds/vmware.py @@ -273,7 +273,7 @@ def _edit_existing_hard_disk_helper(disk, size_kb=None, size_gb=None, mode=None) return disk_spec -def _add_new_hard_disk_helper(disk_label, size_gb, unit_number, controller_key=1000, thin_provision=False, datastore=None, vm_name=None): +def _add_new_hard_disk_helper(disk_label, size_gb, unit_number, controller_key=1000, thin_provision=False, eagerly_scrub=False, datastore=None, vm_name=None): random_key = randint(-2099, -2000) size_kb = int(size_gb * 1024.0 * 1024.0) @@ -289,6 +289,7 @@ def _add_new_hard_disk_helper(disk_label, size_gb, unit_number, controller_key=1 disk_spec.device.backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo() disk_spec.device.backing.thinProvisioned = thin_provision + disk_spec.device.backing.eagerlyScrub = eagerly_scrub disk_spec.device.backing.diskMode = 'persistent' if datastore: @@ -797,8 +798,9 @@ def _manage_devices(devices, vm=None, container_ref=None, new_vm_name=None): # create the disk size_gb = float(devices['disk'][disk_label]['size']) thin_provision = bool(devices['disk'][disk_label]['thin_provision']) if 'thin_provision' in devices['disk'][disk_label] else False + eagerly_scrub = bool(devices['disk'][disk_label]['eagerly_scrub']) if 'eagerly_scrub' in devices['disk'][disk_label] else False datastore = devices['disk'][disk_label].get('datastore', None) - disk_spec = _add_new_hard_disk_helper(disk_label, size_gb, unit_number, thin_provision=thin_provision, datastore=datastore, vm_name=new_vm_name) + disk_spec = _add_new_hard_disk_helper(disk_label, size_gb, unit_number, thin_provision=thin_provision, eagerly_scrub=eagerly_scrub, datastore=datastore, vm_name=new_vm_name) # when creating both SCSI controller and Hard disk at the same time we need the randomly # assigned (temporary) key of the newly created SCSI controller From bc17787c9d7e98d5f8ccbe30e20160041ed98e5c Mon Sep 17 00:00:00 2001 From: Charlie Root Date: Thu, 12 Oct 2017 14:30:33 +0200 Subject: [PATCH 580/633] modules elbv2 fix --- salt/modules/boto_elbv2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/modules/boto_elbv2.py b/salt/modules/boto_elbv2.py index 7fd1833a8b..09a46ad164 100644 --- a/salt/modules/boto_elbv2.py +++ b/salt/modules/boto_elbv2.py @@ -55,6 +55,8 @@ try: # pylint: enable=unused-import # TODO Version check using salt.utils.versions + import boto3 + import botocore from botocore.exceptions import ClientError logging.getLogger('boto3').setLevel(logging.CRITICAL) HAS_BOTO = True From 8e597fcce935e096ab9963672f0745fa2bde9188 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Thu, 12 Oct 2017 09:33:59 -0400 Subject: [PATCH 581/633] Add Known CherryPy Issue to 2017.7.2 Release Notes --- doc/topics/releases/2017.7.2.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/topics/releases/2017.7.2.rst b/doc/topics/releases/2017.7.2.rst index c9123529cb..e311827bcf 100644 --- a/doc/topics/releases/2017.7.2.rst +++ b/doc/topics/releases/2017.7.2.rst @@ -16,6 +16,11 @@ CVE-2017-14696 Remote Denial of Service with a specially crafted authentication Extended changelog courtesy of Todd Stansell (https://github.com/tjstansell/salt-changelogs): +Known Issues +============ + +On 2017.7.2 when using salt-api and cherrypy version 5.6.0, issue `#43581`_ will occur when starting the salt-api service. We have patched the cherry-py packages for python-cherrypy-5.6.0-2 from repo.saltstack.com. If you are using python-cherrypy-5.6.0-1 please ensure to run `yum install python-cherrypy` to install the new patched version. + *Generated at: 2017-09-26T21:06:19Z* Statistics: @@ -3104,6 +3109,7 @@ Changes: .. _`#475`: https://github.com/saltstack/salt/issues/475 .. _`#480`: https://github.com/saltstack/salt/issues/480 .. _`#495`: https://github.com/saltstack/salt/issues/495 +.. _`#43581`: https://github.com/saltstack/salt/issues/43581 .. _`bp-37424`: https://github.com/saltstack/salt/pull/37424 .. _`bp-39366`: https://github.com/saltstack/salt/pull/39366 .. _`bp-41543`: https://github.com/saltstack/salt/pull/41543 From 1e2e2b6ac5005ed9aa8cadef5a9819704418b3ae Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 10 Oct 2017 17:24:23 -0500 Subject: [PATCH 582/633] Move 5 functions from salt.utils These functions are: - salt.utils.ip_bracket -> salt.utils.zeromq.ip_bracket - salt.utils.gen_mac -> salt.utils.network.gen_mac - salt.utils.mac_str_to_bytes -> salt.utils.network.mac_str_to_bytes - salt.utils.refresh_dns -> salt.utils.network.refresh_dns - salt.utils.dns_check -> salt.utils.network.dns_check --- salt/auth/__init__.py | 3 +- salt/cli/daemons.py | 2 +- salt/client/__init__.py | 8 +- salt/config/__init__.py | 4 +- salt/minion.py | 15 ++- salt/modules/event.py | 3 +- salt/modules/lxc.py | 4 +- salt/modules/network.py | 2 +- salt/modules/virt.py | 3 +- salt/runners/network.py | 4 +- salt/utils/__init__.py | 212 ++++++++++--------------------- salt/utils/http.py | 4 +- salt/utils/network.py | 141 ++++++++++++++++++++ salt/utils/zeromq.py | 10 ++ salt/wheel/__init__.py | 4 +- tests/unit/utils/test_network.py | 15 +++ tests/unit/utils/test_utils.py | 22 ---- tests/unit/utils/test_zeromq.py | 21 +++ 18 files changed, 284 insertions(+), 193 deletions(-) create mode 100644 tests/unit/utils/test_zeromq.py diff --git a/salt/auth/__init__.py b/salt/auth/__init__.py index 81a979dd24..9c9bb9855e 100644 --- a/salt/auth/__init__.py +++ b/salt/auth/__init__.py @@ -34,6 +34,7 @@ import salt.utils.files import salt.utils.minions import salt.utils.versions import salt.utils.user +import salt.utils.zeromq import salt.payload log = logging.getLogger(__name__) @@ -653,7 +654,7 @@ class Resolver(object): def _send_token_request(self, load): if self.opts['transport'] in ('zeromq', 'tcp'): - master_uri = 'tcp://' + salt.utils.ip_bracket(self.opts['interface']) + \ + master_uri = 'tcp://' + salt.utils.zeromq.ip_bracket(self.opts['interface']) + \ ':' + str(self.opts['ret_port']) channel = salt.transport.client.ReqChannel.factory(self.opts, crypt='clear', diff --git a/salt/cli/daemons.py b/salt/cli/daemons.py index 82828c47bd..8e9a784082 100644 --- a/salt/cli/daemons.py +++ b/salt/cli/daemons.py @@ -44,7 +44,7 @@ from salt.utils import migrations import salt.utils.kinds as kinds try: - from salt.utils import ip_bracket + from salt.utils.zeromq import ip_bracket import salt.utils.parsers from salt.utils.verify import check_user, verify_env, verify_socket except ImportError as exc: diff --git a/salt/client/__init__.py b/salt/client/__init__.py index 834ee7577f..728e0bd2c3 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -32,16 +32,16 @@ import salt.cache import salt.payload import salt.transport import salt.loader -import salt.utils # Can be removed once ip_bracket is moved import salt.utils.args import salt.utils.event import salt.utils.files +import salt.utils.jid import salt.utils.minions import salt.utils.platform import salt.utils.user import salt.utils.verify import salt.utils.versions -import salt.utils.jid +import salt.utils.zeromq import salt.syspaths as syspaths from salt.exceptions import ( EauthAuthenticationError, SaltInvocationError, SaltReqTimeoutError, @@ -1791,7 +1791,7 @@ class LocalClient(object): timeout, **kwargs) - master_uri = u'tcp://' + salt.utils.ip_bracket(self.opts[u'interface']) + \ + master_uri = u'tcp://' + salt.utils.zeromq.ip_bracket(self.opts[u'interface']) + \ u':' + str(self.opts[u'ret_port']) channel = salt.transport.Channel.factory(self.opts, crypt=u'clear', @@ -1899,7 +1899,7 @@ class LocalClient(object): timeout, **kwargs) - master_uri = u'tcp://' + salt.utils.ip_bracket(self.opts[u'interface']) + \ + master_uri = u'tcp://' + salt.utils.zeromq.ip_bracket(self.opts[u'interface']) + \ u':' + str(self.opts[u'ret_port']) channel = salt.transport.client.AsyncReqChannel.factory(self.opts, io_loop=io_loop, diff --git a/salt/config/__init__.py b/salt/config/__init__.py index 0113987b2d..a0ed6b77bc 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -24,7 +24,6 @@ from salt.ext.six.moves.urllib.parse import urlparse # pylint: enable=import-error,no-name-in-module # Import salt libs -import salt.utils # Can be removed once is_dictlist, ip_bracket are moved import salt.utils.data import salt.utils.dictupdate import salt.utils.files @@ -36,6 +35,7 @@ import salt.utils.user import salt.utils.validate.path import salt.utils.xdg import salt.utils.yamlloader as yamlloader +import salt.utils.zeromq import salt.syspaths import salt.exceptions from salt.utils.locales import sdecode @@ -3915,7 +3915,7 @@ def client_config(path, env_var='SALT_CLIENT_CONFIG', defaults=None): # Make sure the master_uri is set if 'master_uri' not in opts: opts['master_uri'] = 'tcp://{ip}:{port}'.format( - ip=salt.utils.ip_bracket(opts['interface']), + ip=salt.utils.zeromq.ip_bracket(opts['interface']), port=opts['ret_port'] ) diff --git a/salt/minion.py b/salt/minion.py index d414fa6f9d..d70775e38a 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -157,8 +157,11 @@ def resolve_dns(opts, fallback=True): try: if opts[u'master'] == u'': raise SaltSystemExit - ret[u'master_ip'] = \ - salt.utils.dns_check(opts[u'master'], int(opts[u'master_port']), True, opts[u'ipv6']) + ret[u'master_ip'] = salt.utils.network.dns_check( + opts[u'master'], + int(opts[u'master_port']), + True, + opts[u'ipv6']) except SaltClientError: if opts[u'retry_dns']: while True: @@ -171,9 +174,11 @@ def resolve_dns(opts, fallback=True): print(u'WARNING: {0}'.format(msg)) time.sleep(opts[u'retry_dns']) try: - ret[u'master_ip'] = salt.utils.dns_check( - opts[u'master'], int(opts[u'master_port']), True, opts[u'ipv6'] - ) + ret[u'master_ip'] = salt.utils.network.dns_check( + opts[u'master'], + int(opts[u'master_port']), + True, + opts[u'ipv6']) break except SaltClientError: pass diff --git a/salt/modules/event.py b/salt/modules/event.py index 4c5259ec23..25c9153ab7 100644 --- a/salt/modules/event.py +++ b/salt/modules/event.py @@ -14,6 +14,7 @@ import traceback # Import salt libs import salt.crypt import salt.utils.event +import salt.utils.zeromq import salt.payload import salt.transport from salt.ext import six @@ -60,7 +61,7 @@ def fire_master(data, tag, preload=None): # slower because it has to independently authenticate) if 'master_uri' not in __opts__: __opts__['master_uri'] = 'tcp://{ip}:{port}'.format( - ip=salt.utils.ip_bracket(__opts__['interface']), + ip=salt.utils.zeromq.ip_bracket(__opts__['interface']), port=__opts__.get('ret_port', '4506') # TODO, no fallback ) masters = list() diff --git a/salt/modules/lxc.py b/salt/modules/lxc.py index 3a9b4bc9a2..a72430c348 100644 --- a/salt/modules/lxc.py +++ b/salt/modules/lxc.py @@ -483,7 +483,7 @@ def cloud_init_interface(name, vm_=None, **kwargs): ethx['mac'] = iopts[i] break if 'mac' not in ethx: - ethx['mac'] = salt.utils.gen_mac() + ethx['mac'] = salt.utils.network.gen_mac() # last round checking for unique gateway and such gw = None for ethx in [a for a in nic_opts]: @@ -786,7 +786,7 @@ def _network_conf(conf_tuples=None, **kwargs): 'test': not mac, 'value': mac, 'old': old_if.get('lxc.network.hwaddr'), - 'default': salt.utils.gen_mac()}), + 'default': salt.utils.network.gen_mac()}), ('lxc.network.ipv4', { 'test': not ipv4, 'value': ipv4, diff --git a/salt/modules/network.py b/salt/modules/network.py index a023d6ff7b..2346a72026 100644 --- a/salt/modules/network.py +++ b/salt/modules/network.py @@ -57,7 +57,7 @@ def wol(mac, bcast='255.255.255.255', destport=9): salt '*' network.wol 080027136977 255.255.255.255 7 salt '*' network.wol 08:00:27:13:69:77 255.255.255.255 7 ''' - dest = salt.utils.mac_str_to_bytes(mac) + dest = salt.utils.network.mac_str_to_bytes(mac) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.sendto(b'\xff' * 6 + dest * 16, (bcast, int(destport))) diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 3354493736..e460980634 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -37,6 +37,7 @@ except ImportError: # Import salt libs import salt.utils import salt.utils.files +import salt.utils.network import salt.utils.path import salt.utils.stringutils import salt.utils.templates @@ -664,7 +665,7 @@ def _nic_profile(profile_name, hypervisor, **kwargs): msg = 'Malformed MAC address: {0}'.format(dmac) raise CommandExecutionError(msg) else: - attributes['mac'] = salt.utils.gen_mac() + attributes['mac'] = salt.utils.network.gen_mac() for interface in interfaces: _normalize_net_types(interface) diff --git a/salt/runners/network.py b/salt/runners/network.py index 796cae3e3a..ed81446989 100644 --- a/salt/runners/network.py +++ b/salt/runners/network.py @@ -10,8 +10,8 @@ import logging import socket # Import salt libs -import salt.utils import salt.utils.files +import salt.utils.network log = logging.getLogger(__name__) @@ -54,7 +54,7 @@ def wol(mac, bcast='255.255.255.255', destport=9): salt-run network.wol 080027136977 255.255.255.255 7 salt-run network.wol 08:00:27:13:69:77 255.255.255.255 7 ''' - dest = salt.utils.mac_str_to_bytes(mac) + dest = salt.utils.network.mac_str_to_bytes(mac) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.sendto(b'\xff' * 6 + dest * 16, (bcast, int(destport))) diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index d0ab9e3ee9..85da33b01c 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -24,7 +24,6 @@ import random import re import shlex import shutil -import socket import sys import pstats import time @@ -68,15 +67,6 @@ try: except ImportError: HAS_WIN32API = False -try: - import ctypes - import ctypes.util - libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c")) - res_init = libc.__res_init - HAS_RESINIT = True -except (ImportError, OSError, AttributeError, TypeError): - HAS_RESINIT = False - # Import salt libs from salt.defaults import DEFAULT_TARGET_DELIM import salt.defaults.exitcodes @@ -261,143 +251,6 @@ def list_files(directory): return list(ret) -@jinja_filter('gen_mac') -def gen_mac(prefix='AC:DE:48'): - ''' - Generates a MAC address with the defined OUI prefix. - - Common prefixes: - - - ``00:16:3E`` -- Xen - - ``00:18:51`` -- OpenVZ - - ``00:50:56`` -- VMware (manually generated) - - ``52:54:00`` -- QEMU/KVM - - ``AC:DE:48`` -- PRIVATE - - References: - - - http://standards.ieee.org/develop/regauth/oui/oui.txt - - https://www.wireshark.org/tools/oui-lookup.html - - https://en.wikipedia.org/wiki/MAC_address - ''' - return '{0}:{1:02X}:{2:02X}:{3:02X}'.format(prefix, - random.randint(0, 0xff), - random.randint(0, 0xff), - random.randint(0, 0xff)) - - -@jinja_filter('mac_str_to_bytes') -def mac_str_to_bytes(mac_str): - ''' - Convert a MAC address string into bytes. Works with or without separators: - - b1 = mac_str_to_bytes('08:00:27:13:69:77') - b2 = mac_str_to_bytes('080027136977') - assert b1 == b2 - assert isinstance(b1, bytes) - ''' - if len(mac_str) == 12: - pass - elif len(mac_str) == 17: - sep = mac_str[2] - mac_str = mac_str.replace(sep, '') - else: - raise ValueError('Invalid MAC address') - if six.PY3: - mac_bytes = bytes(int(mac_str[s:s+2], 16) for s in range(0, 12, 2)) - else: - mac_bytes = ''.join(chr(int(mac_str[s:s+2], 16)) for s in range(0, 12, 2)) - return mac_bytes - - -def ip_bracket(addr): - ''' - Convert IP address representation to ZMQ (URL) format. ZMQ expects - brackets around IPv6 literals, since they are used in URLs. - ''' - if addr and ':' in addr and not addr.startswith('['): - return '[{0}]'.format(addr) - return addr - - -def refresh_dns(): - ''' - issue #21397: force glibc to re-read resolv.conf - ''' - if HAS_RESINIT: - res_init() - - -@jinja_filter('dns_check') -def dns_check(addr, port, safe=False, ipv6=None): - ''' - Return the ip resolved by dns, but do not exit on failure, only raise an - exception. Obeys system preference for IPv4/6 address resolution. - Tries to connect to the address before considering it useful. If no address - can be reached, the first one resolved is used as a fallback. - ''' - error = False - lookup = addr - seen_ipv6 = False - try: - refresh_dns() - hostnames = socket.getaddrinfo( - addr, None, socket.AF_UNSPEC, socket.SOCK_STREAM - ) - if not hostnames: - error = True - else: - resolved = False - candidates = [] - for h in hostnames: - # It's an IP address, just return it - if h[4][0] == addr: - resolved = addr - break - - if h[0] == socket.AF_INET and ipv6 is True: - continue - if h[0] == socket.AF_INET6 and ipv6 is False: - continue - - candidate_addr = ip_bracket(h[4][0]) - - if h[0] != socket.AF_INET6 or ipv6 is not None: - candidates.append(candidate_addr) - - try: - s = socket.socket(h[0], socket.SOCK_STREAM) - s.connect((candidate_addr.strip('[]'), port)) - s.close() - - resolved = candidate_addr - break - except socket.error: - pass - if not resolved: - if len(candidates) > 0: - resolved = candidates[0] - else: - error = True - except TypeError: - err = ('Attempt to resolve address \'{0}\' failed. Invalid or unresolveable address').format(lookup) - raise SaltSystemExit(code=42, msg=err) - except socket.error: - error = True - - if error: - err = ('DNS lookup or connection check of \'{0}\' failed.').format(addr) - if safe: - if salt.log.is_console_configured(): - # If logging is not configured it also means that either - # the master or minion instance calling this hasn't even - # started running - log.error(err) - raise SaltClientError() - raise SaltSystemExit(code=42, msg=err) - return resolved - - def required_module_list(docstring=None): ''' Return a list of python modules required by a salt module that aren't @@ -2473,3 +2326,68 @@ def exactly_one(l): 'Salt Oxygen. This warning will be removed in Salt Neon.' ) return salt.utils.data.exactly_one(l) + + +def ip_bracket(addr): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.zeromq + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.ip_bracket\' detected. This function ' + 'has been moved to \'salt.utils.zeromq.ip_bracket\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.zeromq.ip_bracket(addr) + + +def gen_mac(prefix='AC:DE:48'): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.network + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.gen_mac\' detected. This function ' + 'has been moved to \'salt.utils.network.gen_mac\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.network.gen_mac(prefix) + + +def mac_str_to_bytes(mac_str): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.network + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.mac_str_to_bytes\' detected. This function ' + 'has been moved to \'salt.utils.network.mac_str_to_bytes\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.network.mac_str_to_bytes(mac_str) + + +def refresh_dns(): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.network + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.refresh_dns\' detected. This function ' + 'has been moved to \'salt.utils.network.refresh_dns\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.network.refresh_dns() + + +def dns_check(addr, port, safe=False, ipv6=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.network + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.dns_check\' detected. This function ' + 'has been moved to \'salt.utils.network.dns_check\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.network.dns_check(addr, port, safe, ipv6) diff --git a/salt/utils/http.py b/salt/utils/http.py index f0b6f0c690..9002b52f66 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -39,9 +39,9 @@ except ImportError: import salt.config import salt.loader import salt.syspaths -import salt.utils # Can be removed once refresh_dns is moved import salt.utils.args import salt.utils.files +import salt.utils.network import salt.utils.platform import salt.utils.stringutils import salt.version @@ -163,7 +163,7 @@ def query(url, match = re.match(r'https?://((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)($|/)', url) if not match: - salt.utils.refresh_dns() + salt.utils.network.refresh_dns() if backend == 'requests': if HAS_REQUESTS is False: diff --git a/salt/utils/network.py b/salt/utils/network.py index 119aeed25a..98fea8b7e2 100644 --- a/salt/utils/network.py +++ b/salt/utils/network.py @@ -12,6 +12,7 @@ import types import socket import logging import platform +import random import subprocess from string import ascii_letters, digits @@ -31,7 +32,9 @@ import salt.utils.files import salt.utils.path import salt.utils.platform import salt.utils.stringutils +import salt.utils.zeromq from salt._compat import ipaddress +from salt.exceptions import SaltClientError, SaltSystemExit from salt.utils.decorators.jinja import jinja_filter # inet_pton does not exist in Windows, this is a workaround @@ -40,6 +43,14 @@ if salt.utils.platform.is_windows(): log = logging.getLogger(__name__) +try: + import ctypes + import ctypes.util + libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c")) + res_init = libc.__res_init +except (ImportError, OSError, AttributeError, TypeError): + pass + # pylint: disable=C0103 @@ -1648,3 +1659,133 @@ def _aix_remotes_on(port, which_end): continue remotes.add(remote_host) return remotes + + +@jinja_filter('gen_mac') +def gen_mac(prefix='AC:DE:48'): + ''' + Generates a MAC address with the defined OUI prefix. + + Common prefixes: + + - ``00:16:3E`` -- Xen + - ``00:18:51`` -- OpenVZ + - ``00:50:56`` -- VMware (manually generated) + - ``52:54:00`` -- QEMU/KVM + - ``AC:DE:48`` -- PRIVATE + + References: + + - http://standards.ieee.org/develop/regauth/oui/oui.txt + - https://www.wireshark.org/tools/oui-lookup.html + - https://en.wikipedia.org/wiki/MAC_address + ''' + return '{0}:{1:02X}:{2:02X}:{3:02X}'.format(prefix, + random.randint(0, 0xff), + random.randint(0, 0xff), + random.randint(0, 0xff)) + + +@jinja_filter('mac_str_to_bytes') +def mac_str_to_bytes(mac_str): + ''' + Convert a MAC address string into bytes. Works with or without separators: + + b1 = mac_str_to_bytes('08:00:27:13:69:77') + b2 = mac_str_to_bytes('080027136977') + assert b1 == b2 + assert isinstance(b1, bytes) + ''' + if len(mac_str) == 12: + pass + elif len(mac_str) == 17: + sep = mac_str[2] + mac_str = mac_str.replace(sep, '') + else: + raise ValueError('Invalid MAC address') + if six.PY3: + mac_bytes = bytes(int(mac_str[s:s+2], 16) for s in range(0, 12, 2)) + else: + mac_bytes = ''.join(chr(int(mac_str[s:s+2], 16)) for s in range(0, 12, 2)) + return mac_bytes + + +def refresh_dns(): + ''' + issue #21397: force glibc to re-read resolv.conf + ''' + try: + res_init() + except NameError: + # Exception raised loading the library, thus res_init is not defined + pass + + +@jinja_filter('dns_check') +def dns_check(addr, port, safe=False, ipv6=None): + ''' + Return the ip resolved by dns, but do not exit on failure, only raise an + exception. Obeys system preference for IPv4/6 address resolution. + Tries to connect to the address before considering it useful. If no address + can be reached, the first one resolved is used as a fallback. + ''' + error = False + lookup = addr + seen_ipv6 = False + try: + refresh_dns() + hostnames = socket.getaddrinfo( + addr, None, socket.AF_UNSPEC, socket.SOCK_STREAM + ) + if not hostnames: + error = True + else: + resolved = False + candidates = [] + for h in hostnames: + # It's an IP address, just return it + if h[4][0] == addr: + resolved = addr + break + + if h[0] == socket.AF_INET and ipv6 is True: + continue + if h[0] == socket.AF_INET6 and ipv6 is False: + continue + + candidate_addr = salt.utils.zeromq.ip_bracket(h[4][0]) + + if h[0] != socket.AF_INET6 or ipv6 is not None: + candidates.append(candidate_addr) + + try: + s = socket.socket(h[0], socket.SOCK_STREAM) + s.connect((candidate_addr.strip('[]'), port)) + s.close() + + resolved = candidate_addr + break + except socket.error: + pass + if not resolved: + if len(candidates) > 0: + resolved = candidates[0] + else: + error = True + except TypeError: + err = ('Attempt to resolve address \'{0}\' failed. Invalid or unresolveable address').format(lookup) + raise SaltSystemExit(code=42, msg=err) + except socket.error: + error = True + + if error: + err = ('DNS lookup or connection check of \'{0}\' failed.').format(addr) + if safe: + if salt.log.is_console_configured(): + # If logging is not configured it also means that either + # the master or minion instance calling this hasn't even + # started running + log.error(err) + raise SaltClientError() + raise SaltSystemExit(code=42, msg=err) + return resolved diff --git a/salt/utils/zeromq.py b/salt/utils/zeromq.py index 321a026a4b..892b23bbaf 100644 --- a/salt/utils/zeromq.py +++ b/salt/utils/zeromq.py @@ -30,3 +30,13 @@ def check_ipc_path_max_len(uri): uri, ipc_path_max_len ) ) + + +def ip_bracket(addr): + ''' + Convert IP address representation to ZMQ (URL) format. ZMQ expects + brackets around IPv6 literals, since they are used in URLs. + ''' + if addr and ':' in addr and not addr.startswith('['): + return '[{0}]'.format(addr) + return addr diff --git a/salt/wheel/__init__.py b/salt/wheel/__init__.py index ef1b55fbba..af462befb8 100644 --- a/salt/wheel/__init__.py +++ b/salt/wheel/__init__.py @@ -12,8 +12,8 @@ import salt.client.mixins import salt.config import salt.loader import salt.transport -import salt.utils import salt.utils.error +import salt.utils.zeromq class WheelClient(salt.client.mixins.SyncClientMixin, @@ -63,7 +63,7 @@ class WheelClient(salt.client.mixins.SyncClientMixin, interface = self.opts['interface'] if interface == '0.0.0.0': interface = '127.0.0.1' - master_uri = 'tcp://' + salt.utils.ip_bracket(interface) + \ + master_uri = 'tcp://' + salt.utils.zeromq.ip_bracket(interface) + \ ':' + str(self.opts['ret_port']) channel = salt.transport.Channel.factory(self.opts, crypt='clear', diff --git a/tests/unit/utils/test_network.py b/tests/unit/utils/test_network.py index fdb70a4403..0d6a6fd275 100644 --- a/tests/unit/utils/test_network.py +++ b/tests/unit/utils/test_network.py @@ -445,3 +445,18 @@ class NetworkTestCase(TestCase): patch('os.path.exists', MagicMock(return_value=False)), \ patch('salt.utils.network.ip_addrs', MagicMock(return_value=['127.0.0.1', '::1', 'fe00::0', 'fe02::1', '1.2.3.4'])): self.assertEqual(network.generate_minion_id(), '1.2.3.4') + + def test_gen_mac(self): + with patch('random.randint', return_value=1) as random_mock: + self.assertEqual(random_mock.return_value, 1) + ret = network.gen_mac('00:16:3E') + expected_mac = '00:16:3E:01:01:01' + self.assertEqual(ret, expected_mac) + + def test_mac_str_to_bytes(self): + self.assertRaises(ValueError, network.mac_str_to_bytes, '31337') + self.assertRaises(ValueError, network.mac_str_to_bytes, '0001020304056') + self.assertRaises(ValueError, network.mac_str_to_bytes, '00:01:02:03:04:056') + self.assertRaises(ValueError, network.mac_str_to_bytes, 'a0:b0:c0:d0:e0:fg') + self.assertEqual(b'\x10\x08\x06\x04\x02\x00', network.mac_str_to_bytes('100806040200')) + self.assertEqual(b'\xf8\xe7\xd6\xc5\xb4\xa3', network.mac_str_to_bytes('f8e7d6c5b4a3')) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 60445c5b00..cfff130667 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -67,28 +67,6 @@ class UtilsTestCase(TestCase): incorrect_jid_length = 2012 self.assertEqual(salt.utils.jid.jid_to_time(incorrect_jid_length), '') - @skipIf(NO_MOCK, NO_MOCK_REASON) - def test_gen_mac(self): - with patch('random.randint', return_value=1) as random_mock: - self.assertEqual(random_mock.return_value, 1) - ret = salt.utils.gen_mac('00:16:3E') - expected_mac = '00:16:3E:01:01:01' - self.assertEqual(ret, expected_mac) - - def test_mac_str_to_bytes(self): - self.assertRaises(ValueError, salt.utils.mac_str_to_bytes, '31337') - self.assertRaises(ValueError, salt.utils.mac_str_to_bytes, '0001020304056') - self.assertRaises(ValueError, salt.utils.mac_str_to_bytes, '00:01:02:03:04:056') - self.assertRaises(ValueError, salt.utils.mac_str_to_bytes, 'a0:b0:c0:d0:e0:fg') - self.assertEqual(b'\x10\x08\x06\x04\x02\x00', salt.utils.mac_str_to_bytes('100806040200')) - self.assertEqual(b'\xf8\xe7\xd6\xc5\xb4\xa3', salt.utils.mac_str_to_bytes('f8e7d6c5b4a3')) - - def test_ip_bracket(self): - test_ipv4 = '127.0.0.1' - test_ipv6 = '::1' - self.assertEqual(test_ipv4, salt.utils.ip_bracket(test_ipv4)) - self.assertEqual('[{0}]'.format(test_ipv6), salt.utils.ip_bracket(test_ipv6)) - def test_is_jid(self): self.assertTrue(salt.utils.jid.is_jid('20131219110700123489')) # Valid JID self.assertFalse(salt.utils.jid.is_jid(20131219110700123489)) # int diff --git a/tests/unit/utils/test_zeromq.py b/tests/unit/utils/test_zeromq.py new file mode 100644 index 0000000000..2835359f2a --- /dev/null +++ b/tests/unit/utils/test_zeromq.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +''' +Test salt.utils.zeromq +''' + +# Import Python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from tests.support.unit import TestCase + +# Import salt libs +import salt.utils.zeromq + + +class UtilsTestCase(TestCase): + def test_ip_bracket(self): + test_ipv4 = '127.0.0.1' + test_ipv6 = '::1' + self.assertEqual(test_ipv4, salt.utils.zeromq.ip_bracket(test_ipv4)) + self.assertEqual('[{0}]'.format(test_ipv6), salt.utils.zeromq.ip_bracket(test_ipv6)) From bf14e5f57872e79aaeb8a33c5a83f872c637430b Mon Sep 17 00:00:00 2001 From: Seth House Date: Tue, 10 Oct 2017 12:27:53 -0600 Subject: [PATCH 583/633] Also catch cpstats AttributeError for bad CherryPy release ~5.6.0 --- salt/netapi/rest_cherrypy/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/salt/netapi/rest_cherrypy/app.py b/salt/netapi/rest_cherrypy/app.py index 6eb389242e..b6b3b81cd8 100644 --- a/salt/netapi/rest_cherrypy/app.py +++ b/salt/netapi/rest_cherrypy/app.py @@ -474,11 +474,14 @@ logger = logging.getLogger(__name__) import cherrypy try: from cherrypy.lib import cpstats -except ImportError: +except AttributeError: cpstats = None logger.warn('Import of cherrypy.cpstats failed. ' 'Possible upstream bug: ' 'https://github.com/cherrypy/cherrypy/issues/1444') +except ImportError: + cpstats = None + logger.warn('Import of cherrypy.cpstats failed.') import yaml import salt.ext.six as six From 1789ca93f905a56b0ab93a54afb1bcb4b3e9f58c Mon Sep 17 00:00:00 2001 From: Charlie Root Date: Thu, 12 Oct 2017 19:42:35 +0200 Subject: [PATCH 584/633] boto_elbv2 fix --- salt/modules/boto_elbv2.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/salt/modules/boto_elbv2.py b/salt/modules/boto_elbv2.py index 09a46ad164..205ff06a41 100644 --- a/salt/modules/boto_elbv2.py +++ b/salt/modules/boto_elbv2.py @@ -33,6 +33,9 @@ Connection module for Amazon ALB keyid: GKTADJGHEIQSXMKKRBJ08H key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs region: us-east-1 + +:depends: boto3 + ''' # keep lint from choking on _get_conn and _cache_id # pylint: disable=E0602 @@ -52,11 +55,11 @@ from salt.ext import six try: # pylint: disable=unused-import import salt.utils.boto3 + import boto3 + import botocore # pylint: enable=unused-import # TODO Version check using salt.utils.versions - import boto3 - import botocore from botocore.exceptions import ClientError logging.getLogger('boto3').setLevel(logging.CRITICAL) HAS_BOTO = True From 820add55512c504d10a91e728646817a0b0250ec Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 12 Oct 2017 20:30:55 +0200 Subject: [PATCH 585/633] Added version tag --- doc/topics/cloud/vmware.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/topics/cloud/vmware.rst b/doc/topics/cloud/vmware.rst index e44701564d..d502185e88 100644 --- a/doc/topics/cloud/vmware.rst +++ b/doc/topics/cloud/vmware.rst @@ -286,6 +286,7 @@ Set up an initial profile at ``/etc/salt/cloud.profiles`` or eagerly_scrub Specifies whether the disk should be rewrite with zeros during thick provisioning or not. Default is ``eagerly_scrub: False``. + .. versionadded:: Oxygen controller Specify the SCSI controller label to which this disk should be attached. This should be specified only when creating both the specified SCSI From e5b8953fc0126e74573f86b760f0206bf926670a Mon Sep 17 00:00:00 2001 From: SaltyCharles Date: Thu, 12 Oct 2017 11:41:50 -0700 Subject: [PATCH 586/633] fix typo in set_volume_tags documentation --- salt/modules/boto_ec2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/boto_ec2.py b/salt/modules/boto_ec2.py index c235a02a8d..376ec7fc0d 100644 --- a/salt/modules/boto_ec2.py +++ b/salt/modules/boto_ec2.py @@ -1778,7 +1778,7 @@ def set_volumes_tags(tag_maps, authoritative=False, dry_run=False, would have been applied. returns (dict) - A dict dsecribing status and any changes. + A dict describing status and any changes. ''' ret = {'success': True, 'comment': '', 'changes': {}} From 42c0c6e6ef14d7702d743106dfaecf3a0448cf87 Mon Sep 17 00:00:00 2001 From: Vernon Cole Date: Thu, 12 Oct 2017 13:11:12 -0600 Subject: [PATCH 587/633] lint and documentation fixes --- doc/topics/cloud/vagrant.rst | 12 ++++++------ salt/cloud/clouds/vagrant.py | 3 +-- salt/modules/vagrant.py | 14 ++++++-------- salt/states/vagrant.py | 9 +++++---- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/doc/topics/cloud/vagrant.rst b/doc/topics/cloud/vagrant.rst index 8a67dfc818..466544e4b3 100644 --- a/doc/topics/cloud/vagrant.rst +++ b/doc/topics/cloud/vagrant.rst @@ -36,7 +36,7 @@ Salt-cloud will push the commands to install and provision a salt minion on the virtual machine, so you need not (perhaps **should** not) provision salt in your Vagrantfile, in most cases. -If, however, your cloud master cannot open an ssh connection to the child VM, +If, however, your cloud master cannot open an SSH connection to the child VM, you may **need** to let Vagrant provision the VM with Salt, and use some other method (such as passing a pillar dictionary to the VM) to pass the master's IP address to the VM. The VM can then attempt to reach the salt master in the @@ -71,9 +71,9 @@ definitions for `multiple machines`_ then you need a ``machine`` parameter, .. _`multiple machines`: https://www.vagrantup.com/docs/multi-machine/ -Salt-cloud uses ssh to provision the minion. There must be a routable path +Salt-cloud uses SSH to provision the minion. There must be a routable path from the cloud master to the VM. Usually, you will want to use -a bridged network adapter for ssh. The address may not be known until +a bridged network adapter for SSH. The address may not be known until DHCP assigns it. If ``ssh_host`` is not defined, and ``target_network`` is defined, the driver will attempt to read the address from the output of an ``ifconfig`` command. Lacking either setting, @@ -98,7 +98,7 @@ Profile configuration example: vagrant_runas: my-username # the username who defined the Vagrantbox on the host # vagrant_up_timeout: 300 # (seconds) timeout for cmd.run of the "vagrant up" command # vagrant_provider: '' # option for "vagrant up" like: "--provider vmware_fusion" - # ssh_host: None # "None" means try to find the routable ip address from "ifconfig" + # ssh_host: None # "None" means try to find the routable IP address from "ifconfig" # target_network: None # Expected CIDR address of your bridged network # force_minion_config: false # Set "true" to re-purpose an existing VM @@ -233,9 +233,9 @@ Create and use your new Salt minion sudo salt-cloud -p q1 v1 sudo salt v1 network.ip_addrs - [ you get a list of ip addresses, including the bridged one ] + [ you get a list of IP addresses, including the bridged one ] -- logged in to your laptop (or some other computer known to github)... +- logged in to your laptop (or some other computer known to GitHub)... \[NOTE:\] if you are using MacOS, you need to type ``ssh-add -K`` after each boot, unless you use one of the methods in `this gist`_. diff --git a/salt/cloud/clouds/vagrant.py b/salt/cloud/clouds/vagrant.py index 462ec6ed01..08fec40997 100644 --- a/salt/cloud/clouds/vagrant.py +++ b/salt/cloud/clouds/vagrant.py @@ -219,10 +219,9 @@ def create(vm_): ret = local.cmd(host, 'vagrant.init', [name], kwarg={'vm': vm_, 'start': True}) log.info('response ==> %s', ret[host]) - network_mask = config.get_cloud_config_value( + network_mask = config.get_cloud_config_value( 'network_mask', vm_, __opts__, default='') if 'ssh_host' not in vm_: - local = salt.client.LocalClient() ret = local.cmd(host, 'vagrant.get_ssh_config', [name], diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index 357362d204..7947a9b638 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -479,26 +479,24 @@ def pause(name): return ret == 0 -def reboot(name, **kwargs): +def reboot(name, provision=False): ''' Reboot a VM. (vagrant reload) - keyword argument: - - - provision: (False) also re-run the Vagrant provisioning scripts. - CLI Example: .. code-block:: bash salt vagrant.reboot provision=True + + :param name: The salt_id name you will use to control this VM + :param provision: (False) also re-run the Vagrant provisioning scripts. ''' vm_ = get_vm_info(name) machine = vm_['machine'] - prov = kwargs.get('provision', False) - provision = '--provision' if prov else '' + prov = '--provision' if provision else '' - cmd = 'vagrant reload {} {}'.format(machine, provision) + cmd = 'vagrant reload {} {}'.format(machine, prov) ret = __salt__['cmd.retcode'](cmd, runas=vm_.get('runas'), cwd=vm_.get('cwd')) diff --git a/salt/states/vagrant.py b/salt/states/vagrant.py index f3f6b097b0..edeec1f0db 100644 --- a/salt/states/vagrant.py +++ b/salt/states/vagrant.py @@ -76,7 +76,7 @@ def __virtual__(): return False -def _vagrant_call(node, function, section, comment, status_when_done=None,**kwargs): +def _vagrant_call(node, function, section, comment, status_when_done=None, **kwargs): ''' Helper to call the vagrant functions. Wildcards supported. @@ -94,7 +94,7 @@ def _vagrant_call(node, function, section, comment, status_when_done=None,**kwar try: # use shortcut if a single node name if __salt__['vagrant.get_vm_info'](node): targeted_nodes = [node] - except (SaltInvocationError): + except SaltInvocationError: pass if not targeted_nodes: # the shortcut failed, do this the hard way @@ -281,12 +281,13 @@ def initialized(name, **kwargs): return ret + def stopped(name): ''' Stops a VM (or VMs) by shutting it (them) down nicely. (Runs ``vagrant halt``) - + :param name: May be a Salt_id node, or a POSIX-style wildcard string. - + .. code-block:: yaml node_name: From d2e91c33bdc18dba246933cea2fb9c2b945d6232 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Thu, 12 Oct 2017 15:31:48 -0400 Subject: [PATCH 588/633] Add spm shell tests --- tests/integration/shell/test_spm.py | 31 +++++++++++++++++++++++++++++ tests/support/case.py | 10 ++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/integration/shell/test_spm.py diff --git a/tests/integration/shell/test_spm.py b/tests/integration/shell/test_spm.py new file mode 100644 index 0000000000..d4e0ebf54b --- /dev/null +++ b/tests/integration/shell/test_spm.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# Import python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from tests.support.case import ShellCase + + +class SPMTest(ShellCase): + ''' + Test spm script + ''' + + def test_spm_help(self): + ''' + test --help argument for spm + ''' + expected_args = ['--version', '--assume-yes', '--help'] + output = self.run_spm('--help') + for arg in expected_args: + self.assertIn(arg, ''.join(output)) + + def test_spm_bad_arg(self): + ''' + test correct output when bad argument passed + ''' + expected_args = ['--version', '--assume-yes', '--help'] + output = self.run_spm('doesnotexist') + for arg in expected_args: + self.assertIn(arg, ''.join(output)) diff --git a/tests/support/case.py b/tests/support/case.py index 9bce03f5a6..c35396ee5d 100644 --- a/tests/support/case.py +++ b/tests/support/case.py @@ -442,6 +442,16 @@ class ShellCase(ShellTestCase, AdaptedConfigurationTestCaseMixin, ScriptPathMixi catch_stderr=catch_stderr, timeout=timeout) + def run_spm(self, arg_str, with_retcode=False, catch_stderr=False, timeout=60): # pylint: disable=W0221 + ''' + Execute spm + ''' + return self.run_script('spm', + arg_str, + with_retcode=with_retcode, + catch_stderr=catch_stderr, + timeout=timeout) + def run_ssh(self, arg_str, with_retcode=False, catch_stderr=False, timeout=60): # pylint: disable=W0221 ''' Execute salt-ssh From b6b12fe49556f8685d44499f5c3eb004f1040207 Mon Sep 17 00:00:00 2001 From: Sergey Kizunov Date: Thu, 12 Oct 2017 16:08:21 -0500 Subject: [PATCH 589/633] opkg: Fix usage with pkgrepo.managed Currently, when using `pkgrepo.managed`, the following error is reported: ``` TypeError: get_repo() got an unexpected keyword argument 'ppa_auth' ``` Fix this by adding `**kwargs` to `get_repo` so that it will absorb unused kwargs instead of erroring out when given an unknown kwarg. Did the same thing for all other public APIs in this file. Signed-off-by: Sergey Kizunov --- salt/modules/opkg.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/salt/modules/opkg.py b/salt/modules/opkg.py index 33449dabf5..137380ea79 100644 --- a/salt/modules/opkg.py +++ b/salt/modules/opkg.py @@ -129,7 +129,7 @@ def version(*names, **kwargs): return __salt__['pkg_resource.version'](*names, **kwargs) -def refresh_db(): +def refresh_db(**kwargs): # pylint: disable=unused-argument ''' Updates the opkg database to latest packages based upon repositories @@ -456,7 +456,7 @@ def purge(name=None, pkgs=None, **kwargs): # pylint: disable=unused-argument return remove(name=name, pkgs=pkgs) -def upgrade(refresh=True): +def upgrade(refresh=True, **kwargs): # pylint: disable=unused-argument ''' Upgrades all packages via ``opkg upgrade`` @@ -739,7 +739,7 @@ def list_pkgs(versions_as_list=False, **kwargs): return ret -def list_upgrades(refresh=True): +def list_upgrades(refresh=True, **kwargs): # pylint: disable=unused-argument ''' List all available package upgrades. @@ -908,7 +908,7 @@ def info_installed(*names, **kwargs): return ret -def upgrade_available(name): +def upgrade_available(name, **kwargs): # pylint: disable=unused-argument ''' Check whether or not an upgrade is available for a given package @@ -921,7 +921,7 @@ def upgrade_available(name): return latest_version(name) != '' -def version_cmp(pkg1, pkg2, ignore_epoch=False): +def version_cmp(pkg1, pkg2, ignore_epoch=False, **kwargs): # pylint: disable=unused-argument ''' Do a cmp-style comparison on two packages. Return -1 if pkg1 < pkg2, 0 if pkg1 == pkg2, and 1 if pkg1 > pkg2. Return None if there was a problem @@ -969,7 +969,7 @@ def version_cmp(pkg1, pkg2, ignore_epoch=False): return None -def list_repos(): +def list_repos(**kwargs): # pylint: disable=unused-argument ''' Lists all repos on /etc/opkg/*.conf @@ -1006,7 +1006,7 @@ def list_repos(): return repos -def get_repo(alias): +def get_repo(alias, **kwargs): # pylint: disable=unused-argument ''' Display a repo from the /etc/opkg/*.conf @@ -1077,7 +1077,7 @@ def _mod_repo_in_file(alias, repostr, filepath): fhandle.writelines(output) -def del_repo(alias): +def del_repo(alias, **kwargs): # pylint: disable=unused-argument ''' Delete a repo from /etc/opkg/*.conf @@ -1191,7 +1191,7 @@ def mod_repo(alias, **kwargs): refresh_db() -def file_list(*packages): +def file_list(*packages, **kwargs): # pylint: disable=unused-argument ''' List the files that belong to a package. Not specifying any packages will return a list of _every_ file on the system's package database (not @@ -1212,7 +1212,7 @@ def file_list(*packages): return {'errors': output['errors'], 'files': files} -def file_dict(*packages): +def file_dict(*packages, **kwargs): # pylint: disable=unused-argument ''' List the files that belong to a package, grouped by package. Not specifying any packages will return a list of _every_ file on the system's @@ -1254,7 +1254,7 @@ def file_dict(*packages): return {'errors': errors, 'packages': ret} -def owner(*paths): +def owner(*paths, **kwargs): # pylint: disable=unused-argument ''' Return the name of the package that owns the file. Multiple file paths can be passed. Like :mod:`pkg.version Date: Fri, 13 Oct 2017 07:25:57 +0200 Subject: [PATCH 590/633] boto_elbv2 fix --- doc/topics/cloud/digitalocean.rst | 4 + salt/auth/__init__.py | 3 +- salt/cli/daemons.py | 2 +- salt/client/__init__.py | 8 +- salt/client/mixins.py | 4 +- salt/cloud/__init__.py | 7 +- salt/cloud/clouds/digitalocean.py | 7 +- salt/config/__init__.py | 4 +- salt/crypt.py | 7 +- salt/daemons/flo/jobber.py | 3 +- salt/log/setup.py | 5 +- salt/master.py | 13 +- salt/minion.py | 22 +- salt/modules/event.py | 3 +- salt/modules/inspectlib/collector.py | 10 +- salt/modules/lxc.py | 4 +- salt/modules/network.py | 2 +- salt/modules/vault.py | 5 + salt/modules/virt.py | 7 +- salt/runners/network.py | 4 +- salt/scripts.py | 3 +- salt/transport/tcp.py | 3 +- salt/transport/zeromq.py | 5 +- salt/utils/__init__.py | 369 +++++++++------------------ salt/utils/event.py | 5 +- salt/utils/http.py | 4 +- salt/utils/network.py | 141 ++++++++++ salt/utils/parsers.py | 3 +- salt/utils/process.py | 87 ++++++- salt/utils/reactor.py | 2 +- salt/utils/schedule.py | 5 +- salt/utils/vt.py | 6 +- salt/utils/zeromq.py | 10 + salt/wheel/__init__.py | 4 +- tests/integration/__init__.py | 11 +- tests/unit/utils/test_network.py | 15 ++ tests/unit/utils/test_process.py | 36 ++- tests/unit/utils/test_utils.py | 42 +-- tests/unit/utils/test_zeromq.py | 21 ++ 39 files changed, 524 insertions(+), 372 deletions(-) create mode 100644 tests/unit/utils/test_zeromq.py diff --git a/doc/topics/cloud/digitalocean.rst b/doc/topics/cloud/digitalocean.rst index dd7c76d91f..1c286a46a4 100644 --- a/doc/topics/cloud/digitalocean.rst +++ b/doc/topics/cloud/digitalocean.rst @@ -54,6 +54,10 @@ Set up an initial profile at ``/etc/salt/cloud.profiles`` or in the ipv6: True create_dns_record: True userdata_file: /etc/salt/cloud.userdata.d/setup + tags: + - tag1 + - tag2 + - tag3 Locations can be obtained using the ``--list-locations`` option for the ``salt-cloud`` command: diff --git a/salt/auth/__init__.py b/salt/auth/__init__.py index 81a979dd24..9c9bb9855e 100644 --- a/salt/auth/__init__.py +++ b/salt/auth/__init__.py @@ -34,6 +34,7 @@ import salt.utils.files import salt.utils.minions import salt.utils.versions import salt.utils.user +import salt.utils.zeromq import salt.payload log = logging.getLogger(__name__) @@ -653,7 +654,7 @@ class Resolver(object): def _send_token_request(self, load): if self.opts['transport'] in ('zeromq', 'tcp'): - master_uri = 'tcp://' + salt.utils.ip_bracket(self.opts['interface']) + \ + master_uri = 'tcp://' + salt.utils.zeromq.ip_bracket(self.opts['interface']) + \ ':' + str(self.opts['ret_port']) channel = salt.transport.client.ReqChannel.factory(self.opts, crypt='clear', diff --git a/salt/cli/daemons.py b/salt/cli/daemons.py index 82828c47bd..8e9a784082 100644 --- a/salt/cli/daemons.py +++ b/salt/cli/daemons.py @@ -44,7 +44,7 @@ from salt.utils import migrations import salt.utils.kinds as kinds try: - from salt.utils import ip_bracket + from salt.utils.zeromq import ip_bracket import salt.utils.parsers from salt.utils.verify import check_user, verify_env, verify_socket except ImportError as exc: diff --git a/salt/client/__init__.py b/salt/client/__init__.py index 834ee7577f..728e0bd2c3 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -32,16 +32,16 @@ import salt.cache import salt.payload import salt.transport import salt.loader -import salt.utils # Can be removed once ip_bracket is moved import salt.utils.args import salt.utils.event import salt.utils.files +import salt.utils.jid import salt.utils.minions import salt.utils.platform import salt.utils.user import salt.utils.verify import salt.utils.versions -import salt.utils.jid +import salt.utils.zeromq import salt.syspaths as syspaths from salt.exceptions import ( EauthAuthenticationError, SaltInvocationError, SaltReqTimeoutError, @@ -1791,7 +1791,7 @@ class LocalClient(object): timeout, **kwargs) - master_uri = u'tcp://' + salt.utils.ip_bracket(self.opts[u'interface']) + \ + master_uri = u'tcp://' + salt.utils.zeromq.ip_bracket(self.opts[u'interface']) + \ u':' + str(self.opts[u'ret_port']) channel = salt.transport.Channel.factory(self.opts, crypt=u'clear', @@ -1899,7 +1899,7 @@ class LocalClient(object): timeout, **kwargs) - master_uri = u'tcp://' + salt.utils.ip_bracket(self.opts[u'interface']) + \ + master_uri = u'tcp://' + salt.utils.zeromq.ip_bracket(self.opts[u'interface']) + \ u':' + str(self.opts[u'ret_port']) channel = salt.transport.client.AsyncReqChannel.factory(self.opts, io_loop=io_loop, diff --git a/salt/client/mixins.py b/salt/client/mixins.py index c16ec72f53..0df7cf7dd1 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -16,7 +16,7 @@ import copy as pycopy # Import Salt libs import salt.exceptions import salt.minion -import salt.utils # Can be removed once daemonize, format_call are moved +import salt.utils # Can be removed once format_call is moved import salt.utils.args import salt.utils.doc import salt.utils.error @@ -469,7 +469,7 @@ class AsyncClientMixin(object): # Shutdown the multiprocessing before daemonizing salt.log.setup.shutdown_multiprocessing_logging() - salt.utils.daemonize() + salt.utils.process.daemonize() # Reconfigure multiprocessing logging after daemonizing salt.log.setup.setup_multiprocessing_logging() diff --git a/salt/cloud/__init__.py b/salt/cloud/__init__.py index 0372f91f63..04b84a1759 100644 --- a/salt/cloud/__init__.py +++ b/salt/cloud/__init__.py @@ -33,6 +33,7 @@ import salt.utils import salt.utils.args import salt.utils.cloud import salt.utils.context +import salt.utils.crypt import salt.utils.dictupdate import salt.utils.files import salt.syspaths @@ -2300,7 +2301,7 @@ def create_multiprocessing(parallel_data, queue=None): This function will be called from another process when running a map in parallel mode. The result from the create is always a json object. ''' - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() parallel_data['opts']['output'] = 'json' cloud = Cloud(parallel_data['opts']) @@ -2332,7 +2333,7 @@ def destroy_multiprocessing(parallel_data, queue=None): This function will be called from another process when running a map in parallel mode. The result from the destroy is always a json object. ''' - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() parallel_data['opts']['output'] = 'json' clouds = salt.loader.clouds(parallel_data['opts']) @@ -2368,7 +2369,7 @@ def run_parallel_map_providers_query(data, queue=None): This function will be called from another process when building the providers map. ''' - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() cloud = Cloud(data['opts']) try: diff --git a/salt/cloud/clouds/digitalocean.py b/salt/cloud/clouds/digitalocean.py index d5bcb4fb6f..aee635b3e5 100644 --- a/salt/cloud/clouds/digitalocean.py +++ b/salt/cloud/clouds/digitalocean.py @@ -298,7 +298,8 @@ def create(vm_): 'size': get_size(vm_), 'image': get_image(vm_), 'region': get_location(vm_), - 'ssh_keys': [] + 'ssh_keys': [], + 'tags': [] } # backwards compat @@ -379,6 +380,10 @@ def create(vm_): raise SaltCloudConfigError("'ipv6' should be a boolean value.") kwargs['ipv6'] = ipv6 + kwargs['tags'] = config.get_cloud_config_value( + 'tags', vm_, __opts__, search_global=False, default=False + ) + userdata_file = config.get_cloud_config_value( 'userdata_file', vm_, __opts__, search_global=False, default=None ) diff --git a/salt/config/__init__.py b/salt/config/__init__.py index 0113987b2d..a0ed6b77bc 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -24,7 +24,6 @@ from salt.ext.six.moves.urllib.parse import urlparse # pylint: enable=import-error,no-name-in-module # Import salt libs -import salt.utils # Can be removed once is_dictlist, ip_bracket are moved import salt.utils.data import salt.utils.dictupdate import salt.utils.files @@ -36,6 +35,7 @@ import salt.utils.user import salt.utils.validate.path import salt.utils.xdg import salt.utils.yamlloader as yamlloader +import salt.utils.zeromq import salt.syspaths import salt.exceptions from salt.utils.locales import sdecode @@ -3915,7 +3915,7 @@ def client_config(path, env_var='SALT_CLIENT_CONFIG', defaults=None): # Make sure the master_uri is set if 'master_uri' not in opts: opts['master_uri'] = 'tcp://{ip}:{port}'.format( - ip=salt.utils.ip_bracket(opts['interface']), + ip=salt.utils.zeromq.ip_bracket(opts['interface']), port=opts['ret_port'] ) diff --git a/salt/crypt.py b/salt/crypt.py index 3d002a3847..764015e480 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -50,7 +50,8 @@ import salt.defaults.exitcodes import salt.payload import salt.transport.client import salt.transport.frame -import salt.utils # Can be removed when pem_finger, reinit_crypto are moved +import salt.utils # Can be removed when pem_finger is moved +import salt.utils.crypt import salt.utils.decorators import salt.utils.event import salt.utils.files @@ -113,7 +114,7 @@ def gen_keys(keydir, keyname, keysize, user=None, passphrase=None): priv = u'{0}.pem'.format(base) pub = u'{0}.pub'.format(base) - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() gen = RSA.generate(bits=keysize, e=65537) if os.path.isfile(priv): # Between first checking and the generation another process has made @@ -446,7 +447,7 @@ class AsyncAuth(object): self.io_loop = io_loop or tornado.ioloop.IOLoop.current() - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() key = self.__key(self.opts) # TODO: if we already have creds for this key, lets just re-use if key in AsyncAuth.creds_map: diff --git a/salt/daemons/flo/jobber.py b/salt/daemons/flo/jobber.py index da11b7e5c6..1f50b3227e 100644 --- a/salt/daemons/flo/jobber.py +++ b/salt/daemons/flo/jobber.py @@ -24,6 +24,7 @@ import salt.utils.args import salt.utils.data import salt.utils.files import salt.utils.kinds as kinds +import salt.utils.process import salt.utils.stringutils import salt.transport from raet import raeting, nacling @@ -273,7 +274,7 @@ class SaltRaetNixJobber(ioflo.base.deeding.Deed): data = msg['pub'] fn_ = os.path.join(self.proc_dir, data['jid']) self.opts['__ex_id'] = data['jid'] - salt.utils.daemonize_if(self.opts) + salt.utils.process.daemonize_if(self.opts) salt.transport.jobber_stack = stack = self._setup_jobber_stack() # set up return destination from source diff --git a/salt/log/setup.py b/salt/log/setup.py index 73361391ae..9dc1966b30 100644 --- a/salt/log/setup.py +++ b/salt/log/setup.py @@ -38,7 +38,6 @@ GARBAGE = logging.GARBAGE = 1 QUIET = logging.QUIET = 1000 # Import salt libs -import salt.utils # Can be removed once appendproctitle is moved from salt.textformat import TextFormat from salt.log.handlers import (TemporaryLoggingHandler, StreamHandler, @@ -998,7 +997,9 @@ def patch_python_logging_handlers(): def __process_multiprocessing_logging_queue(opts, queue): - salt.utils.appendproctitle('MultiprocessingLoggingQueue') + # Avoid circular import + import salt.utils.process + salt.utils.process.appendproctitle('MultiprocessingLoggingQueue') # Assign UID/GID of user to proc if set from salt.utils.verify import check_user diff --git a/salt/master.py b/salt/master.py index ea7abbfa84..f24f78330c 100644 --- a/salt/master.py +++ b/salt/master.py @@ -47,7 +47,7 @@ import tornado.gen # pylint: disable=F0401 # Import salt libs import salt.crypt -import salt.utils +import salt.utils # Can be removed once get_values_of_matching_keys is moved import salt.client import salt.payload import salt.pillar @@ -65,6 +65,7 @@ import salt.transport.server import salt.log.setup import salt.utils.args import salt.utils.atomicfile +import salt.utils.crypt import salt.utils.event import salt.utils.files import salt.utils.gitfs @@ -222,7 +223,7 @@ class Maintenance(salt.utils.process.SignalHandlingMultiprocessingProcess): This is where any data that needs to be cleanly maintained from the master is maintained. ''' - salt.utils.appendproctitle(u'Maintenance') + salt.utils.process.appendproctitle(u'Maintenance') # init things that need to be done after the process is forked self._post_fork_init() @@ -652,7 +653,7 @@ class Halite(salt.utils.process.SignalHandlingMultiprocessingProcess): ''' Fire up halite! ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) halite.start(self.hopts) @@ -912,13 +913,13 @@ class MWorker(salt.utils.process.SignalHandlingMultiprocessingProcess): ''' Start a Master Worker ''' - salt.utils.appendproctitle(self.name) + salt.utils.process.appendproctitle(self.name) self.clear_funcs = ClearFuncs( self.opts, self.key, ) self.aes_funcs = AESFuncs(self.opts) - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() self.__bind() @@ -2166,7 +2167,7 @@ class FloMWorker(MWorker): ''' Prepare the needed objects and socket for iteration within ioflo ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.crypt.appendproctitle(self.__class__.__name__) self.clear_funcs = salt.master.ClearFuncs( self.opts, self.key, diff --git a/salt/minion.py b/salt/minion.py index bada1dc0f5..d70775e38a 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -98,6 +98,7 @@ import salt.utils.minion import salt.utils.minions import salt.utils.network import salt.utils.platform +import salt.utils.process import salt.utils.schedule import salt.utils.user import salt.utils.zeromq @@ -156,8 +157,11 @@ def resolve_dns(opts, fallback=True): try: if opts[u'master'] == u'': raise SaltSystemExit - ret[u'master_ip'] = \ - salt.utils.dns_check(opts[u'master'], int(opts[u'master_port']), True, opts[u'ipv6']) + ret[u'master_ip'] = salt.utils.network.dns_check( + opts[u'master'], + int(opts[u'master_port']), + True, + opts[u'ipv6']) except SaltClientError: if opts[u'retry_dns']: while True: @@ -170,9 +174,11 @@ def resolve_dns(opts, fallback=True): print(u'WARNING: {0}'.format(msg)) time.sleep(opts[u'retry_dns']) try: - ret[u'master_ip'] = salt.utils.dns_check( - opts[u'master'], int(opts[u'master_port']), True, opts[u'ipv6'] - ) + ret[u'master_ip'] = salt.utils.network.dns_check( + opts[u'master'], + int(opts[u'master_port']), + True, + opts[u'ipv6']) break except SaltClientError: pass @@ -1470,12 +1476,12 @@ class Minion(MinionBase): # Shutdown the multiprocessing before daemonizing salt.log.setup.shutdown_multiprocessing_logging() - salt.utils.daemonize_if(opts) + salt.utils.process.daemonize_if(opts) # Reconfigure multiprocessing logging after daemonizing salt.log.setup.setup_multiprocessing_logging() - salt.utils.appendproctitle(u'{0}._thread_return {1}'.format(cls.__name__, data[u'jid'])) + salt.utils.process.appendproctitle(u'{0}._thread_return {1}'.format(cls.__name__, data[u'jid'])) sdata = {u'pid': os.getpid()} sdata.update(data) @@ -1654,7 +1660,7 @@ class Minion(MinionBase): This method should be used as a threading target, start the actual minion side execution. ''' - salt.utils.appendproctitle(u'{0}._thread_multi_return {1}'.format(cls.__name__, data[u'jid'])) + salt.utils.process.appendproctitle(u'{0}._thread_multi_return {1}'.format(cls.__name__, data[u'jid'])) multifunc_ordered = opts.get(u'multifunc_ordered', False) num_funcs = len(data[u'fun']) if multifunc_ordered: diff --git a/salt/modules/event.py b/salt/modules/event.py index 4c5259ec23..25c9153ab7 100644 --- a/salt/modules/event.py +++ b/salt/modules/event.py @@ -14,6 +14,7 @@ import traceback # Import salt libs import salt.crypt import salt.utils.event +import salt.utils.zeromq import salt.payload import salt.transport from salt.ext import six @@ -60,7 +61,7 @@ def fire_master(data, tag, preload=None): # slower because it has to independently authenticate) if 'master_uri' not in __opts__: __opts__['master_uri'] = 'tcp://{ip}:{port}'.format( - ip=salt.utils.ip_bracket(__opts__['interface']), + ip=salt.utils.zeromq.ip_bracket(__opts__['interface']), port=__opts__.get('ret_port', '4506') # TODO, no fallback ) masters = list() diff --git a/salt/modules/inspectlib/collector.py b/salt/modules/inspectlib/collector.py index d67b2519bb..74951391cb 100644 --- a/salt/modules/inspectlib/collector.py +++ b/salt/modules/inspectlib/collector.py @@ -28,7 +28,7 @@ from salt.modules.inspectlib import kiwiproc from salt.modules.inspectlib.entities import (AllowedDir, IgnoredDir, Package, PayloadFile, PackageCfgFile) -import salt.utils # Can be removed when reinit_crypto is moved +import salt.utils.crypt import salt.utils.files import salt.utils.fsutils import salt.utils.path @@ -505,10 +505,10 @@ if __name__ == '__main__': # Double-fork stuff try: if os.fork() > 0: - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() sys.exit(0) else: - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() except OSError as ex: sys.exit(1) @@ -518,12 +518,12 @@ if __name__ == '__main__': try: pid = os.fork() if pid > 0: - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() with salt.utils.files.fopen(os.path.join(pidfile, EnvLoader.PID_FILE), 'w') as fp_: fp_.write('{0}\n'.format(pid)) sys.exit(0) except OSError as ex: sys.exit(1) - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() main(dbfile, pidfile, mode) diff --git a/salt/modules/lxc.py b/salt/modules/lxc.py index 3a9b4bc9a2..a72430c348 100644 --- a/salt/modules/lxc.py +++ b/salt/modules/lxc.py @@ -483,7 +483,7 @@ def cloud_init_interface(name, vm_=None, **kwargs): ethx['mac'] = iopts[i] break if 'mac' not in ethx: - ethx['mac'] = salt.utils.gen_mac() + ethx['mac'] = salt.utils.network.gen_mac() # last round checking for unique gateway and such gw = None for ethx in [a for a in nic_opts]: @@ -786,7 +786,7 @@ def _network_conf(conf_tuples=None, **kwargs): 'test': not mac, 'value': mac, 'old': old_if.get('lxc.network.hwaddr'), - 'default': salt.utils.gen_mac()}), + 'default': salt.utils.network.gen_mac()}), ('lxc.network.ipv4', { 'test': not ipv4, 'value': ipv4, diff --git a/salt/modules/network.py b/salt/modules/network.py index a023d6ff7b..2346a72026 100644 --- a/salt/modules/network.py +++ b/salt/modules/network.py @@ -57,7 +57,7 @@ def wol(mac, bcast='255.255.255.255', destport=9): salt '*' network.wol 080027136977 255.255.255.255 7 salt '*' network.wol 08:00:27:13:69:77 255.255.255.255 7 ''' - dest = salt.utils.mac_str_to_bytes(mac) + dest = salt.utils.network.mac_str_to_bytes(mac) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.sendto(b'\xff' * 6 + dest * 16, (bcast, int(destport))) diff --git a/salt/modules/vault.py b/salt/modules/vault.py index c5dab85044..ab5e5ba684 100644 --- a/salt/modules/vault.py +++ b/salt/modules/vault.py @@ -6,6 +6,11 @@ Functions to interact with Hashicorp Vault. +:note: If you see the following error, you'll need to upgrade ``requests`` to atleast 2.4.2 +.. code-block:: shell + [salt.pillar][CRITICAL][14337] Pillar render error: Failed to load ext_pillar vault: {'error': "request() got an unexpected keyword argument 'json'"} + + :configuration: The salt-master must be configured to allow peer-runner configuration, as well as configuration for the module. diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 3354493736..3d291a6ebd 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -9,6 +9,7 @@ Work with virtual machines managed by libvirt # Import python libs from __future__ import absolute_import +import copy import os import re import sys @@ -37,6 +38,7 @@ except ImportError: # Import salt libs import salt.utils import salt.utils.files +import salt.utils.network import salt.utils.path import salt.utils.stringutils import salt.utils.templates @@ -553,7 +555,8 @@ def _disk_profile(profile, hypervisor, **kwargs): else: overlay = {} - disklist = __salt__['config.get']('virt:disk', {}).get(profile, default) + disklist = copy.deepcopy( + __salt__['config.get']('virt:disk', {}).get(profile, default)) for key, val in six.iteritems(overlay): for i, disks in enumerate(disklist): for disk in disks: @@ -664,7 +667,7 @@ def _nic_profile(profile_name, hypervisor, **kwargs): msg = 'Malformed MAC address: {0}'.format(dmac) raise CommandExecutionError(msg) else: - attributes['mac'] = salt.utils.gen_mac() + attributes['mac'] = salt.utils.network.gen_mac() for interface in interfaces: _normalize_net_types(interface) diff --git a/salt/runners/network.py b/salt/runners/network.py index 796cae3e3a..ed81446989 100644 --- a/salt/runners/network.py +++ b/salt/runners/network.py @@ -10,8 +10,8 @@ import logging import socket # Import salt libs -import salt.utils import salt.utils.files +import salt.utils.network log = logging.getLogger(__name__) @@ -54,7 +54,7 @@ def wol(mac, bcast='255.255.255.255', destport=9): salt-run network.wol 080027136977 255.255.255.255 7 salt-run network.wol 08:00:27:13:69:77 255.255.255.255 7 ''' - dest = salt.utils.mac_str_to_bytes(mac) + dest = salt.utils.network.mac_str_to_bytes(mac) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.sendto(b'\xff' * 6 + dest * 16, (bcast, int(destport))) diff --git a/salt/scripts.py b/salt/scripts.py index acef4987f9..0e7da6a8ea 100644 --- a/salt/scripts.py +++ b/salt/scripts.py @@ -97,11 +97,12 @@ def minion_process(): Start a minion process ''' import salt.utils.platform + import salt.utils.process import salt.cli.daemons # salt_minion spawns this function in a new process - salt.utils.appendproctitle(u'KeepAlive') + salt.utils.process.appendproctitle(u'KeepAlive') def handle_hup(manager, sig, frame): manager.minion.reload() diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py index dcfd8ef685..ffc6871949 100644 --- a/salt/transport/tcp.py +++ b/salt/transport/tcp.py @@ -23,6 +23,7 @@ import salt.utils import salt.utils.async import salt.utils.event import salt.utils.platform +import salt.utils.process import salt.utils.verify import salt.payload import salt.exceptions @@ -1314,7 +1315,7 @@ class TCPPubServerChannel(salt.transport.server.PubServerChannel): ''' Bind to the interface specified in the configuration file ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) if log_queue is not None: salt.log.setup.set_multiprocessing_logging_queue(log_queue) diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py index 7bb5409baf..57ffebcc63 100644 --- a/salt/transport/zeromq.py +++ b/salt/transport/zeromq.py @@ -21,6 +21,7 @@ import salt.crypt import salt.utils import salt.utils.verify import salt.utils.event +import salt.utils.process import salt.utils.stringutils import salt.payload import salt.transport.client @@ -449,7 +450,7 @@ class ZeroMQReqServerChannel(salt.transport.mixins.auth.AESReqServerMixin, salt. Multiprocessing target for the zmq queue device ''' self.__setup_signals() - salt.utils.appendproctitle('MWorkerQueue') + salt.utils.process.appendproctitle('MWorkerQueue') self.context = zmq.Context(self.opts['worker_threads']) # Prepare the zeromq sockets self.uri = 'tcp://{interface}:{ret_port}'.format(**self.opts) @@ -694,7 +695,7 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel): ''' Bind to the interface specified in the configuration file ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) # Set up the context context = zmq.Context(1) # Prepare minion publish socket diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 9d5b6e4de8..85da33b01c 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -24,7 +24,6 @@ import random import re import shlex import shutil -import socket import sys import pstats import time @@ -50,12 +49,6 @@ try: except ImportError: HAS_CPROFILE = False -try: - import Crypto.Random - HAS_CRYPTO = True -except ImportError: - HAS_CRYPTO = False - try: import timelib HAS_TIMELIB = True @@ -74,21 +67,6 @@ try: except ImportError: HAS_WIN32API = False -try: - import setproctitle - HAS_SETPROCTITLE = True -except ImportError: - HAS_SETPROCTITLE = False - -try: - import ctypes - import ctypes.util - libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c")) - res_init = libc.__res_init - HAS_RESINIT = True -except (ImportError, OSError, AttributeError, TypeError): - HAS_RESINIT = False - # Import salt libs from salt.defaults import DEFAULT_TARGET_DELIM import salt.defaults.exitcodes @@ -173,91 +151,6 @@ def get_master_key(key_user, opts, skip_perm_errors=False): return '' -def reinit_crypto(): - ''' - When a fork arises, pycrypto needs to reinit - From its doc:: - - Caveat: For the random number generator to work correctly, - you must call Random.atfork() in both the parent and - child processes after using os.fork() - - ''' - if HAS_CRYPTO: - Crypto.Random.atfork() - - -def daemonize(redirect_out=True): - ''' - Daemonize a process - ''' - # Late import to avoid circular import. - import salt.utils.files - - try: - pid = os.fork() - if pid > 0: - # exit first parent - reinit_crypto() - sys.exit(salt.defaults.exitcodes.EX_OK) - except OSError as exc: - log.error( - 'fork #1 failed: {0} ({1})'.format(exc.errno, exc.strerror) - ) - sys.exit(salt.defaults.exitcodes.EX_GENERIC) - - # decouple from parent environment - os.chdir('/') - # noinspection PyArgumentList - os.setsid() - os.umask(18) - - # do second fork - try: - pid = os.fork() - if pid > 0: - reinit_crypto() - sys.exit(salt.defaults.exitcodes.EX_OK) - except OSError as exc: - log.error( - 'fork #2 failed: {0} ({1})'.format( - exc.errno, exc.strerror - ) - ) - sys.exit(salt.defaults.exitcodes.EX_GENERIC) - - reinit_crypto() - - # A normal daemonization redirects the process output to /dev/null. - # Unfortunately when a python multiprocess is called the output is - # not cleanly redirected and the parent process dies when the - # multiprocessing process attempts to access stdout or err. - if redirect_out: - with salt.utils.files.fopen('/dev/null', 'r+') as dev_null: - # Redirect python stdin/out/err - # and the os stdin/out/err which can be different - os.dup2(dev_null.fileno(), sys.stdin.fileno()) - os.dup2(dev_null.fileno(), sys.stdout.fileno()) - os.dup2(dev_null.fileno(), sys.stderr.fileno()) - os.dup2(dev_null.fileno(), 0) - os.dup2(dev_null.fileno(), 1) - os.dup2(dev_null.fileno(), 2) - - -def daemonize_if(opts): - ''' - Daemonize a module function process if multiprocessing is True and the - process is not being called by salt-call - ''' - if 'salt-call' in sys.argv[0]: - return - if not opts.get('multiprocessing', True): - return - if sys.platform.startswith('win'): - return - daemonize(False) - - def profile_func(filename=None): ''' Decorator for adding profiling to a nested function in Salt @@ -358,143 +251,6 @@ def list_files(directory): return list(ret) -@jinja_filter('gen_mac') -def gen_mac(prefix='AC:DE:48'): - ''' - Generates a MAC address with the defined OUI prefix. - - Common prefixes: - - - ``00:16:3E`` -- Xen - - ``00:18:51`` -- OpenVZ - - ``00:50:56`` -- VMware (manually generated) - - ``52:54:00`` -- QEMU/KVM - - ``AC:DE:48`` -- PRIVATE - - References: - - - http://standards.ieee.org/develop/regauth/oui/oui.txt - - https://www.wireshark.org/tools/oui-lookup.html - - https://en.wikipedia.org/wiki/MAC_address - ''' - return '{0}:{1:02X}:{2:02X}:{3:02X}'.format(prefix, - random.randint(0, 0xff), - random.randint(0, 0xff), - random.randint(0, 0xff)) - - -@jinja_filter('mac_str_to_bytes') -def mac_str_to_bytes(mac_str): - ''' - Convert a MAC address string into bytes. Works with or without separators: - - b1 = mac_str_to_bytes('08:00:27:13:69:77') - b2 = mac_str_to_bytes('080027136977') - assert b1 == b2 - assert isinstance(b1, bytes) - ''' - if len(mac_str) == 12: - pass - elif len(mac_str) == 17: - sep = mac_str[2] - mac_str = mac_str.replace(sep, '') - else: - raise ValueError('Invalid MAC address') - if six.PY3: - mac_bytes = bytes(int(mac_str[s:s+2], 16) for s in range(0, 12, 2)) - else: - mac_bytes = ''.join(chr(int(mac_str[s:s+2], 16)) for s in range(0, 12, 2)) - return mac_bytes - - -def ip_bracket(addr): - ''' - Convert IP address representation to ZMQ (URL) format. ZMQ expects - brackets around IPv6 literals, since they are used in URLs. - ''' - if addr and ':' in addr and not addr.startswith('['): - return '[{0}]'.format(addr) - return addr - - -def refresh_dns(): - ''' - issue #21397: force glibc to re-read resolv.conf - ''' - if HAS_RESINIT: - res_init() - - -@jinja_filter('dns_check') -def dns_check(addr, port, safe=False, ipv6=None): - ''' - Return the ip resolved by dns, but do not exit on failure, only raise an - exception. Obeys system preference for IPv4/6 address resolution. - Tries to connect to the address before considering it useful. If no address - can be reached, the first one resolved is used as a fallback. - ''' - error = False - lookup = addr - seen_ipv6 = False - try: - refresh_dns() - hostnames = socket.getaddrinfo( - addr, None, socket.AF_UNSPEC, socket.SOCK_STREAM - ) - if not hostnames: - error = True - else: - resolved = False - candidates = [] - for h in hostnames: - # It's an IP address, just return it - if h[4][0] == addr: - resolved = addr - break - - if h[0] == socket.AF_INET and ipv6 is True: - continue - if h[0] == socket.AF_INET6 and ipv6 is False: - continue - - candidate_addr = ip_bracket(h[4][0]) - - if h[0] != socket.AF_INET6 or ipv6 is not None: - candidates.append(candidate_addr) - - try: - s = socket.socket(h[0], socket.SOCK_STREAM) - s.connect((candidate_addr.strip('[]'), port)) - s.close() - - resolved = candidate_addr - break - except socket.error: - pass - if not resolved: - if len(candidates) > 0: - resolved = candidates[0] - else: - error = True - except TypeError: - err = ('Attempt to resolve address \'{0}\' failed. Invalid or unresolveable address').format(lookup) - raise SaltSystemExit(code=42, msg=err) - except socket.error: - error = True - - if error: - err = ('DNS lookup or connection check of \'{0}\' failed.').format(addr) - if safe: - if salt.log.is_console_configured(): - # If logging is not configured it also means that either - # the master or minion instance calling this hasn't even - # started running - log.error(err) - raise SaltClientError() - raise SaltSystemExit(code=42, msg=err) - return resolved - - def required_module_list(docstring=None): ''' Return a list of python modules required by a salt module that aren't @@ -1385,14 +1141,6 @@ def import_json(): continue -def appendproctitle(name): - ''' - Append "name" to the current process title - ''' - if HAS_SETPROCTITLE: - setproctitle.setproctitle(setproctitle.getproctitle() + ' ' + name) - - def human_size_to_bytes(human_size): ''' Convert human-readable units to bytes @@ -1517,6 +1265,58 @@ def fnmatch_multiple(candidates, pattern): # MOVED FUNCTIONS # # These are deprecated and will be removed in Neon. +def appendproctitle(name): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.process + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.appendproctitle\' detected. This function has been ' + 'moved to \'salt.utils.process.appendproctitle\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.process.appendproctitle(name) + + +def daemonize(redirect_out=True): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.process + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.daemonize\' detected. This function has been ' + 'moved to \'salt.utils.process.daemonize\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.process.daemonize(redirect_out) + + +def daemonize_if(opts): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.process + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.daemonize_if\' detected. This function has been ' + 'moved to \'salt.utils.process.daemonize_if\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.process.daemonize_if(opts) + + +def reinit_crypto(): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.crypt + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.reinit_crypto\' detected. This function has been ' + 'moved to \'salt.utils.crypt.reinit_crypto\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.crypt.reinit_crypto() + + def to_bytes(s, encoding=None): # Late import to avoid circular import. import salt.utils.versions @@ -2526,3 +2326,68 @@ def exactly_one(l): 'Salt Oxygen. This warning will be removed in Salt Neon.' ) return salt.utils.data.exactly_one(l) + + +def ip_bracket(addr): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.zeromq + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.ip_bracket\' detected. This function ' + 'has been moved to \'salt.utils.zeromq.ip_bracket\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.zeromq.ip_bracket(addr) + + +def gen_mac(prefix='AC:DE:48'): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.network + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.gen_mac\' detected. This function ' + 'has been moved to \'salt.utils.network.gen_mac\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.network.gen_mac(prefix) + + +def mac_str_to_bytes(mac_str): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.network + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.mac_str_to_bytes\' detected. This function ' + 'has been moved to \'salt.utils.network.mac_str_to_bytes\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.network.mac_str_to_bytes(mac_str) + + +def refresh_dns(): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.network + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.refresh_dns\' detected. This function ' + 'has been moved to \'salt.utils.network.refresh_dns\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.network.refresh_dns() + + +def dns_check(addr, port, safe=False, ipv6=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.network + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.dns_check\' detected. This function ' + 'has been moved to \'salt.utils.network.dns_check\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.network.dns_check(addr, port, safe, ipv6) diff --git a/salt/utils/event.py b/salt/utils/event.py index c7e8b457a3..e47161b631 100644 --- a/salt/utils/event.py +++ b/salt/utils/event.py @@ -72,7 +72,6 @@ import tornado.iostream # Import salt libs import salt.config import salt.payload -import salt.utils import salt.utils.async import salt.utils.cache import salt.utils.dicttrim @@ -1078,7 +1077,7 @@ class EventPublisher(salt.utils.process.SignalHandlingMultiprocessingProcess): ''' Bind the pub and pull sockets for events ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) self.io_loop = tornado.ioloop.IOLoop() with salt.utils.async.current_ioloop(self.io_loop): if self.opts['ipc_mode'] == 'tcp': @@ -1243,7 +1242,7 @@ class EventReturn(salt.utils.process.SignalHandlingMultiprocessingProcess): ''' Spin up the multiprocess event returner ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) self.event = get_event('master', opts=self.opts, listen=True) events = self.event.iter_events(full=True) self.event.fire_event({}, 'salt/event_listen/start') diff --git a/salt/utils/http.py b/salt/utils/http.py index f0b6f0c690..9002b52f66 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -39,9 +39,9 @@ except ImportError: import salt.config import salt.loader import salt.syspaths -import salt.utils # Can be removed once refresh_dns is moved import salt.utils.args import salt.utils.files +import salt.utils.network import salt.utils.platform import salt.utils.stringutils import salt.version @@ -163,7 +163,7 @@ def query(url, match = re.match(r'https?://((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)($|/)', url) if not match: - salt.utils.refresh_dns() + salt.utils.network.refresh_dns() if backend == 'requests': if HAS_REQUESTS is False: diff --git a/salt/utils/network.py b/salt/utils/network.py index 119aeed25a..98fea8b7e2 100644 --- a/salt/utils/network.py +++ b/salt/utils/network.py @@ -12,6 +12,7 @@ import types import socket import logging import platform +import random import subprocess from string import ascii_letters, digits @@ -31,7 +32,9 @@ import salt.utils.files import salt.utils.path import salt.utils.platform import salt.utils.stringutils +import salt.utils.zeromq from salt._compat import ipaddress +from salt.exceptions import SaltClientError, SaltSystemExit from salt.utils.decorators.jinja import jinja_filter # inet_pton does not exist in Windows, this is a workaround @@ -40,6 +43,14 @@ if salt.utils.platform.is_windows(): log = logging.getLogger(__name__) +try: + import ctypes + import ctypes.util + libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c")) + res_init = libc.__res_init +except (ImportError, OSError, AttributeError, TypeError): + pass + # pylint: disable=C0103 @@ -1648,3 +1659,133 @@ def _aix_remotes_on(port, which_end): continue remotes.add(remote_host) return remotes + + +@jinja_filter('gen_mac') +def gen_mac(prefix='AC:DE:48'): + ''' + Generates a MAC address with the defined OUI prefix. + + Common prefixes: + + - ``00:16:3E`` -- Xen + - ``00:18:51`` -- OpenVZ + - ``00:50:56`` -- VMware (manually generated) + - ``52:54:00`` -- QEMU/KVM + - ``AC:DE:48`` -- PRIVATE + + References: + + - http://standards.ieee.org/develop/regauth/oui/oui.txt + - https://www.wireshark.org/tools/oui-lookup.html + - https://en.wikipedia.org/wiki/MAC_address + ''' + return '{0}:{1:02X}:{2:02X}:{3:02X}'.format(prefix, + random.randint(0, 0xff), + random.randint(0, 0xff), + random.randint(0, 0xff)) + + +@jinja_filter('mac_str_to_bytes') +def mac_str_to_bytes(mac_str): + ''' + Convert a MAC address string into bytes. Works with or without separators: + + b1 = mac_str_to_bytes('08:00:27:13:69:77') + b2 = mac_str_to_bytes('080027136977') + assert b1 == b2 + assert isinstance(b1, bytes) + ''' + if len(mac_str) == 12: + pass + elif len(mac_str) == 17: + sep = mac_str[2] + mac_str = mac_str.replace(sep, '') + else: + raise ValueError('Invalid MAC address') + if six.PY3: + mac_bytes = bytes(int(mac_str[s:s+2], 16) for s in range(0, 12, 2)) + else: + mac_bytes = ''.join(chr(int(mac_str[s:s+2], 16)) for s in range(0, 12, 2)) + return mac_bytes + + +def refresh_dns(): + ''' + issue #21397: force glibc to re-read resolv.conf + ''' + try: + res_init() + except NameError: + # Exception raised loading the library, thus res_init is not defined + pass + + +@jinja_filter('dns_check') +def dns_check(addr, port, safe=False, ipv6=None): + ''' + Return the ip resolved by dns, but do not exit on failure, only raise an + exception. Obeys system preference for IPv4/6 address resolution. + Tries to connect to the address before considering it useful. If no address + can be reached, the first one resolved is used as a fallback. + ''' + error = False + lookup = addr + seen_ipv6 = False + try: + refresh_dns() + hostnames = socket.getaddrinfo( + addr, None, socket.AF_UNSPEC, socket.SOCK_STREAM + ) + if not hostnames: + error = True + else: + resolved = False + candidates = [] + for h in hostnames: + # It's an IP address, just return it + if h[4][0] == addr: + resolved = addr + break + + if h[0] == socket.AF_INET and ipv6 is True: + continue + if h[0] == socket.AF_INET6 and ipv6 is False: + continue + + candidate_addr = salt.utils.zeromq.ip_bracket(h[4][0]) + + if h[0] != socket.AF_INET6 or ipv6 is not None: + candidates.append(candidate_addr) + + try: + s = socket.socket(h[0], socket.SOCK_STREAM) + s.connect((candidate_addr.strip('[]'), port)) + s.close() + + resolved = candidate_addr + break + except socket.error: + pass + if not resolved: + if len(candidates) > 0: + resolved = candidates[0] + else: + error = True + except TypeError: + err = ('Attempt to resolve address \'{0}\' failed. Invalid or unresolveable address').format(lookup) + raise SaltSystemExit(code=42, msg=err) + except socket.error: + error = True + + if error: + err = ('DNS lookup or connection check of \'{0}\' failed.').format(addr) + if safe: + if salt.log.is_console_configured(): + # If logging is not configured it also means that either + # the master or minion instance calling this hasn't even + # started running + log.error(err) + raise SaltClientError() + raise SaltSystemExit(code=42, msg=err) + return resolved diff --git a/salt/utils/parsers.py b/salt/utils/parsers.py index 7b2fca742a..6be34f8c0d 100644 --- a/salt/utils/parsers.py +++ b/salt/utils/parsers.py @@ -36,6 +36,7 @@ import salt.utils.files import salt.utils.jid import salt.utils.kinds as kinds import salt.utils.platform +import salt.utils.process import salt.utils.user import salt.utils.xdg from salt.defaults import DEFAULT_TARGET_DELIM @@ -1004,7 +1005,7 @@ class DaemonMixIn(six.with_metaclass(MixInMeta, object)): log.shutdown_multiprocessing_logging_listener(daemonizing=True) # Late import so logging works correctly - salt.utils.daemonize() + salt.utils.process.daemonize() # Setup the multiprocessing log queue listener if enabled self._setup_mp_logging_listener() diff --git a/salt/utils/process.py b/salt/utils/process.py index 39c7e02145..f156145a4c 100644 --- a/salt/utils/process.py +++ b/salt/utils/process.py @@ -20,7 +20,6 @@ import socket # Import salt libs import salt.defaults.exitcodes -import salt.utils # Can be removed once appendproctitle is moved import salt.utils.files import salt.utils.path import salt.utils.platform @@ -43,6 +42,90 @@ try: except ImportError: pass +try: + import setproctitle + HAS_SETPROCTITLE = True +except ImportError: + HAS_SETPROCTITLE = False + + +def appendproctitle(name): + ''' + Append "name" to the current process title + ''' + if HAS_SETPROCTITLE: + setproctitle.setproctitle(setproctitle.getproctitle() + ' ' + name) + + +def daemonize(redirect_out=True): + ''' + Daemonize a process + ''' + # Avoid circular import + import salt.utils.crypt + try: + pid = os.fork() + if pid > 0: + # exit first parent + salt.utils.crypt.reinit_crypto() + sys.exit(salt.defaults.exitcodes.EX_OK) + except OSError as exc: + log.error( + 'fork #1 failed: {0} ({1})'.format(exc.errno, exc.strerror) + ) + sys.exit(salt.defaults.exitcodes.EX_GENERIC) + + # decouple from parent environment + os.chdir('/') + # noinspection PyArgumentList + os.setsid() + os.umask(18) + + # do second fork + try: + pid = os.fork() + if pid > 0: + salt.utils.crypt.reinit_crypto() + sys.exit(salt.defaults.exitcodes.EX_OK) + except OSError as exc: + log.error( + 'fork #2 failed: {0} ({1})'.format( + exc.errno, exc.strerror + ) + ) + sys.exit(salt.defaults.exitcodes.EX_GENERIC) + + salt.utils.crypt.reinit_crypto() + + # A normal daemonization redirects the process output to /dev/null. + # Unfortunately when a python multiprocess is called the output is + # not cleanly redirected and the parent process dies when the + # multiprocessing process attempts to access stdout or err. + if redirect_out: + with salt.utils.files.fopen('/dev/null', 'r+') as dev_null: + # Redirect python stdin/out/err + # and the os stdin/out/err which can be different + os.dup2(dev_null.fileno(), sys.stdin.fileno()) + os.dup2(dev_null.fileno(), sys.stdout.fileno()) + os.dup2(dev_null.fileno(), sys.stderr.fileno()) + os.dup2(dev_null.fileno(), 0) + os.dup2(dev_null.fileno(), 1) + os.dup2(dev_null.fileno(), 2) + + +def daemonize_if(opts): + ''' + Daemonize a module function process if multiprocessing is True and the + process is not being called by salt-call + ''' + if 'salt-call' in sys.argv[0]: + return + if not opts.get('multiprocessing', True): + return + if sys.platform.startswith('win'): + return + daemonize(False) + def systemd_notify_call(action): process = subprocess.Popen(['systemd-notify', action], stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -397,7 +480,7 @@ class ProcessManager(object): Load and start all available api modules ''' log.debug('Process Manager starting!') - salt.utils.appendproctitle(self.name) + appendproctitle(self.name) # make sure to kill the subprocesses if the parent is killed if signal.getsignal(signal.SIGTERM) is signal.SIG_DFL: diff --git a/salt/utils/reactor.py b/salt/utils/reactor.py index 6dfd6d0a39..5bc60b8bd4 100644 --- a/salt/utils/reactor.py +++ b/salt/utils/reactor.py @@ -232,7 +232,7 @@ class Reactor(salt.utils.process.SignalHandlingMultiprocessingProcess, salt.stat ''' Enter into the server loop ''' - salt.utils.appendproctitle(self.__class__.__name__) + salt.utils.process.appendproctitle(self.__class__.__name__) # instantiate some classes inside our new process self.event = salt.utils.event.get_event( diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index d29a3bf314..055c7fc0db 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -337,7 +337,6 @@ import copy # Import Salt libs import salt.config -import salt.utils # Can be removed once appendproctitle and daemonize_if are moved import salt.utils.args import salt.utils.error import salt.utils.event @@ -779,7 +778,7 @@ class Schedule(object): log.warning('schedule: The metadata parameter must be ' 'specified as a dictionary. Ignoring.') - salt.utils.appendproctitle('{0} {1}'.format(self.__class__.__name__, ret['jid'])) + salt.utils.process.appendproctitle('{0} {1}'.format(self.__class__.__name__, ret['jid'])) if not self.standalone: proc_fn = os.path.join( @@ -818,7 +817,7 @@ class Schedule(object): log_setup.setup_multiprocessing_logging() # Don't *BEFORE* to go into try to don't let it triple execute the finally section. - salt.utils.daemonize_if(self.opts) + salt.utils.process.daemonize_if(self.opts) # TODO: Make it readable! Splt to funcs, remove nested try-except-finally sections. try: diff --git a/salt/utils/vt.py b/salt/utils/vt.py index b3ffb6d239..bdb609f493 100644 --- a/salt/utils/vt.py +++ b/salt/utils/vt.py @@ -52,7 +52,7 @@ except ImportError: import resource # Import salt libs -import salt.utils +import salt.utils.crypt import salt.utils.stringutils from salt.ext.six import string_types from salt.log.setup import LOG_LEVELS @@ -493,7 +493,7 @@ class Terminal(object): # Close parent FDs os.close(stdout_parent_fd) os.close(stderr_parent_fd) - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() # ----- Make STDOUT the controlling PTY ---------------------> child_name = os.ttyname(stdout_child_fd) @@ -554,7 +554,7 @@ class Terminal(object): # <---- Duplicate Descriptors -------------------------------- else: # Parent. Close Child PTY's - salt.utils.reinit_crypto() + salt.utils.crypt.reinit_crypto() os.close(stdout_child_fd) os.close(stderr_child_fd) diff --git a/salt/utils/zeromq.py b/salt/utils/zeromq.py index 321a026a4b..892b23bbaf 100644 --- a/salt/utils/zeromq.py +++ b/salt/utils/zeromq.py @@ -30,3 +30,13 @@ def check_ipc_path_max_len(uri): uri, ipc_path_max_len ) ) + + +def ip_bracket(addr): + ''' + Convert IP address representation to ZMQ (URL) format. ZMQ expects + brackets around IPv6 literals, since they are used in URLs. + ''' + if addr and ':' in addr and not addr.startswith('['): + return '[{0}]'.format(addr) + return addr diff --git a/salt/wheel/__init__.py b/salt/wheel/__init__.py index ef1b55fbba..af462befb8 100644 --- a/salt/wheel/__init__.py +++ b/salt/wheel/__init__.py @@ -12,8 +12,8 @@ import salt.client.mixins import salt.config import salt.loader import salt.transport -import salt.utils import salt.utils.error +import salt.utils.zeromq class WheelClient(salt.client.mixins.SyncClientMixin, @@ -63,7 +63,7 @@ class WheelClient(salt.client.mixins.SyncClientMixin, interface = self.opts['interface'] if interface == '0.0.0.0': interface = '127.0.0.1' - master_uri = 'tcp://' + salt.utils.ip_bracket(interface) + \ + master_uri = 'tcp://' + salt.utils.zeromq.ip_bracket(interface) + \ ':' + str(self.opts['ret_port']) channel = salt.transport.Channel.factory(self.opts, crypt='clear', diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 92dcd8fedf..0ede993700 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -49,7 +49,6 @@ import salt.minion import salt.runner import salt.output import salt.version -import salt.utils # Can be removed once appendproctitle is moved import salt.utils.color import salt.utils.files import salt.utils.path @@ -261,7 +260,7 @@ class TestDaemon(object): def start_daemon(self, cls, opts, start_fun): def start(cls, opts, start_fun): - salt.utils.appendproctitle('{0}-{1}'.format(self.__class__.__name__, cls.__name__)) + salt.utils.process.appendproctitle('{0}-{1}'.format(self.__class__.__name__, cls.__name__)) daemon = cls(opts) getattr(daemon, start_fun)() process = multiprocessing.Process(target=start, @@ -1143,7 +1142,7 @@ class TestDaemon(object): ] def wait_for_minion_connections(self, targets, timeout): - salt.utils.appendproctitle('WaitForMinionConnections') + salt.utils.process.appendproctitle('WaitForMinionConnections') sys.stdout.write( ' {LIGHT_BLUE}*{ENDC} Waiting at most {0} for minions({1}) to ' 'connect back\n'.format( @@ -1286,13 +1285,13 @@ class TestDaemon(object): return True def sync_minion_states(self, targets, timeout=None): - salt.utils.appendproctitle('SyncMinionStates') + salt.utils.process.appendproctitle('SyncMinionStates') self.sync_minion_modules_('states', targets, timeout=timeout) def sync_minion_modules(self, targets, timeout=None): - salt.utils.appendproctitle('SyncMinionModules') + salt.utils.process.appendproctitle('SyncMinionModules') self.sync_minion_modules_('modules', targets, timeout=timeout) def sync_minion_grains(self, targets, timeout=None): - salt.utils.appendproctitle('SyncMinionGrains') + salt.utils.process.appendproctitle('SyncMinionGrains') self.sync_minion_modules_('grains', targets, timeout=timeout) diff --git a/tests/unit/utils/test_network.py b/tests/unit/utils/test_network.py index fdb70a4403..0d6a6fd275 100644 --- a/tests/unit/utils/test_network.py +++ b/tests/unit/utils/test_network.py @@ -445,3 +445,18 @@ class NetworkTestCase(TestCase): patch('os.path.exists', MagicMock(return_value=False)), \ patch('salt.utils.network.ip_addrs', MagicMock(return_value=['127.0.0.1', '::1', 'fe00::0', 'fe02::1', '1.2.3.4'])): self.assertEqual(network.generate_minion_id(), '1.2.3.4') + + def test_gen_mac(self): + with patch('random.randint', return_value=1) as random_mock: + self.assertEqual(random_mock.return_value, 1) + ret = network.gen_mac('00:16:3E') + expected_mac = '00:16:3E:01:01:01' + self.assertEqual(ret, expected_mac) + + def test_mac_str_to_bytes(self): + self.assertRaises(ValueError, network.mac_str_to_bytes, '31337') + self.assertRaises(ValueError, network.mac_str_to_bytes, '0001020304056') + self.assertRaises(ValueError, network.mac_str_to_bytes, '00:01:02:03:04:056') + self.assertRaises(ValueError, network.mac_str_to_bytes, 'a0:b0:c0:d0:e0:fg') + self.assertEqual(b'\x10\x08\x06\x04\x02\x00', network.mac_str_to_bytes('100806040200')) + self.assertEqual(b'\xf8\xe7\xd6\xc5\xb4\xa3', network.mac_str_to_bytes('f8e7d6c5b4a3')) diff --git a/tests/unit/utils/test_process.py b/tests/unit/utils/test_process.py index 25be0b7a1b..af17079697 100644 --- a/tests/unit/utils/test_process.py +++ b/tests/unit/utils/test_process.py @@ -10,9 +10,13 @@ import multiprocessing # Import Salt Testing libs from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + patch, + NO_MOCK, + NO_MOCK_REASON +) # Import salt libs -import salt.utils import salt.utils.process # Import 3rd-party libs @@ -27,7 +31,7 @@ class TestProcessManager(TestCase): Make sure that the process is alive 2s later ''' def spin(): - salt.utils.appendproctitle('test_basic') + salt.utils.process.appendproctitle('test_basic') while True: time.sleep(1) @@ -50,7 +54,7 @@ class TestProcessManager(TestCase): def test_kill(self): def spin(): - salt.utils.appendproctitle('test_kill') + salt.utils.process.appendproctitle('test_kill') while True: time.sleep(1) @@ -79,7 +83,7 @@ class TestProcessManager(TestCase): Make sure that the process is alive 2s later ''' def die(): - salt.utils.appendproctitle('test_restarting') + salt.utils.process.appendproctitle('test_restarting') process_manager = salt.utils.process.ProcessManager() process_manager.add_process(die) @@ -101,7 +105,7 @@ class TestProcessManager(TestCase): @skipIf(sys.version_info < (2, 7), 'Needs > Py 2.7 due to bug in stdlib') def test_counter(self): def incr(counter, num): - salt.utils.appendproctitle('test_counter') + salt.utils.process.appendproctitle('test_counter') for _ in range(0, num): counter.value += 1 counter = multiprocessing.Value('i', 0) @@ -162,3 +166,25 @@ class TestThreadPool(TestCase): self.assertEqual(counter.value, 0) # make sure the queue is still full self.assertEqual(pool._job_queue.qsize(), 1) + + +class TestProcess(TestCase): + + @skipIf(NO_MOCK, NO_MOCK_REASON) + def test_daemonize_if(self): + # pylint: disable=assignment-from-none + with patch('sys.argv', ['salt-call']): + ret = salt.utils.process.daemonize_if({}) + self.assertEqual(None, ret) + + ret = salt.utils.process.daemonize_if({'multiprocessing': False}) + self.assertEqual(None, ret) + + with patch('sys.platform', 'win'): + ret = salt.utils.process.daemonize_if({}) + self.assertEqual(None, ret) + + with patch('salt.utils.process.daemonize'): + salt.utils.process.daemonize_if({}) + self.assertTrue(salt.utils.process.daemonize.called) + # pylint: enable=assignment-from-none diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 08620470bc..cfff130667 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -18,6 +18,7 @@ from tests.support.mock import ( import salt.utils import salt.utils.data import salt.utils.jid +import salt.utils.process import salt.utils.yamlencoding import salt.utils.zeromq from salt.exceptions import SaltSystemExit, CommandNotFoundError @@ -66,28 +67,6 @@ class UtilsTestCase(TestCase): incorrect_jid_length = 2012 self.assertEqual(salt.utils.jid.jid_to_time(incorrect_jid_length), '') - @skipIf(NO_MOCK, NO_MOCK_REASON) - def test_gen_mac(self): - with patch('random.randint', return_value=1) as random_mock: - self.assertEqual(random_mock.return_value, 1) - ret = salt.utils.gen_mac('00:16:3E') - expected_mac = '00:16:3E:01:01:01' - self.assertEqual(ret, expected_mac) - - def test_mac_str_to_bytes(self): - self.assertRaises(ValueError, salt.utils.mac_str_to_bytes, '31337') - self.assertRaises(ValueError, salt.utils.mac_str_to_bytes, '0001020304056') - self.assertRaises(ValueError, salt.utils.mac_str_to_bytes, '00:01:02:03:04:056') - self.assertRaises(ValueError, salt.utils.mac_str_to_bytes, 'a0:b0:c0:d0:e0:fg') - self.assertEqual(b'\x10\x08\x06\x04\x02\x00', salt.utils.mac_str_to_bytes('100806040200')) - self.assertEqual(b'\xf8\xe7\xd6\xc5\xb4\xa3', salt.utils.mac_str_to_bytes('f8e7d6c5b4a3')) - - def test_ip_bracket(self): - test_ipv4 = '127.0.0.1' - test_ipv6 = '::1' - self.assertEqual(test_ipv4, salt.utils.ip_bracket(test_ipv4)) - self.assertEqual('[{0}]'.format(test_ipv6), salt.utils.ip_bracket(test_ipv6)) - def test_is_jid(self): self.assertTrue(salt.utils.jid.is_jid('20131219110700123489')) # Valid JID self.assertFalse(salt.utils.jid.is_jid(20131219110700123489)) # int @@ -432,25 +411,6 @@ class UtilsTestCase(TestCase): ret = salt.utils.data.repack_dictlist(LOREM_IPSUM) self.assertDictEqual(ret, {}) - @skipIf(NO_MOCK, NO_MOCK_REASON) - def test_daemonize_if(self): - # pylint: disable=assignment-from-none - with patch('sys.argv', ['salt-call']): - ret = salt.utils.daemonize_if({}) - self.assertEqual(None, ret) - - ret = salt.utils.daemonize_if({'multiprocessing': False}) - self.assertEqual(None, ret) - - with patch('sys.platform', 'win'): - ret = salt.utils.daemonize_if({}) - self.assertEqual(None, ret) - - with patch('salt.utils.daemonize'): - salt.utils.daemonize_if({}) - self.assertTrue(salt.utils.daemonize.called) - # pylint: enable=assignment-from-none - @skipIf(NO_MOCK, NO_MOCK_REASON) def test_gen_jid(self): now = datetime.datetime(2002, 12, 25, 12, 00, 00, 00) diff --git a/tests/unit/utils/test_zeromq.py b/tests/unit/utils/test_zeromq.py new file mode 100644 index 0000000000..2835359f2a --- /dev/null +++ b/tests/unit/utils/test_zeromq.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +''' +Test salt.utils.zeromq +''' + +# Import Python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from tests.support.unit import TestCase + +# Import salt libs +import salt.utils.zeromq + + +class UtilsTestCase(TestCase): + def test_ip_bracket(self): + test_ipv4 = '127.0.0.1' + test_ipv6 = '::1' + self.assertEqual(test_ipv4, salt.utils.zeromq.ip_bracket(test_ipv4)) + self.assertEqual('[{0}]'.format(test_ipv6), salt.utils.zeromq.ip_bracket(test_ipv6)) From 238941cd0248547a9c2142e486aa7146a6b2acd7 Mon Sep 17 00:00:00 2001 From: Senthilkumar Eswaran Date: Thu, 12 Oct 2017 22:28:25 -0700 Subject: [PATCH 591/633] Fixing a typo in the troubleshooting document --- doc/man/salt.7 | 2 +- doc/topics/troubleshooting/master.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/man/salt.7 b/doc/man/salt.7 index 86c463b771..3d8331d330 100644 --- a/doc/man/salt.7 +++ b/doc/man/salt.7 @@ -29818,7 +29818,7 @@ If the master seems to be unresponsive, a SIGUSR1 can be passed to the salt\-master threads to display what piece of code is executing. This debug information can be invaluable in tracking down bugs. .sp -To pass a SIGUSR1 to the master, first make sure the minion is running in the +To pass a SIGUSR1 to the master, first make sure the master is running in the foreground. Stop the service if it is running as a daemon, and start it in the foreground like so: .INDENT 0.0 diff --git a/doc/topics/troubleshooting/master.rst b/doc/topics/troubleshooting/master.rst index 5d3ca74af7..2b3b7348e4 100644 --- a/doc/topics/troubleshooting/master.rst +++ b/doc/topics/troubleshooting/master.rst @@ -141,7 +141,7 @@ If the master seems to be unresponsive, a SIGUSR1 can be passed to the salt-master threads to display what piece of code is executing. This debug information can be invaluable in tracking down bugs. -To pass a SIGUSR1 to the master, first make sure the minion is running in the +To pass a SIGUSR1 to the master, first make sure the master is running in the foreground. Stop the service if it is running as a daemon, and start it in the foreground like so: From 344923e231c777d1cfc0ad25e77b59d4ef20fed1 Mon Sep 17 00:00:00 2001 From: Vasili Syrakis Date: Fri, 13 Oct 2017 22:54:58 +1100 Subject: [PATCH 592/633] Catch on empty Virtualbox network addr #43427 --- salt/utils/virtualbox.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/salt/utils/virtualbox.py b/salt/utils/virtualbox.py index c055a6ff8a..6aa108f57d 100644 --- a/salt/utils/virtualbox.py +++ b/salt/utils/virtualbox.py @@ -281,7 +281,10 @@ def vb_get_network_addresses(machine_name=None, machine=None): # We can't trust virtualbox to give us up to date guest properties if the machine isn't running # For some reason it may give us outdated (cached?) values if machine.state == _virtualboxManager.constants.MachineState_Running: - total_slots = int(machine.getGuestPropertyValue('/VirtualBox/GuestInfo/Net/Count')) + try: + total_slots = int(machine.getGuestPropertyValue('/VirtualBox/GuestInfo/Net/Count')) + except ValueError: + total_slots = 0 for i in range(total_slots): try: address = machine.getGuestPropertyValue('/VirtualBox/GuestInfo/Net/{0}/V4/IP'.format(i)) From c6b655b6e9d6a17efea89ff3c39ec839ddf0b9a9 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 12 Oct 2017 13:17:24 -0500 Subject: [PATCH 593/633] Move 14 more functions from salt.utils These functions are: salt.utils.test_mode -> salt.utils.args.test_mode salt.utils.split_input -> salt.utils.args.split_input alt.utils.pem_finger -> salt.utils.crypt.pem_finger salt.utils.is_bin_file -> salt.utils.files.is_binary salt.utils.list_files -> salt.utils.files.list_files salt.utils.safe_walk -> salt.utils.files.safe_walk salt.utils.st_mode_to_octal -> salt.utils.files.st_mode_to_octal salt.utils.normalize_mode -> salt.utils.files.normalize_mode salt.utils.human_size_to_bytes -> salt.utils.files.human_size_to_bytes salt.utils.get_hash -> salt.utils.hashutils.get_hash salt.utils.is_list -> salt.utils.data.is_list salt.utils.is_iter -> salt.utils.data.is_iter salt.utils.isorted -> salt.utils.data.sorted_ignorecase salt.utils.is_true -> salt.utils.data.is_true --- salt/client/ssh/__init__.py | 3 +- salt/client/ssh/ssh_py_shim.py | 2 +- salt/client/ssh/wrapper/config.py | 4 +- salt/client/ssh/wrapper/grains.py | 5 +- salt/client/ssh/wrapper/state.py | 23 +- salt/cloud/__init__.py | 4 +- salt/cloud/clouds/opennebula.py | 14 +- salt/crypt.py | 11 +- salt/fileclient.py | 11 +- salt/fileserver/azurefs.py | 7 +- salt/fileserver/hgfs.py | 5 +- salt/fileserver/minionfs.py | 5 +- salt/fileserver/roots.py | 6 +- salt/fileserver/s3fs.py | 7 +- salt/fileserver/svnfs.py | 5 +- salt/key.py | 34 +- salt/modules/apk.py | 13 +- salt/modules/aptpkg.py | 26 +- salt/modules/chocolatey.py | 16 +- salt/modules/cmdmod.py | 6 +- salt/modules/config.py | 5 +- salt/modules/cp.py | 2 +- salt/modules/dockermod.py | 3 +- salt/modules/ebuild.py | 16 +- salt/modules/file.py | 35 +- salt/modules/freebsdpkg.py | 12 +- salt/modules/grains.py | 5 +- salt/modules/hg.py | 4 +- salt/modules/key.py | 6 +- salt/modules/lxc.py | 3 +- salt/modules/mac_brew.py | 10 +- salt/modules/mac_ports.py | 14 +- salt/modules/mac_softwareupdate.py | 6 +- salt/modules/minion.py | 4 +- salt/modules/mount.py | 10 +- salt/modules/mysql.py | 20 +- salt/modules/openbsdpkg.py | 5 +- salt/modules/opkg.py | 13 +- salt/modules/pacman.py | 14 +- salt/modules/pecl.py | 4 +- salt/modules/pkg_resource.py | 3 +- salt/modules/pkgin.py | 8 +- salt/modules/pkgng.py | 50 +-- salt/modules/pkgutil.py | 12 +- salt/modules/pw_group.py | 4 +- salt/modules/pw_user.py | 8 +- salt/modules/saltutil.py | 6 +- salt/modules/shadow.py | 4 +- salt/modules/solaris_group.py | 4 +- salt/modules/solaris_user.py | 6 +- salt/modules/solarisips.py | 8 +- salt/modules/solarispkg.py | 8 +- salt/modules/state.py | 8 +- salt/modules/timezone.py | 6 +- salt/modules/win_pkg.py | 42 +- salt/modules/xbpspkg.py | 9 +- salt/modules/yumpkg.py | 16 +- salt/modules/zypper.py | 6 +- salt/pillar/s3.py | 4 +- salt/runners/cache.py | 4 +- salt/runners/jobs.py | 6 +- salt/states/aptpkg.py | 4 +- salt/states/archive.py | 4 +- salt/states/file.py | 37 +- salt/states/mysql_user.py | 10 +- salt/states/pkgrepo.py | 20 +- salt/states/service.py | 6 +- salt/states/win_wua.py | 18 +- salt/utils/__init__.py | 490 +++++++++--------------- salt/utils/args.py | 30 ++ salt/utils/cloud.py | 4 +- salt/utils/crypt.py | 29 ++ salt/utils/data.py | 75 ++++ salt/utils/extmods.py | 5 +- salt/utils/files.py | 137 ++++++- salt/utils/find.py | 4 +- salt/utils/gitfs.py | 5 +- salt/utils/hashutils.py | 24 ++ salt/utils/minions.py | 10 +- salt/utils/pkg/__init__.py | 4 +- salt/utils/s3.py | 4 +- salt/utils/templates.py | 2 +- salt/utils/thin.py | 5 +- salt/utils/win_update.py | 30 +- salt/wheel/file_roots.py | 2 +- salt/wheel/key.py | 4 +- salt/wheel/pillar_roots.py | 2 +- tests/integration/states/test_file.py | 3 +- tests/unit/modules/test_file.py | 4 +- tests/unit/modules/test_key.py | 6 +- tests/unit/modules/test_pkg_resource.py | 4 +- tests/unit/modules/test_state.py | 4 +- tests/unit/states/test_file.py | 2 +- tests/unit/states/test_mysql_user.py | 6 +- tests/unit/utils/test_args.py | 5 + tests/unit/utils/test_data.py | 19 + tests/unit/utils/test_hashutils.py | 20 + tests/unit/utils/test_utils.py | 13 - 98 files changed, 948 insertions(+), 723 deletions(-) create mode 100644 tests/unit/utils/test_data.py create mode 100644 tests/unit/utils/test_hashutils.py diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py index cd3e53f0ea..96f20ba023 100644 --- a/salt/client/ssh/__init__.py +++ b/salt/client/ssh/__init__.py @@ -41,6 +41,7 @@ import salt.utils.args import salt.utils.atomicfile import salt.utils.event import salt.utils.files +import salt.utils.hashutils import salt.utils.network import salt.utils.path import salt.utils.stringutils @@ -1507,7 +1508,7 @@ def mod_data(fsclient): if not os.path.isfile(mod_path): continue mods_data[os.path.basename(fn_)] = mod_path - chunk = salt.utils.get_hash(mod_path) + chunk = salt.utils.hashutils.get_hash(mod_path) ver_base += chunk if mods_data: if ref in ret: diff --git a/salt/client/ssh/ssh_py_shim.py b/salt/client/ssh/ssh_py_shim.py index ea2d78f3c4..bff348ab43 100644 --- a/salt/client/ssh/ssh_py_shim.py +++ b/salt/client/ssh/ssh_py_shim.py @@ -137,7 +137,7 @@ def need_deployment(): sys.exit(EX_THIN_DEPLOY) -# Adapted from salt.utils.get_hash() +# Adapted from salt.utils.hashutils.get_hash() def get_hash(path, form=u'sha1', chunk_size=4096): ''' Generate a hash digest string for a file. diff --git a/salt/client/ssh/wrapper/config.py b/salt/client/ssh/wrapper/config.py index cf97316928..fd02eb7b83 100644 --- a/salt/client/ssh/wrapper/config.py +++ b/salt/client/ssh/wrapper/config.py @@ -9,8 +9,8 @@ import re import os # Import salt libs -import salt.utils # Can be removed once normalize_mode is moved import salt.utils.data +import salt.utils.files import salt.syspaths as syspaths # Import 3rd-party libs @@ -82,7 +82,7 @@ def manage_mode(mode): # config.manage_mode should no longer be invoked from the __salt__ dunder # in Salt code, this function is only being left here for backwards # compatibility. - return salt.utils.normalize_mode(mode) + return salt.utils.files.normalize_mode(mode) def valid_fileproto(uri): diff --git a/salt/client/ssh/wrapper/grains.py b/salt/client/ssh/wrapper/grains.py index 1d610da52d..599f7fc13f 100644 --- a/salt/client/ssh/wrapper/grains.py +++ b/salt/client/ssh/wrapper/grains.py @@ -11,7 +11,6 @@ import math import json # Import salt libs -import salt.utils # Can be removed once is_true is moved import salt.utils.data import salt.utils.dictupdate from salt.defaults import DEFAULT_TARGET_DELIM @@ -122,7 +121,7 @@ def items(sanitize=False): salt '*' grains.items sanitize=True ''' - if salt.utils.is_true(sanitize): + if salt.utils.data.is_true(sanitize): out = dict(__grains__) for key, func in six.iteritems(_SANITIZERS): if key in out: @@ -155,7 +154,7 @@ def item(*args, **kwargs): ret[arg] = __grains__[arg] except KeyError: pass - if salt.utils.is_true(kwargs.get(u'sanitize')): + if salt.utils.data.is_true(kwargs.get(u'sanitize')): for arg, func in six.iteritems(_SANITIZERS): if arg in ret: ret[arg] = func(ret[arg]) diff --git a/salt/client/ssh/wrapper/state.py b/salt/client/ssh/wrapper/state.py index bc4c610a28..cac5a5cef2 100644 --- a/salt/client/ssh/wrapper/state.py +++ b/salt/client/ssh/wrapper/state.py @@ -13,8 +13,9 @@ import logging # Import salt libs import salt.client.ssh.shell import salt.client.ssh.state -import salt.utils # Can be removed once get_hash, test_mode are moved +import salt.utils.args import salt.utils.data +import salt.utils.hashutils import salt.utils.thin import salt.roster import salt.state @@ -101,7 +102,7 @@ def sls(mods, saltenv=u'base', test=None, exclude=None, **kwargs): __pillar__, st_kwargs[u'id_'], roster_grains) - trans_tar_sum = salt.utils.get_hash(trans_tar, __opts__[u'hash_type']) + trans_tar_sum = salt.utils.hashutils.get_hash(trans_tar, __opts__[u'hash_type']) cmd = u'state.pkg {0}/salt_state.tgz test={1} pkg_sum={2} hash_type={3}'.format( __opts__[u'thin_dir'], test, @@ -178,7 +179,7 @@ def low(data, **kwargs): __pillar__, st_kwargs[u'id_'], roster_grains) - trans_tar_sum = salt.utils.get_hash(trans_tar, __opts__[u'hash_type']) + trans_tar_sum = salt.utils.hashutils.get_hash(trans_tar, __opts__[u'hash_type']) cmd = u'state.pkg {0}/salt_state.tgz pkg_sum={1} hash_type={2}'.format( __opts__[u'thin_dir'], trans_tar_sum, @@ -251,7 +252,7 @@ def high(data, **kwargs): __pillar__, st_kwargs[u'id_'], roster_grains) - trans_tar_sum = salt.utils.get_hash(trans_tar, __opts__[u'hash_type']) + trans_tar_sum = salt.utils.hashutils.get_hash(trans_tar, __opts__[u'hash_type']) cmd = u'state.pkg {0}/salt_state.tgz pkg_sum={1} hash_type={2}'.format( __opts__[u'thin_dir'], trans_tar_sum, @@ -354,7 +355,7 @@ def highstate(test=None, **kwargs): __pillar__, st_kwargs[u'id_'], roster_grains) - trans_tar_sum = salt.utils.get_hash(trans_tar, __opts__[u'hash_type']) + trans_tar_sum = salt.utils.hashutils.get_hash(trans_tar, __opts__[u'hash_type']) cmd = u'state.pkg {0}/salt_state.tgz test={1} pkg_sum={2} hash_type={3}'.format( __opts__[u'thin_dir'], test, @@ -403,7 +404,7 @@ def top(topfn, test=None, **kwargs): __pillar__.update(kwargs.get(u'pillar', {})) st_kwargs = __salt__.kwargs __opts__[u'grains'] = __grains__ - if salt.utils.test_mode(test=test, **kwargs): + if salt.utils.args.test_mode(test=test, **kwargs): __opts__[u'test'] = True else: __opts__[u'test'] = __opts__.get(u'test', None) @@ -434,7 +435,7 @@ def top(topfn, test=None, **kwargs): __pillar__, st_kwargs[u'id_'], roster_grains) - trans_tar_sum = salt.utils.get_hash(trans_tar, __opts__[u'hash_type']) + trans_tar_sum = salt.utils.hashutils.get_hash(trans_tar, __opts__[u'hash_type']) cmd = u'state.pkg {0}/salt_state.tgz test={1} pkg_sum={2} hash_type={3}'.format( __opts__[u'thin_dir'], test, @@ -520,7 +521,7 @@ def show_sls(mods, saltenv=u'base', test=None, **kwargs): __pillar__.update(kwargs.get(u'pillar', {})) __opts__[u'grains'] = __grains__ opts = copy.copy(__opts__) - if salt.utils.test_mode(test=test, **kwargs): + if salt.utils.args.test_mode(test=test, **kwargs): opts[u'test'] = True else: opts[u'test'] = __opts__.get(u'test', None) @@ -563,7 +564,7 @@ def show_low_sls(mods, saltenv=u'base', test=None, **kwargs): __opts__[u'grains'] = __grains__ opts = copy.copy(__opts__) - if salt.utils.test_mode(test=test, **kwargs): + if salt.utils.args.test_mode(test=test, **kwargs): opts[u'test'] = True else: opts[u'test'] = __opts__.get(u'test', None) @@ -652,7 +653,7 @@ def single(fun, name, test=None, **kwargs): opts = copy.deepcopy(__opts__) # Set test mode - if salt.utils.test_mode(test=test, **kwargs): + if salt.utils.args.test_mode(test=test, **kwargs): opts[u'test'] = True else: opts[u'test'] = __opts__.get(u'test', None) @@ -696,7 +697,7 @@ def single(fun, name, test=None, **kwargs): roster_grains) # Create a hash so we can verify the tar on the target system - trans_tar_sum = salt.utils.get_hash(trans_tar, __opts__[u'hash_type']) + trans_tar_sum = salt.utils.hashutils.get_hash(trans_tar, __opts__[u'hash_type']) # We use state.pkg to execute the "state package" cmd = u'state.pkg {0}/salt_state.tgz test={1} pkg_sum={2} hash_type={3}'.format( diff --git a/salt/cloud/__init__.py b/salt/cloud/__init__.py index 04b84a1759..fc48740255 100644 --- a/salt/cloud/__init__.py +++ b/salt/cloud/__init__.py @@ -2099,7 +2099,7 @@ class Map(Cloud): master_temp_pub = salt.utils.files.mkstemp() with salt.utils.files.fopen(master_temp_pub, 'w') as mtp: mtp.write(pub) - master_finger = salt.utils.pem_finger(master_temp_pub, sum_type=self.opts['hash_type']) + master_finger = salt.utils.crypt.pem_finger(master_temp_pub, sum_type=self.opts['hash_type']) os.unlink(master_temp_pub) if master_profile.get('make_minion', True) is True: @@ -2184,7 +2184,7 @@ class Map(Cloud): # mitigate man-in-the-middle attacks master_pub = os.path.join(self.opts['pki_dir'], 'master.pub') if os.path.isfile(master_pub): - master_finger = salt.utils.pem_finger(master_pub, sum_type=self.opts['hash_type']) + master_finger = salt.utils.crypt.pem_finger(master_pub, sum_type=self.opts['hash_type']) opts = self.opts.copy() if self.opts['parallel']: diff --git a/salt/cloud/clouds/opennebula.py b/salt/cloud/clouds/opennebula.py index 55aa531d0d..9d03202f17 100644 --- a/salt/cloud/clouds/opennebula.py +++ b/salt/cloud/clouds/opennebula.py @@ -76,7 +76,7 @@ from salt.exceptions import ( SaltCloudNotFound, SaltCloudSystemExit ) -import salt.utils +import salt.utils.data import salt.utils.files # Import Third Party Libs @@ -1575,7 +1575,7 @@ def image_persistent(call=None, kwargs=None): server, user, password = _get_xml_rpc() auth = ':'.join([user, password]) - response = server.one.image.persistent(auth, int(image_id), salt.utils.is_true(persist)) + response = server.one.image.persistent(auth, int(image_id), salt.utils.data.is_true(persist)) data = { 'action': 'image.persistent', @@ -2794,7 +2794,7 @@ def vm_allocate(call=None, kwargs=None): server, user, password = _get_xml_rpc() auth = ':'.join([user, password]) - response = server.one.vm.allocate(auth, data, salt.utils.is_true(hold)) + response = server.one.vm.allocate(auth, data, salt.utils.data.is_true(hold)) ret = { 'action': 'vm.allocate', @@ -3026,7 +3026,7 @@ def vm_deploy(name, kwargs=None, call=None): response = server.one.vm.deploy(auth, int(vm_id), int(host_id), - salt.utils.is_true(capacity_maintained), + salt.utils.data.is_true(capacity_maintained), int(datastore_id)) data = { @@ -3495,8 +3495,8 @@ def vm_migrate(name, kwargs=None, call=None): response = server.one.vm.migrate(auth, vm_id, int(host_id), - salt.utils.is_true(live_migration), - salt.utils.is_true(capacity_maintained), + salt.utils.data.is_true(live_migration), + salt.utils.data.is_true(capacity_maintained), int(datastore_id)) data = { @@ -3615,7 +3615,7 @@ def vm_resize(name, kwargs=None, call=None): server, user, password = _get_xml_rpc() auth = ':'.join([user, password]) vm_id = int(get_vm_id(kwargs={'name': name})) - response = server.one.vm.resize(auth, vm_id, data, salt.utils.is_true(capacity_maintained)) + response = server.one.vm.resize(auth, vm_id, data, salt.utils.data.is_true(capacity_maintained)) ret = { 'action': 'vm.resize', diff --git a/salt/crypt.py b/salt/crypt.py index 764015e480..241b9b02b7 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -50,7 +50,6 @@ import salt.defaults.exitcodes import salt.payload import salt.transport.client import salt.transport.frame -import salt.utils # Can be removed when pem_finger is moved import salt.utils.crypt import salt.utils.decorators import salt.utils.event @@ -681,11 +680,11 @@ class AsyncAuth(object): if self.opts.get(u'syndic_master', False): # Is syndic syndic_finger = self.opts.get(u'syndic_finger', self.opts.get(u'master_finger', False)) if syndic_finger: - if salt.utils.pem_finger(m_pub_fn, sum_type=self.opts[u'hash_type']) != syndic_finger: + if salt.utils.crypt.pem_finger(m_pub_fn, sum_type=self.opts[u'hash_type']) != syndic_finger: self._finger_fail(syndic_finger, m_pub_fn) else: if self.opts.get(u'master_finger', False): - if salt.utils.pem_finger(m_pub_fn, sum_type=self.opts[u'hash_type']) != self.opts[u'master_finger']: + if salt.utils.crypt.pem_finger(m_pub_fn, sum_type=self.opts[u'hash_type']) != self.opts[u'master_finger']: self._finger_fail(self.opts[u'master_finger'], m_pub_fn) auth[u'publish_port'] = payload[u'publish_port'] raise tornado.gen.Return(auth) @@ -1030,7 +1029,7 @@ class AsyncAuth(object): u'matches the fingerprint of the correct master and that ' u'this minion is not subject to a man-in-the-middle attack.', finger, - salt.utils.pem_finger(master_key, sum_type=self.opts[u'hash_type']) + salt.utils.crypt.pem_finger(master_key, sum_type=self.opts[u'hash_type']) ) sys.exit(42) @@ -1237,11 +1236,11 @@ class SAuth(AsyncAuth): if self.opts.get(u'syndic_master', False): # Is syndic syndic_finger = self.opts.get(u'syndic_finger', self.opts.get(u'master_finger', False)) if syndic_finger: - if salt.utils.pem_finger(m_pub_fn, sum_type=self.opts[u'hash_type']) != syndic_finger: + if salt.utils.crypt.pem_finger(m_pub_fn, sum_type=self.opts[u'hash_type']) != syndic_finger: self._finger_fail(syndic_finger, m_pub_fn) else: if self.opts.get(u'master_finger', False): - if salt.utils.pem_finger(m_pub_fn, sum_type=self.opts[u'hash_type']) != self.opts[u'master_finger']: + if salt.utils.crypt.pem_finger(m_pub_fn, sum_type=self.opts[u'hash_type']) != self.opts[u'master_finger']: self._finger_fail(self.opts[u'master_finger'], m_pub_fn) auth[u'publish_port'] = payload[u'publish_port'] return auth diff --git a/salt/fileclient.py b/salt/fileclient.py index 35c63b2cb1..0836df7230 100644 --- a/salt/fileclient.py +++ b/salt/fileclient.py @@ -27,6 +27,7 @@ import salt.fileserver import salt.utils import salt.utils.files import salt.utils.gzip_util +import salt.utils.hashutils import salt.utils.http import salt.utils.path import salt.utils.platform @@ -530,7 +531,7 @@ class Client(object): try: source_hash = source_hash.split('=')[-1] form = salt.utils.files.HASHES_REVMAP[len(source_hash)] - if salt.utils.get_hash(dest, form) == source_hash: + if salt.utils.hashutils.get_hash(dest, form) == source_hash: log.debug( 'Cached copy of %s (%s) matches source_hash %s, ' 'skipping download', url, dest, source_hash @@ -968,7 +969,7 @@ class LocalClient(Client): fnd_path = fnd hash_type = self.opts.get(u'hash_type', u'md5') - ret[u'hsum'] = salt.utils.get_hash(fnd_path, form=hash_type) + ret[u'hsum'] = salt.utils.hashutils.get_hash(fnd_path, form=hash_type) ret[u'hash_type'] = hash_type return ret @@ -999,7 +1000,7 @@ class LocalClient(Client): fnd_stat = None hash_type = self.opts.get(u'hash_type', u'md5') - ret[u'hsum'] = salt.utils.get_hash(fnd_path, form=hash_type) + ret[u'hsum'] = salt.utils.hashutils.get_hash(fnd_path, form=hash_type) ret[u'hash_type'] = hash_type return ret, fnd_stat @@ -1194,7 +1195,7 @@ class RemoteClient(Client): # Master has prompted a file verification, if the # verification fails, re-download the file. Try 3 times d_tries += 1 - hsum = salt.utils.get_hash(dest, salt.utils.stringutils.to_str(data.get(u'hash_type', b'md5'))) # future lint: disable=non-unicode-string + hsum = salt.utils.hashutils.get_hash(dest, salt.utils.stringutils.to_str(data.get(u'hash_type', b'md5'))) # future lint: disable=non-unicode-string if hsum != data[u'hsum']: log.warning( u'Bad download of file %s, attempt %d of 3', @@ -1308,7 +1309,7 @@ class RemoteClient(Client): else: ret = {} hash_type = self.opts.get(u'hash_type', u'md5') - ret[u'hsum'] = salt.utils.get_hash(path, form=hash_type) + ret[u'hsum'] = salt.utils.hashutils.get_hash(path, form=hash_type) ret[u'hash_type'] = hash_type return ret load = {u'path': path, diff --git a/salt/fileserver/azurefs.py b/salt/fileserver/azurefs.py index 7eb919fa7d..560455779b 100644 --- a/salt/fileserver/azurefs.py +++ b/salt/fileserver/azurefs.py @@ -58,6 +58,7 @@ import salt.fileserver import salt.utils import salt.utils.files import salt.utils.gzip_util +import salt.utils.hashutils import salt.utils.path from salt.utils.versions import LooseVersion @@ -166,7 +167,7 @@ def serve_file(load, fnd): with salt.utils.files.fopen(fpath, 'rb') as fp_: fp_.seek(load['loc']) data = fp_.read(__opts__['file_buffer_size']) - if data and six.PY3 and not salt.utils.is_bin_file(fpath): + if data and six.PY3 and not salt.utils.files.is_binary(fpath): data = data.decode(__salt_system_encoding__) if gzip and data: data = salt.utils.gzip_util.compress(data, gzip) @@ -226,7 +227,7 @@ def update(): if os.path.exists(fname): # File exists, check the hashes source_md5 = blob.properties.content_settings.content_md5 - local_md5 = base64.b64encode(salt.utils.get_hash(fname, 'md5').decode('hex')) + local_md5 = base64.b64encode(salt.utils.hashutils.get_hash(fname, 'md5').decode('hex')) if local_md5 != source_md5: update = True else: @@ -289,7 +290,7 @@ def file_hash(load, fnd): if not os.path.isfile(hashdest): if not os.path.exists(os.path.dirname(hashdest)): os.makedirs(os.path.dirname(hashdest)) - ret['hsum'] = salt.utils.get_hash(path, __opts__['hash_type']) + ret['hsum'] = salt.utils.hashutils.get_hash(path, __opts__['hash_type']) with salt.utils.files.fopen(hashdest, 'w+') as fp_: fp_.write(ret['hsum']) return ret diff --git a/salt/fileserver/hgfs.py b/salt/fileserver/hgfs.py index 42c830c528..495021dfd5 100644 --- a/salt/fileserver/hgfs.py +++ b/salt/fileserver/hgfs.py @@ -63,6 +63,7 @@ import salt.utils import salt.utils.data import salt.utils.files import salt.utils.gzip_util +import salt.utils.hashutils import salt.utils.url import salt.utils.versions import salt.fileserver @@ -752,7 +753,7 @@ def serve_file(load, fnd): with salt.utils.files.fopen(fpath, 'rb') as fp_: fp_.seek(load['loc']) data = fp_.read(__opts__['file_buffer_size']) - if data and six.PY3 and not salt.utils.is_bin_file(fpath): + if data and six.PY3 and not salt.utils.files.is_binary(fpath): data = data.decode(__salt_system_encoding__) if gzip and data: data = salt.utils.gzip_util.compress(data, gzip) @@ -780,7 +781,7 @@ def file_hash(load, fnd): '{0}.hash.{1}'.format(relpath, __opts__['hash_type'])) if not os.path.isfile(hashdest): - ret['hsum'] = salt.utils.get_hash(path, __opts__['hash_type']) + ret['hsum'] = salt.utils.hashutils.get_hash(path, __opts__['hash_type']) with salt.utils.files.fopen(hashdest, 'w+') as fp_: fp_.write(ret['hsum']) return ret diff --git a/salt/fileserver/minionfs.py b/salt/fileserver/minionfs.py index 292bf0f85e..fd5dd2ed23 100644 --- a/salt/fileserver/minionfs.py +++ b/salt/fileserver/minionfs.py @@ -34,6 +34,7 @@ import salt.fileserver import salt.utils import salt.utils.files import salt.utils.gzip_util +import salt.utils.hashutils import salt.utils.url import salt.utils.versions @@ -135,7 +136,7 @@ def serve_file(load, fnd): with salt.utils.files.fopen(fpath, 'rb') as fp_: fp_.seek(load['loc']) data = fp_.read(__opts__['file_buffer_size']) - if data and six.PY3 and not salt.utils.is_bin_file(fpath): + if data and six.PY3 and not salt.utils.files.is_binary(fpath): data = data.decode(__salt_system_encoding__) if gzip and data: data = salt.utils.gzip_util.compress(data, gzip) @@ -213,7 +214,7 @@ def file_hash(load, fnd): return ret # if we don't have a cache entry-- lets make one - ret['hsum'] = salt.utils.get_hash(path, __opts__['hash_type']) + ret['hsum'] = salt.utils.hashutils.get_hash(path, __opts__['hash_type']) cache_dir = os.path.dirname(cache_path) # make cache directory if it doesn't exist if not os.path.exists(cache_dir): diff --git a/salt/fileserver/roots.py b/salt/fileserver/roots.py index 0547ff0e62..a92537fa14 100644 --- a/salt/fileserver/roots.py +++ b/salt/fileserver/roots.py @@ -24,10 +24,10 @@ import logging # Import salt libs import salt.fileserver -import salt.utils # Can be removed once is_bin_file and get_hash are moved import salt.utils.event import salt.utils.files import salt.utils.gzip_util +import salt.utils.hashutils import salt.utils.path import salt.utils.versions from salt.ext import six @@ -127,7 +127,7 @@ def serve_file(load, fnd): with salt.utils.files.fopen(fpath, 'rb') as fp_: fp_.seek(load['loc']) data = fp_.read(__opts__['file_buffer_size']) - if data and six.PY3 and not salt.utils.is_bin_file(fpath): + if data and six.PY3 and not salt.utils.files.is_binary(fpath): data = data.decode(__salt_system_encoding__) if gzip and data: data = salt.utils.gzip_util.compress(data, gzip) @@ -258,7 +258,7 @@ def file_hash(load, fnd): return file_hash(load, fnd) # if we don't have a cache entry-- lets make one - ret['hsum'] = salt.utils.get_hash(path, __opts__['hash_type']) + ret['hsum'] = salt.utils.hashutils.get_hash(path, __opts__['hash_type']) cache_dir = os.path.dirname(cache_path) # make cache directory if it doesn't exist if not os.path.exists(cache_dir): diff --git a/salt/fileserver/s3fs.py b/salt/fileserver/s3fs.py index 04f0b5e51c..5883408d94 100644 --- a/salt/fileserver/s3fs.py +++ b/salt/fileserver/s3fs.py @@ -71,6 +71,7 @@ import salt.modules import salt.utils import salt.utils.files import salt.utils.gzip_util +import salt.utils.hashutils import salt.utils.versions # Import 3rd-party libs @@ -180,7 +181,7 @@ def file_hash(load, fnd): fnd['path']) if os.path.isfile(cached_file_path): - ret['hsum'] = salt.utils.get_hash(cached_file_path) + ret['hsum'] = salt.utils.hashutils.get_hash(cached_file_path) ret['hash_type'] = 'md5' return ret @@ -216,7 +217,7 @@ def serve_file(load, fnd): with salt.utils.files.fopen(cached_file_path, 'rb') as fp_: fp_.seek(load['loc']) data = fp_.read(__opts__['file_buffer_size']) - if data and six.PY3 and not salt.utils.is_bin_file(cached_file_path): + if data and six.PY3 and not salt.utils.files.is_binary(cached_file_path): data = data.decode(__salt_system_encoding__) if gzip and data: data = salt.utils.gzip_util.compress(data, gzip) @@ -619,7 +620,7 @@ def _get_file_from_s3(metadata, saltenv, bucket_name, path, cached_file_path): if file_etag.find('-') == -1: file_md5 = file_etag - cached_md5 = salt.utils.get_hash(cached_file_path, 'md5') + cached_md5 = salt.utils.hashutils.get_hash(cached_file_path, 'md5') # hashes match we have a cache hit if cached_md5 == file_md5: diff --git a/salt/fileserver/svnfs.py b/salt/fileserver/svnfs.py index cc60fbb312..c2ff3d373a 100644 --- a/salt/fileserver/svnfs.py +++ b/salt/fileserver/svnfs.py @@ -58,6 +58,7 @@ import salt.utils import salt.utils.data import salt.utils.files import salt.utils.gzip_util +import salt.utils.hashutils import salt.utils.url import salt.utils.versions import salt.fileserver @@ -647,7 +648,7 @@ def serve_file(load, fnd): with salt.utils.files.fopen(fpath, 'rb') as fp_: fp_.seek(load['loc']) data = fp_.read(__opts__['file_buffer_size']) - if data and six.PY3 and not salt.utils.is_bin_file(fpath): + if data and six.PY3 and not salt.utils.files.is_binary(fpath): data = data.decode(__salt_system_encoding__) if gzip and data: data = salt.utils.gzip_util.compress(data, gzip) @@ -697,7 +698,7 @@ def file_hash(load, fnd): return ret # if we don't have a cache entry-- lets make one - ret['hsum'] = salt.utils.get_hash(path, __opts__['hash_type']) + ret['hsum'] = salt.utils.hashutils.get_hash(path, __opts__['hash_type']) cache_dir = os.path.dirname(cache_path) # make cache directory if it doesn't exist if not os.path.exists(cache_dir): diff --git a/salt/key.py b/salt/key.py index 4d315a9e74..97904971b6 100644 --- a/salt/key.py +++ b/salt/key.py @@ -22,8 +22,10 @@ import salt.crypt import salt.daemons.masterapi import salt.exceptions import salt.minion -import salt.utils +import salt.utils # Can be removed once get_master_key is moved import salt.utils.args +import salt.utils.crypt +import salt.utils.data import salt.utils.event import salt.utils.files import salt.utils.kinds @@ -416,7 +418,7 @@ class Key(object): keydir, keyname, keysize, user = self._get_key_attrs(keydir, keyname, keysize, user) salt.crypt.gen_keys(keydir, keyname, keysize, user, self.passphrase) - return salt.utils.pem_finger(os.path.join(keydir, keyname + u'.pub')) + return salt.utils.crypt.pem_finger(os.path.join(keydir, keyname + u'.pub')) def gen_signature(self, privkey, pubkey, sig_path): ''' @@ -544,7 +546,7 @@ class Key(object): if u',' in match and isinstance(match, six.string_types): match = match.split(u',') for status, keys in six.iteritems(matches): - for key in salt.utils.isorted(keys): + for key in salt.utils.data.sorted_ignorecase(keys): if isinstance(match, list): for match_item in match: if fnmatch.fnmatch(key, match_item): @@ -566,7 +568,7 @@ class Key(object): ret = {} cur_keys = self.list_keys() for status, keys in six.iteritems(match_dict): - for key in salt.utils.isorted(keys): + for key in salt.utils.data.sorted_ignorecase(keys): for keydir in (self.ACC, self.PEND, self.REJ, self.DEN): if keydir and fnmatch.filter(cur_keys.get(keydir, []), key): ret.setdefault(keydir, []).append(key) @@ -577,7 +579,7 @@ class Key(object): Return a dict of local keys ''' ret = {u'local': []} - for fn_ in salt.utils.isorted(os.listdir(self.opts[u'pki_dir'])): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(self.opts[u'pki_dir'])): if fn_.endswith(u'.pub') or fn_.endswith(u'.pem'): path = os.path.join(self.opts[u'pki_dir'], fn_) if os.path.isfile(path): @@ -603,7 +605,7 @@ class Key(object): continue ret[os.path.basename(dir_)] = [] try: - for fn_ in salt.utils.isorted(os.listdir(dir_)): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(dir_)): if not fn_.startswith(u'.'): if os.path.isfile(os.path.join(dir_, fn_)): ret[os.path.basename(dir_)].append(fn_) @@ -628,25 +630,25 @@ class Key(object): ret = {} if match.startswith(u'acc'): ret[os.path.basename(acc)] = [] - for fn_ in salt.utils.isorted(os.listdir(acc)): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(acc)): if not fn_.startswith(u'.'): if os.path.isfile(os.path.join(acc, fn_)): ret[os.path.basename(acc)].append(fn_) elif match.startswith(u'pre') or match.startswith(u'un'): ret[os.path.basename(pre)] = [] - for fn_ in salt.utils.isorted(os.listdir(pre)): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(pre)): if not fn_.startswith(u'.'): if os.path.isfile(os.path.join(pre, fn_)): ret[os.path.basename(pre)].append(fn_) elif match.startswith(u'rej'): ret[os.path.basename(rej)] = [] - for fn_ in salt.utils.isorted(os.listdir(rej)): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(rej)): if not fn_.startswith(u'.'): if os.path.isfile(os.path.join(rej, fn_)): ret[os.path.basename(rej)].append(fn_) elif match.startswith(u'den') and den is not None: ret[os.path.basename(den)] = [] - for fn_ in salt.utils.isorted(os.listdir(den)): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(den)): if not fn_.startswith(u'.'): if os.path.isfile(os.path.join(den, fn_)): ret[os.path.basename(den)].append(fn_) @@ -661,7 +663,7 @@ class Key(object): ret = {} for status, keys in six.iteritems(self.name_match(match)): ret[status] = {} - for key in salt.utils.isorted(keys): + for key in salt.utils.data.sorted_ignorecase(keys): path = os.path.join(self.opts[u'pki_dir'], status, key) with salt.utils.files.fopen(path, u'r') as fp_: ret[status][key] = fp_.read() @@ -674,7 +676,7 @@ class Key(object): ret = {} for status, keys in six.iteritems(self.list_keys()): ret[status] = {} - for key in salt.utils.isorted(keys): + for key in salt.utils.data.sorted_ignorecase(keys): path = os.path.join(self.opts[u'pki_dir'], status, key) with salt.utils.files.fopen(path, u'r') as fp_: ret[status][key] = fp_.read() @@ -927,7 +929,7 @@ class Key(object): path = os.path.join(self.opts[u'pki_dir'], key) else: path = os.path.join(self.opts[u'pki_dir'], status, key) - ret[status][key] = salt.utils.pem_finger(path, sum_type=hash_type) + ret[status][key] = salt.utils.crypt.pem_finger(path, sum_type=hash_type) return ret def finger_all(self, hash_type=None): @@ -945,7 +947,7 @@ class Key(object): path = os.path.join(self.opts[u'pki_dir'], key) else: path = os.path.join(self.opts[u'pki_dir'], status, key) - ret[status][key] = salt.utils.pem_finger(path, sum_type=hash_type) + ret[status][key] = salt.utils.crypt.pem_finger(path, sum_type=hash_type) return ret @@ -1165,7 +1167,7 @@ class RaetKey(Key): ret = {} for status, keys in six.iteritems(self.name_match(match)): ret[status] = {} - for key in salt.utils.isorted(keys): + for key in salt.utils.data.sorted_ignorecase(keys): ret[status][key] = self._get_key_str(key, status) return ret @@ -1176,7 +1178,7 @@ class RaetKey(Key): ret = {} for status, keys in six.iteritems(self.list_keys()): ret[status] = {} - for key in salt.utils.isorted(keys): + for key in salt.utils.data.sorted_ignorecase(keys): ret[status][key] = self._get_key_str(key, status) return ret diff --git a/salt/modules/apk.py b/salt/modules/apk.py index 51c1ad6627..13975fe8d6 100644 --- a/salt/modules/apk.py +++ b/salt/modules/apk.py @@ -18,7 +18,6 @@ import copy import logging # Import salt libs -import salt.utils import salt.utils.data import salt.utils.itertools @@ -131,9 +130,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs salt '*' pkg.list_pkgs versions_as_list=True ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} @@ -177,7 +176,7 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version salt '*' pkg.latest_version ... ''' - refresh = salt.utils.is_true(kwargs.pop('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', True)) if len(names) == 0: return '' @@ -290,7 +289,7 @@ def install(name=None, {'': {'old': '', 'new': ''}} ''' - refreshdb = salt.utils.is_true(refresh) + refreshdb = salt.utils.data.is_true(refresh) pkg_to_install = [] old = list_pkgs() @@ -447,7 +446,7 @@ def upgrade(name=None, pkgs=None, refresh=True): 'comment': '', } - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() old = list_pkgs() @@ -497,7 +496,7 @@ def list_upgrades(refresh=True): salt '*' pkg.list_upgrades ''' ret = {} - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() cmd = ['apk', 'upgrade', '-s'] diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py index 2ee9223cc2..9fb042a9cc 100644 --- a/salt/modules/aptpkg.py +++ b/salt/modules/aptpkg.py @@ -38,7 +38,7 @@ from salt.ext.six.moves.urllib.request import Request as _Request, urlopen as _u import salt.config import salt.syspaths from salt.modules.cmdmod import _parse_env -import salt.utils +import salt.utils # Can be removed when alias_function is moved import salt.utils.args import salt.utils.data import salt.utils.files @@ -240,8 +240,8 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version fromrepo=unstable salt '*' pkg.latest_version ... ''' - refresh = salt.utils.is_true(kwargs.pop('refresh', True)) - show_installed = salt.utils.is_true(kwargs.pop('show_installed', False)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', True)) + show_installed = salt.utils.data.is_true(kwargs.pop('show_installed', False)) if 'repo' in kwargs: raise SaltInvocationError( 'The \'repo\' argument is invalid, use \'fromrepo\' instead' @@ -371,7 +371,7 @@ def refresh_db(cache_valid_time=0, failhard=False): ''' # Remove rtag file to keep multiple refreshes from happening in pkg states salt.utils.pkg.clear_rtag(__opts__) - failhard = salt.utils.is_true(failhard) + failhard = salt.utils.data.is_true(failhard) ret = {} error_repos = list() @@ -576,7 +576,7 @@ def install(name=None, 'new': ''}} ''' _refresh_db = False - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): _refresh_db = True if 'version' in kwargs and kwargs['version']: _refresh_db = False @@ -1099,7 +1099,7 @@ def upgrade(refresh=True, dist_upgrade=False, **kwargs): salt '*' pkg.upgrade ''' cache_valid_time = kwargs.pop('cache_valid_time', 0) - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db(cache_valid_time) old = list_pkgs() @@ -1198,7 +1198,7 @@ def hold(name=None, pkgs=None, sources=None, **kwargs): # pylint: disable=W0613 if not state: ret[target]['comment'] = ('Package {0} not currently held.' .format(target)) - elif not salt.utils.is_true(state.get('hold', False)): + elif not salt.utils.data.is_true(state.get('hold', False)): if 'test' in __opts__ and __opts__['test']: ret[target].update(result=None) ret[target]['comment'] = ('Package {0} is set to be held.' @@ -1272,7 +1272,7 @@ def unhold(name=None, pkgs=None, sources=None, **kwargs): # pylint: disable=W06 if not state: ret[target]['comment'] = ('Package {0} does not have a state.' .format(target)) - elif salt.utils.is_true(state.get('hold', False)): + elif salt.utils.data.is_true(state.get('hold', False)): if 'test' in __opts__ and __opts__['test']: ret[target].update(result=None) ret[target]['comment'] = ('Package {0} is set not to be ' @@ -1340,9 +1340,9 @@ def list_pkgs(versions_as_list=False, salt '*' pkg.list_pkgs salt '*' pkg.list_pkgs versions_as_list=True ''' - versions_as_list = salt.utils.is_true(versions_as_list) - removed = salt.utils.is_true(removed) - purge_desired = salt.utils.is_true(purge_desired) + versions_as_list = salt.utils.data.is_true(versions_as_list) + removed = salt.utils.data.is_true(removed) + purge_desired = salt.utils.data.is_true(purge_desired) if 'pkg.list_pkgs' in __context__: if removed: @@ -1502,7 +1502,7 @@ def list_upgrades(refresh=True, dist_upgrade=True, **kwargs): salt '*' pkg.list_upgrades ''' cache_valid_time = kwargs.pop('cache_valid_time', 0) - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db(cache_valid_time) return _get_upgradable(dist_upgrade, **kwargs) @@ -2389,7 +2389,7 @@ def mod_repo(repo, saltenv='base', **kwargs): kwargs['architectures'] = kwargs['architectures'].split(',') if 'disabled' in kwargs: - kwargs['disabled'] = salt.utils.is_true(kwargs['disabled']) + kwargs['disabled'] = salt.utils.data.is_true(kwargs['disabled']) kw_type = kwargs.get('type') kw_dist = kwargs.get('dist') diff --git a/salt/modules/chocolatey.py b/salt/modules/chocolatey.py index 1226505f47..5f19196447 100644 --- a/salt/modules/chocolatey.py +++ b/salt/modules/chocolatey.py @@ -14,7 +14,7 @@ import re import tempfile # Import salt libs -import salt.utils +import salt.utils.data import salt.utils.platform from salt.utils.versions import LooseVersion as _LooseVersion from salt.exceptions import CommandExecutionError, CommandNotFoundError, \ @@ -274,9 +274,9 @@ def list_(narrow=None, cmd = [choc_path, 'list'] if narrow: cmd.append(narrow) - if salt.utils.is_true(all_versions): + if salt.utils.data.is_true(all_versions): cmd.append('--allversions') - if salt.utils.is_true(pre_versions): + if salt.utils.data.is_true(pre_versions): cmd.append('--prerelease') if source: cmd.extend(['--source', source]) @@ -452,9 +452,9 @@ def install(name, cmd.extend(['--version', version]) if source: cmd.extend(['--source', source]) - if salt.utils.is_true(force): + if salt.utils.data.is_true(force): cmd.append('--force') - if salt.utils.is_true(pre_versions): + if salt.utils.data.is_true(pre_versions): cmd.append('--prerelease') if install_args: cmd.extend(['--installarguments', install_args]) @@ -802,9 +802,9 @@ def upgrade(name, cmd.extend(['-version', version]) if source: cmd.extend(['--source', source]) - if salt.utils.is_true(force): + if salt.utils.data.is_true(force): cmd.append('--force') - if salt.utils.is_true(pre_versions): + if salt.utils.data.is_true(pre_versions): cmd.append('--prerelease') if install_args: cmd.extend(['--installarguments', install_args]) @@ -862,7 +862,7 @@ def update(name, source=None, pre_versions=False, no_progress=False): cmd = [choc_path, 'update', name] if source: cmd.extend(['--source', source]) - if salt.utils.is_true(pre_versions): + if salt.utils.data.is_true(pre_versions): cmd.append('--prerelease') if no_progress: cmd.append(_no_progress(__context__)) diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index de4f503019..8a03bacd7e 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -24,7 +24,6 @@ import re import tempfile # Import salt libs -import salt.utils import salt.utils.args import salt.utils.data import salt.utils.files @@ -34,6 +33,7 @@ import salt.utils.powershell import salt.utils.stringutils import salt.utils.templates import salt.utils.timed_subprocess +import salt.utils.user import salt.utils.versions import salt.utils.vt import salt.grains.extra @@ -188,7 +188,7 @@ def _check_loglevel(level='info', quiet=False): ) return LOG_LEVELS['info'] - if salt.utils.is_true(quiet) or str(level).lower() == 'quiet': + if salt.utils.data.is_true(quiet) or str(level).lower() == 'quiet': return None try: @@ -525,7 +525,7 @@ def _run(cmd, if runas or umask: kwargs['preexec_fn'] = functools.partial( - salt.utils.chugid_and_umask, + salt.utils.user.chugid_and_umask, runas, _umask) diff --git a/salt/modules/config.py b/salt/modules/config.py index d9dfeafbaf..3b641f8e87 100644 --- a/salt/modules/config.py +++ b/salt/modules/config.py @@ -12,8 +12,9 @@ import logging # Import salt libs import salt.config -import salt.utils import salt.utils.data +import salt.utils.dictupdate +import salt.utils.files import salt.utils.platform try: # Gated for salt-ssh (salt.utils.cloud imports msgpack) @@ -108,7 +109,7 @@ def manage_mode(mode): # config.manage_mode should no longer be invoked from the __salt__ dunder # in Salt code, this function is only being left here for backwards # compatibility. - return salt.utils.normalize_mode(mode) + return salt.utils.files.normalize_mode(mode) def valid_fileproto(uri): diff --git a/salt/modules/cp.py b/salt/modules/cp.py index cdbeb4434e..9c2ea4e3c1 100644 --- a/salt/modules/cp.py +++ b/salt/modules/cp.py @@ -748,7 +748,7 @@ def stat_file(path, saltenv='base', octal=True): stat = _client().hash_and_stat_file(path, saltenv)[1] if stat is None: return stat - return salt.utils.st_mode_to_octal(stat[0]) if octal is True else stat[0] + return salt.utils.files.st_mode_to_octal(stat[0]) if octal is True else stat[0] def push(path, keep_symlinks=False, upload_path=None, remove_source=False): diff --git a/salt/modules/dockermod.py b/salt/modules/dockermod.py index 5e9ef7d3b4..58cc1f0d25 100644 --- a/salt/modules/dockermod.py +++ b/salt/modules/dockermod.py @@ -206,6 +206,7 @@ import salt.utils.args import salt.utils.decorators import salt.utils.docker import salt.utils.files +import salt.utils.hashutils import salt.utils.path import salt.utils.stringutils import salt.utils.thin @@ -5431,7 +5432,7 @@ def sls(name, mods=None, saltenv='base', **kwargs): ret = None try: - trans_tar_sha256 = salt.utils.get_hash(trans_tar, 'sha256') + trans_tar_sha256 = salt.utils.hashutils.get_hash(trans_tar, 'sha256') copy_to(name, trans_tar, os.path.join(trans_dest_path, 'salt_state.tgz'), diff --git a/salt/modules/ebuild.py b/salt/modules/ebuild.py index 354a37732d..34ee1f147c 100644 --- a/salt/modules/ebuild.py +++ b/salt/modules/ebuild.py @@ -21,7 +21,7 @@ import logging import re # Import salt libs -import salt.utils +import salt.utils # Can be removed once alias_function is moved import salt.utils.args import salt.utils.data import salt.utils.path @@ -237,7 +237,7 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version salt '*' pkg.latest_version ... ''' - refresh = salt.utils.is_true(kwargs.pop('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', True)) if len(names) == 0: return '' @@ -334,7 +334,7 @@ def list_upgrades(refresh=True, backtrack=3, **kwargs): # pylint: disable=W0613 salt '*' pkg.list_upgrades ''' - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() return _get_upgradable(backtrack) @@ -394,9 +394,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} @@ -597,7 +597,7 @@ def install(name=None, 'binhost': binhost, } )) - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() try: @@ -764,7 +764,7 @@ def update(pkg, slot=None, fromrepo=None, refresh=False, binhost=None): salt '*' pkg.update ''' - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() full_atom = pkg @@ -864,7 +864,7 @@ def upgrade(refresh=True, binhost=None, backtrack=3): 'result': True, 'comment': ''} - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() if binhost == 'try': diff --git a/salt/modules/file.py b/salt/modules/file.py index 0ca2a16dc6..1e91d73552 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -52,6 +52,7 @@ import salt.utils.atomicfile import salt.utils.filebuffer import salt.utils.files import salt.utils.find +import salt.utils.hashutils import salt.utils.itertools import salt.utils.locales import salt.utils.path @@ -118,8 +119,8 @@ def _binary_replace(old, new): This function should only be run AFTER it has been determined that the files differ. ''' - old_isbin = not __utils__['files.is_text_file'](old) - new_isbin = not __utils__['files.is_text_file'](new) + old_isbin = not __utils__['files.is_text'](old) + new_isbin = not __utils__['files.is_text'](new) if any((old_isbin, new_isbin)): if all((old_isbin, new_isbin)): return u'Replace binary file' @@ -660,7 +661,7 @@ def get_sum(path, form='sha256'): if not os.path.isfile(path): return 'File not found' - return salt.utils.get_hash(path, form, 4096) + return salt.utils.hashutils.get_hash(path, form, 4096) def get_hash(path, form='sha256', chunk_size=65536): @@ -688,7 +689,7 @@ def get_hash(path, form='sha256', chunk_size=65536): salt '*' file.get_hash /etc/shadow ''' - return salt.utils.get_hash(os.path.expanduser(path), form, chunk_size) + return salt.utils.hashutils.get_hash(os.path.expanduser(path), form, chunk_size) def get_source_sum(file_name='', @@ -1428,7 +1429,7 @@ def comment_line(path, raise SaltInvocationError('File not found: {0}'.format(path)) # Make sure it is a text file - if not __utils__['files.is_text_file'](path): + if not __utils__['files.is_text'](path): raise SaltInvocationError( 'Cannot perform string replacements on a binary file: {0}'.format(path)) @@ -1471,7 +1472,7 @@ def comment_line(path, if not salt.utils.platform.is_windows(): pre_user = get_user(path) pre_group = get_group(path) - pre_mode = salt.utils.normalize_mode(get_mode(path)) + pre_mode = salt.utils.files.normalize_mode(get_mode(path)) # Create a copy to read from and to use as a backup later try: @@ -2172,7 +2173,7 @@ def replace(path, else: raise SaltInvocationError('File not found: {0}'.format(path)) - if not __utils__['files.is_text_file'](path): + if not __utils__['files.is_text'](path): raise SaltInvocationError( 'Cannot perform string replacements on a binary file: {0}' .format(path) @@ -2201,7 +2202,7 @@ def replace(path, if not salt.utils.platform.is_windows(): pre_user = get_user(path) pre_group = get_group(path) - pre_mode = salt.utils.normalize_mode(get_mode(path)) + pre_mode = salt.utils.files.normalize_mode(get_mode(path)) # Avoid TypeErrors by forcing repl to be bytearray related to mmap # Replacement text may contains integer: 123 for example @@ -2489,7 +2490,7 @@ def blockreplace(path, 'Only one of append and prepend_if_not_found is permitted' ) - if not __utils__['files.is_text_file'](path): + if not __utils__['files.is_text'](path): raise SaltInvocationError( 'Cannot perform string replacements on a binary file: {0}' .format(path) @@ -2604,7 +2605,7 @@ def blockreplace(path, perms = {} perms['user'] = get_user(path) perms['group'] = get_group(path) - perms['mode'] = salt.utils.normalize_mode(get_mode(path)) + perms['mode'] = salt.utils.files.normalize_mode(get_mode(path)) # backup old content if backup is not False: @@ -3300,7 +3301,7 @@ def copy(src, dst, recurse=False, remove_existing=False): if not salt.utils.platform.is_windows(): pre_user = get_user(src) pre_group = get_group(src) - pre_mode = salt.utils.normalize_mode(get_mode(src)) + pre_mode = salt.utils.files.normalize_mode(get_mode(src)) try: if (os.path.exists(dst) and os.path.isdir(dst)) or os.path.isdir(src): @@ -4354,7 +4355,7 @@ def check_perms(name, ret, user, group, mode, attrs=None, follow_symlinks=False) raise CommandExecutionError('{0} does not exist'.format(name)) perms['luser'] = cur['user'] perms['lgroup'] = cur['group'] - perms['lmode'] = salt.utils.normalize_mode(cur['mode']) + perms['lmode'] = salt.utils.files.normalize_mode(cur['mode']) is_dir = os.path.isdir(name) if not salt.utils.platform.is_windows() and not is_dir and lsattr_cmd: @@ -4371,13 +4372,13 @@ def check_perms(name, ret, user, group, mode, attrs=None, follow_symlinks=False) if os.path.islink(name) and not follow_symlinks: pass else: - mode = salt.utils.normalize_mode(mode) + mode = salt.utils.files.normalize_mode(mode) if mode != perms['lmode']: if __opts__['test'] is True: ret['changes']['mode'] = mode else: set_mode(name, mode) - if mode != salt.utils.normalize_mode(get_mode(name)): + if mode != salt.utils.files.normalize_mode(get_mode(name)): ret['result'] = False ret['comment'].append( 'Failed to change mode to {0}'.format(mode) @@ -4758,8 +4759,8 @@ def check_file_meta( changes['group'] = group # Normalize the file mode - smode = salt.utils.normalize_mode(lstats['mode']) - mode = salt.utils.normalize_mode(mode) + smode = salt.utils.files.normalize_mode(lstats['mode']) + mode = salt.utils.files.normalize_mode(mode) if mode is not None and mode != smode: changes['mode'] = mode @@ -5441,7 +5442,7 @@ def makedirs_(path, path = os.path.expanduser(path) if mode: - mode = salt.utils.normalize_mode(mode) + mode = salt.utils.files.normalize_mode(mode) # walk up the directory structure until we find the first existing # directory diff --git a/salt/modules/freebsdpkg.py b/salt/modules/freebsdpkg.py index ebec22ef7f..28806aa338 100644 --- a/salt/modules/freebsdpkg.py +++ b/salt/modules/freebsdpkg.py @@ -80,7 +80,7 @@ import logging import re # Import salt libs -import salt.utils +import salt.utils # Can be removed when alias_function is moved import salt.utils.data import salt.utils.pkg from salt.exceptions import CommandExecutionError, MinionError @@ -225,7 +225,7 @@ def version(*names, **kwargs): ''' with_origin = kwargs.pop('with_origin', False) ret = __salt__['pkg_resource.version'](*names, **kwargs) - if not salt.utils.is_true(with_origin): + if not salt.utils.data.is_true(with_origin): return ret # Put the return value back into a dict since we're adding a subdict if len(names) == 1: @@ -271,9 +271,9 @@ def list_pkgs(versions_as_list=False, with_origin=False, **kwargs): salt '*' pkg.list_pkgs ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} @@ -281,7 +281,7 @@ def list_pkgs(versions_as_list=False, with_origin=False, **kwargs): ret = copy.deepcopy(__context__['pkg.list_pkgs']) if not versions_as_list: __salt__['pkg_resource.stringify'](ret) - if salt.utils.is_true(with_origin): + if salt.utils.data.is_true(with_origin): origins = __context__.get('pkg.origin', {}) return dict([ (x, {'origin': origins.get(x, ''), 'version': y}) @@ -310,7 +310,7 @@ def list_pkgs(versions_as_list=False, with_origin=False, **kwargs): __context__['pkg.origin'] = origins if not versions_as_list: __salt__['pkg_resource.stringify'](ret) - if salt.utils.is_true(with_origin): + if salt.utils.data.is_true(with_origin): return dict([ (x, {'origin': origins.get(x, ''), 'version': y}) for x, y in six.iteritems(ret) diff --git a/salt/modules/grains.py b/salt/modules/grains.py index 0b21a43672..a4822477b1 100644 --- a/salt/modules/grains.py +++ b/salt/modules/grains.py @@ -17,7 +17,6 @@ from functools import reduce # pylint: disable=redefined-builtin # Import Salt libs from salt.ext import six -import salt.utils # Can be removed once is_true is moved import salt.utils.compat import salt.utils.data import salt.utils.files @@ -157,7 +156,7 @@ def items(sanitize=False): salt '*' grains.items sanitize=True ''' - if salt.utils.is_true(sanitize): + if salt.utils.data.is_true(sanitize): out = dict(__grains__) for key, func in six.iteritems(_SANITIZERS): if key in out: @@ -198,7 +197,7 @@ def item(*args, **kwargs): except KeyError: pass - if salt.utils.is_true(kwargs.get('sanitize')): + if salt.utils.data.is_true(kwargs.get('sanitize')): for arg, func in six.iteritems(_SANITIZERS): if arg in ret: ret[arg] = func(ret[arg]) diff --git a/salt/modules/hg.py b/salt/modules/hg.py index d4992eb34a..bb0282cb98 100644 --- a/salt/modules/hg.py +++ b/salt/modules/hg.py @@ -9,7 +9,7 @@ import logging # Import salt libs from salt.exceptions import CommandExecutionError -import salt.utils +import salt.utils.data import salt.utils.path log = logging.getLogger(__name__) @@ -323,7 +323,7 @@ def status(cwd, opts=None, user=None): ret[t].append(f) return ret - if salt.utils.is_iter(cwd): + if salt.utils.data.is_iter(cwd): return dict((cwd, _status(cwd)) for cwd in cwd) else: return _status(cwd) diff --git a/salt/modules/key.py b/salt/modules/key.py index c68871cbc6..73fe04fdd9 100644 --- a/salt/modules/key.py +++ b/salt/modules/key.py @@ -8,7 +8,7 @@ from __future__ import absolute_import import os # Import Salt libs -import salt.utils +import salt.utils.crypt def finger(hash_type=None): @@ -27,7 +27,7 @@ def finger(hash_type=None): if hash_type is None: hash_type = __opts__['hash_type'] - return salt.utils.pem_finger( + return salt.utils.crypt.pem_finger( os.path.join(__opts__['pki_dir'], 'minion.pub'), sum_type=hash_type) @@ -48,6 +48,6 @@ def finger_master(hash_type=None): if hash_type is None: hash_type = __opts__['hash_type'] - return salt.utils.pem_finger( + return salt.utils.crypt.pem_finger( os.path.join(__opts__['pki_dir'], 'minion_master.pub'), sum_type=hash_type) diff --git a/salt/modules/lxc.py b/salt/modules/lxc.py index a72430c348..f9c69b5404 100644 --- a/salt/modules/lxc.py +++ b/salt/modules/lxc.py @@ -30,6 +30,7 @@ import salt.utils.args import salt.utils.cloud import salt.utils.dictupdate import salt.utils.files +import salt.utils.hashutils import salt.utils.network import salt.utils.odict import salt.utils.path @@ -1094,7 +1095,7 @@ def _get_base(**kwargs): proto = _urlparse(image).scheme img_tar = __salt__['cp.cache_file'](image) img_name = os.path.basename(img_tar) - hash_ = salt.utils.get_hash( + hash_ = salt.utils.hashutils.get_hash( img_tar, __salt__['config.get']('hash_type')) name = '__base_{0}_{1}_{2}'.format(proto, img_name, hash_) diff --git a/salt/modules/mac_brew.py b/salt/modules/mac_brew.py index 076f2873b6..0aa63efeef 100644 --- a/salt/modules/mac_brew.py +++ b/salt/modules/mac_brew.py @@ -17,7 +17,7 @@ import json import logging # Import salt libs -import salt.utils # Can be removed when alias_function, is_true are moved +import salt.utils # Can be removed when alias_function is moved import salt.utils.data import salt.utils.path import salt.utils.pkg @@ -108,9 +108,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} @@ -174,7 +174,7 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version salt '*' pkg.latest_version ''' - refresh = salt.utils.is_true(kwargs.pop('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', True)) if refresh: refresh_db() @@ -475,7 +475,7 @@ def upgrade(refresh=True): old = list_pkgs() - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() result = _call_brew('brew upgrade', failhard=False) diff --git a/salt/modules/mac_ports.py b/salt/modules/mac_ports.py index 07bf2c8f90..b50d5320ea 100644 --- a/salt/modules/mac_ports.py +++ b/salt/modules/mac_ports.py @@ -37,7 +37,7 @@ import logging import re # Import salt libs -import salt.utils # Can be removed when alias_function, is_true are removed +import salt.utils # Can be removed when alias_function is removed import salt.utils.data import salt.utils.path import salt.utils.pkg @@ -96,9 +96,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # 'removed', 'purge_desired' not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} @@ -163,7 +163,7 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version ''' - if salt.utils.is_true(kwargs.get('refresh', True)): + if salt.utils.data.is_true(kwargs.get('refresh', True)): refresh_db() available = _list(' '.join(names)) or {} @@ -299,11 +299,9 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): salt '*' pkg.install 'package package package' ''' pkg_params, pkg_type = \ - __salt__['pkg_resource.parse_targets'](name, - pkgs, - {}) + __salt__['pkg_resource.parse_targets'](name, pkgs, {}) - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() # Handle version kwarg for a single package target diff --git a/salt/modules/mac_softwareupdate.py b/salt/modules/mac_softwareupdate.py index 0f598ed0bf..3bc45200d4 100644 --- a/salt/modules/mac_softwareupdate.py +++ b/salt/modules/mac_softwareupdate.py @@ -10,7 +10,7 @@ import re import os # import salt libs -import salt.utils +import salt.utils.data import salt.utils.files import salt.utils.mac_utils import salt.utils.platform @@ -48,7 +48,7 @@ def _get_available(recommended=False, restart=False): rexp = re.compile('(?m)^ [*|-] ' r'([^ ].*)[\r\n].*\(([^\)]+)') - if salt.utils.is_true(recommended): + if salt.utils.data.is_true(recommended): # rexp parses lines that look like the following: # * Safari6.1.2MountainLion-6.1.2 # Safari (6.1.2), 51679K [recommended] @@ -66,7 +66,7 @@ def _get_available(recommended=False, restart=False): version_num = _get(line, 'version') ret[name] = version_num - if not salt.utils.is_true(restart): + if not salt.utils.data.is_true(restart): return ret # rexp parses lines that look like the following: diff --git a/salt/modules/minion.py b/salt/modules/minion.py index 94bf948b0b..9cf348a5c6 100644 --- a/salt/modules/minion.py +++ b/salt/modules/minion.py @@ -10,7 +10,7 @@ import sys import time # Import Salt libs -import salt.utils +import salt.utils.data import salt.key # Import third party libs @@ -55,7 +55,7 @@ def list_(): for dir_ in key_dirs: ret[os.path.basename(dir_)] = [] try: - for fn_ in salt.utils.isorted(os.listdir(dir_)): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(dir_)): if not fn_.startswith('.'): if os.path.isfile(os.path.join(dir_, fn_)): ret[os.path.basename(dir_)].append(fn_) diff --git a/salt/modules/mount.py b/salt/modules/mount.py index 9463283a4c..30fc955877 100644 --- a/salt/modules/mount.py +++ b/salt/modules/mount.py @@ -10,7 +10,7 @@ import re import logging # Import salt libs -import salt.utils # Can be removed once test_mode is moved +import salt.utils.args import salt.utils.files import salt.utils.path import salt.utils.platform @@ -625,7 +625,7 @@ def set_fstab( ret = 'new' if ret != 'present': # ret in ['new', 'change']: - if not salt.utils.test_mode(test=test, **kwargs): + if not salt.utils.args.test_mode(test=test, **kwargs): try: with salt.utils.files.fopen(config, 'w+') as ofile: # The line was changed, commit it! @@ -753,7 +753,7 @@ def set_vfstab( ret = 'new' if ret != 'present': # ret in ['new', 'change']: - if not salt.utils.test_mode(test=test, **kwargs): + if not salt.utils.args.test_mode(test=test, **kwargs): try: with salt.utils.files.fopen(config, 'w+') as ofile: # The line was changed, commit it! @@ -908,7 +908,7 @@ def set_automaster( raise CommandExecutionError(msg.format(config, str(exc))) if change: - if not salt.utils.test_mode(test=test, **kwargs): + if not salt.utils.args.test_mode(test=test, **kwargs): try: with salt.utils.files.fopen(config, 'w+') as ofile: # The line was changed, commit it! @@ -924,7 +924,7 @@ def set_automaster( # The right entry is already here return 'present' else: - if not salt.utils.test_mode(test=test, **kwargs): + if not salt.utils.args.test_mode(test=test, **kwargs): # The entry is new, add it to the end of the fstab newline = ( '{0}\t{1}\t{2}\n'.format( diff --git a/salt/modules/mysql.py b/salt/modules/mysql.py index 5dcb7957dd..bc0bbe7928 100644 --- a/salt/modules/mysql.py +++ b/salt/modules/mysql.py @@ -43,7 +43,7 @@ import shlex import os # Import salt libs -import salt.utils +import salt.utils.data import salt.utils.files # Import third party libs @@ -1228,8 +1228,8 @@ def user_exists(user, args['user'] = user args['host'] = host - if salt.utils.is_true(passwordless): - if salt.utils.is_true(unix_socket): + if salt.utils.data.is_true(passwordless): + if salt.utils.data.is_true(unix_socket): qry += ' AND plugin=%(unix_socket)s' args['unix_socket'] = 'unix_socket' else: @@ -1356,8 +1356,8 @@ def user_create(user, elif password_hash is not None: qry += ' IDENTIFIED BY PASSWORD %(password)s' args['password'] = password_hash - elif salt.utils.is_true(allow_passwordless): - if salt.utils.is_true(unix_socket): + elif salt.utils.data.is_true(allow_passwordless): + if salt.utils.data.is_true(unix_socket): if host == 'localhost': qry += ' IDENTIFIED VIA unix_socket' else: @@ -1441,7 +1441,7 @@ def user_chpass(user, elif password_hash is not None: password_sql = '%(password)s' args['password'] = password_hash - elif not salt.utils.is_true(allow_passwordless): + elif not salt.utils.data.is_true(allow_passwordless): log.error('password or password_hash must be specified, unless ' 'allow_passwordless=True') return False @@ -1461,8 +1461,8 @@ def user_chpass(user, ' WHERE User=%(user)s AND Host = %(host)s;') args['user'] = user args['host'] = host - if salt.utils.is_true(allow_passwordless) and \ - salt.utils.is_true(unix_socket): + if salt.utils.data.is_true(allow_passwordless) and \ + salt.utils.data.is_true(unix_socket): if host == 'localhost': qry = ('UPDATE mysql.user SET ' + password_column + '=' + password_sql + ', plugin=%(unix_socket)s' + @@ -1715,7 +1715,7 @@ def __grant_generate(grant, args['host'] = host if isinstance(ssl_option, list) and len(ssl_option): qry += __ssl_option_sanitize(ssl_option) - if salt.utils.is_true(grant_option): + if salt.utils.data.is_true(grant_option): qry += ' WITH GRANT OPTION' log.debug('Grant Query generated: {0} args {1}'.format(qry, repr(args))) return {'qry': qry, 'args': args} @@ -1903,7 +1903,7 @@ def grant_revoke(grant, grant = __grant_normalize(grant) - if salt.utils.is_true(grant_option): + if salt.utils.data.is_true(grant_option): grant += ', GRANT OPTION' db_part = database.rpartition('.') diff --git a/salt/modules/openbsdpkg.py b/salt/modules/openbsdpkg.py index bcf1123fea..b6e8d2478b 100644 --- a/salt/modules/openbsdpkg.py +++ b/salt/modules/openbsdpkg.py @@ -29,7 +29,6 @@ import re import logging # Import Salt libs -import salt.utils # Can be removed when is_true is moved import salt.utils.data import salt.utils.versions from salt.exceptions import CommandExecutionError, MinionError @@ -66,9 +65,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} diff --git a/salt/modules/opkg.py b/salt/modules/opkg.py index 2b583b3cf9..04fa9abfa7 100644 --- a/salt/modules/opkg.py +++ b/salt/modules/opkg.py @@ -24,7 +24,6 @@ import re import logging # Import salt libs -import salt.utils # Can be removed when is_true is moved import salt.utils.args import salt.utils.data import salt.utils.files @@ -82,7 +81,7 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version salt '*' pkg.latest_version ... ''' - refresh = salt.utils.is_true(kwargs.pop('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', True)) if len(names) == 0: return '' @@ -283,7 +282,7 @@ def install(name=None, {'': {'old': '', 'new': ''}} ''' - refreshdb = salt.utils.is_true(refresh) + refreshdb = salt.utils.data.is_true(refresh) try: pkg_params, pkg_type = __salt__['pkg_resource.parse_targets']( @@ -538,7 +537,7 @@ def upgrade(refresh=True): 'comment': '', } - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() old = list_pkgs() @@ -770,9 +769,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs salt '*' pkg.list_pkgs versions_as_list=True ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} @@ -815,7 +814,7 @@ def list_upgrades(refresh=True): salt '*' pkg.list_upgrades ''' ret = {} - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() cmd = ['opkg', 'list-upgradable'] diff --git a/salt/modules/pacman.py b/salt/modules/pacman.py index 757387160b..0ad735d937 100644 --- a/salt/modules/pacman.py +++ b/salt/modules/pacman.py @@ -18,7 +18,7 @@ import logging import os.path # Import salt libs -import salt.utils +import salt.utils # Can be removed once alias_function, fnmatch_multiple are moved import salt.utils.args import salt.utils.data import salt.utils.pkg @@ -68,7 +68,7 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version salt '*' pkg.latest_version ... ''' - refresh = salt.utils.is_true(kwargs.pop('refresh', False)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', False)) if len(names) == 0: return '' @@ -201,9 +201,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} @@ -517,8 +517,8 @@ def install(name=None, {'': {'old': '', 'new': ''}} ''' - refresh = salt.utils.is_true(refresh) - sysupgrade = salt.utils.is_true(sysupgrade) + refresh = salt.utils.data.is_true(refresh) + sysupgrade = salt.utils.data.is_true(sysupgrade) try: pkg_params, pkg_type = __salt__['pkg_resource.parse_targets']( @@ -678,7 +678,7 @@ def upgrade(refresh=False, root=None, **kwargs): and __salt__['config.get']('systemd.scope', True): cmd.extend(['systemd-run', '--scope']) cmd.extend(['pacman', '-Su', '--noprogressbar', '--noconfirm']) - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): cmd.append('-y') if root is not None: diff --git a/salt/modules/pecl.py b/salt/modules/pecl.py index 284ce74e49..e80b608a90 100644 --- a/salt/modules/pecl.py +++ b/salt/modules/pecl.py @@ -14,7 +14,7 @@ except ImportError: from pipes import quote as _cmd_quote # Import salt libs -import salt.utils +import salt.utils.data import salt.utils.path # Import 3rd-party libs @@ -42,7 +42,7 @@ def _pecl(command, defaults=False): Execute the command passed with pecl ''' cmdline = 'pecl {0}'.format(command) - if salt.utils.is_true(defaults): + if salt.utils.data.is_true(defaults): cmdline = 'yes ' "''" + ' | ' + cmdline ret = __salt__['cmd.run_all'](cmdline, python_shell=True) diff --git a/salt/modules/pkg_resource.py b/salt/modules/pkg_resource.py index d72829830b..a59f858cb7 100644 --- a/salt/modules/pkg_resource.py +++ b/salt/modules/pkg_resource.py @@ -16,7 +16,6 @@ import yaml from salt.ext import six # Import salt libs -import salt.utils # Can be removed once is_true is moved import salt.utils.data import salt.utils.versions from salt.exceptions import SaltInvocationError @@ -192,7 +191,7 @@ def version(*names, **kwargs): ''' ret = {} versions_as_list = \ - salt.utils.is_true(kwargs.pop('versions_as_list', False)) + salt.utils.data.is_true(kwargs.pop('versions_as_list', False)) pkg_glob = False if len(names) != 0: pkgs = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs) diff --git a/salt/modules/pkgin.py b/salt/modules/pkgin.py index fdd8ed4b8e..a85baacd82 100644 --- a/salt/modules/pkgin.py +++ b/salt/modules/pkgin.py @@ -17,7 +17,7 @@ import os import re # Import salt libs -import salt.utils # Can be removed when alias_function, is_true are moved +import salt.utils # Can be removed when alias_function is moved import salt.utils.data import salt.utils.path import salt.utils.pkg @@ -152,7 +152,7 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version ... ''' - refresh = salt.utils.is_true(kwargs.pop('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', True)) pkglist = {} pkgin = _check_pkgin() @@ -256,9 +256,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} diff --git a/salt/modules/pkgng.py b/salt/modules/pkgng.py index ceac214a80..026cf68e98 100644 --- a/salt/modules/pkgng.py +++ b/salt/modules/pkgng.py @@ -44,7 +44,7 @@ import logging import os # Import salt libs -import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils # Can be removed once alias_function is moved import salt.utils.data import salt.utils.files import salt.utils.itertools @@ -205,7 +205,7 @@ def version(*names, **kwargs): ''' with_origin = kwargs.pop('with_origin', False) ret = __salt__['pkg_resource.version'](*names, **kwargs) - if not salt.utils.is_true(with_origin): + if not salt.utils.data.is_true(with_origin): return ret # Put the return value back into a dict since we're adding a subdict if len(names) == 1: @@ -313,7 +313,7 @@ def latest_version(*names, **kwargs): cmd = _pkg(jail, chroot, root) + ['search', '-S', 'name', '-Q', 'version', '-e'] if quiet: cmd.append('-q') - if not salt.utils.is_true(refresh): + if not salt.utils.data.is_true(refresh): cmd.append('-U') cmd.append(name) @@ -385,11 +385,11 @@ def list_pkgs(versions_as_list=False, salt '*' pkg.list_pkgs chroot=/path/to/chroot ''' # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) contextkey_pkg = _contextkey(jail, chroot, root) contextkey_origins = _contextkey(jail, chroot, root, prefix='pkg.origin') @@ -397,7 +397,7 @@ def list_pkgs(versions_as_list=False, ret = copy.deepcopy(__context__[contextkey_pkg]) if not versions_as_list: __salt__['pkg_resource.stringify'](ret) - if salt.utils.is_true(with_origin): + if salt.utils.data.is_true(with_origin): origins = __context__.get(contextkey_origins, {}) return dict([ (x, {'origin': origins.get(x, ''), 'version': y}) @@ -427,7 +427,7 @@ def list_pkgs(versions_as_list=False, __context__[contextkey_origins] = origins if not versions_as_list: __salt__['pkg_resource.stringify'](ret) - if salt.utils.is_true(with_origin): + if salt.utils.data.is_true(with_origin): return dict([ (x, {'origin': origins.get(x, ''), 'version': y}) for x, y in six.iteritems(ret) @@ -811,23 +811,23 @@ def install(name=None, return {} opts = 'y' - if salt.utils.is_true(orphan): + if salt.utils.data.is_true(orphan): opts += 'A' - if salt.utils.is_true(force): + if salt.utils.data.is_true(force): opts += 'f' - if salt.utils.is_true(glob): + if salt.utils.data.is_true(glob): opts += 'g' - if salt.utils.is_true(local): + if salt.utils.data.is_true(local): opts += 'U' - if salt.utils.is_true(dryrun): + if salt.utils.data.is_true(dryrun): opts += 'n' - if salt.utils.is_true(quiet): + if salt.utils.data.is_true(quiet): opts += 'q' - if salt.utils.is_true(reinstall_requires): + if salt.utils.data.is_true(reinstall_requires): opts += 'R' - if salt.utils.is_true(regex): + if salt.utils.data.is_true(regex): opts += 'x' - if salt.utils.is_true(pcre): + if salt.utils.data.is_true(pcre): opts += 'X' old = list_pkgs(jail=jail, chroot=chroot, root=root) @@ -858,7 +858,7 @@ def install(name=None, cmd.append('-' + opts) cmd.extend(targets) - if pkg_cmd == 'add' and salt.utils.is_true(dryrun): + if pkg_cmd == 'add' and salt.utils.data.is_true(dryrun): # pkg add doesn't have a dryrun mode, so echo out what will be run return ' '.join(cmd) @@ -1014,21 +1014,21 @@ def remove(name=None, return {} opts = '' - if salt.utils.is_true(all_installed): + if salt.utils.data.is_true(all_installed): opts += 'a' - if salt.utils.is_true(force): + if salt.utils.data.is_true(force): opts += 'f' - if salt.utils.is_true(glob): + if salt.utils.data.is_true(glob): opts += 'g' - if salt.utils.is_true(dryrun): + if salt.utils.data.is_true(dryrun): opts += 'n' - if not salt.utils.is_true(dryrun): + if not salt.utils.data.is_true(dryrun): opts += 'y' - if salt.utils.is_true(recurse): + if salt.utils.data.is_true(recurse): opts += 'R' - if salt.utils.is_true(regex): + if salt.utils.data.is_true(regex): opts += 'x' - if salt.utils.is_true(pcre): + if salt.utils.data.is_true(pcre): opts += 'X' cmd = _pkg(jail, chroot, root) diff --git a/salt/modules/pkgutil.py b/salt/modules/pkgutil.py index 8c8bc95750..2367c40d23 100644 --- a/salt/modules/pkgutil.py +++ b/salt/modules/pkgutil.py @@ -14,7 +14,7 @@ from __future__ import absolute_import import copy # Import salt libs -import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils # Can be removed once alias_function is moved import salt.utils.data import salt.utils.pkg import salt.utils.versions @@ -84,7 +84,7 @@ def list_upgrades(refresh=True, **kwargs): # pylint: disable=W0613 salt '*' pkgutil.list_upgrades ''' - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() upgrades = {} lines = __salt__['cmd.run_stdout']( @@ -114,7 +114,7 @@ def upgrade(refresh=True): salt '*' pkgutil.upgrade ''' - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() old = list_pkgs() @@ -141,9 +141,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs salt '*' pkg.list_pkgs versions_as_list=True ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # 'removed' not yet implemented or not applicable - if salt.utils.is_true(kwargs.get('removed')): + if salt.utils.data.is_true(kwargs.get('removed')): return {} if 'pkg.list_pkgs' in __context__: @@ -204,7 +204,7 @@ def latest_version(*names, **kwargs): salt '*' pkgutil.latest_version CSWpython salt '*' pkgutil.latest_version ... ''' - refresh = salt.utils.is_true(kwargs.pop('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', True)) if not names: return '' diff --git a/salt/modules/pw_group.py b/salt/modules/pw_group.py index 143b3a1495..7f3e8beee1 100644 --- a/salt/modules/pw_group.py +++ b/salt/modules/pw_group.py @@ -14,8 +14,8 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils import salt.utils.args +import salt.utils.data log = logging.getLogger(__name__) @@ -51,7 +51,7 @@ def add(name, gid=None, **kwargs): salt '*' group.add foo 3456 ''' kwargs = salt.utils.args.clean_kwargs(**kwargs) - if salt.utils.is_true(kwargs.pop('system', False)): + if salt.utils.data.is_true(kwargs.pop('system', False)): log.warning('pw_group module does not support the \'system\' argument') if kwargs: log.warning('Invalid kwargs passed to group.add') diff --git a/salt/modules/pw_user.py b/salt/modules/pw_user.py index 3d1ca97856..f3362b65e8 100644 --- a/salt/modules/pw_user.py +++ b/salt/modules/pw_user.py @@ -47,8 +47,8 @@ except ImportError: from salt.ext import six # Import salt libs -import salt.utils # Can be removed once is_true is moved import salt.utils.args +import salt.utils.data import salt.utils.locales import salt.utils.user from salt.exceptions import CommandExecutionError @@ -144,7 +144,7 @@ def add(name, salt '*' user.add name ''' kwargs = salt.utils.args.clean_kwargs(**kwargs) - if salt.utils.is_true(kwargs.pop('system', False)): + if salt.utils.data.is_true(kwargs.pop('system', False)): log.warning('pw_user module does not support the \'system\' argument') if kwargs: log.warning('Invalid kwargs passed to user.add') @@ -166,7 +166,7 @@ def add(name, cmd.extend(['-L', loginclass]) if shell: cmd.extend(['-s', shell]) - if not salt.utils.is_true(unique): + if not salt.utils.data.is_true(unique): cmd.append('-o') gecos_field = _build_gecos({'fullname': fullname, 'roomnumber': roomnumber, @@ -187,7 +187,7 @@ def delete(name, remove=False, force=False): salt '*' user.delete name remove=True force=True ''' - if salt.utils.is_true(force): + if salt.utils.data.is_true(force): log.error('pw userdel does not support force-deleting user while ' 'user is logged in') cmd = ['pw', 'userdel'] diff --git a/salt/modules/saltutil.py b/salt/modules/saltutil.py index 869fb1ac39..3d8d55dee6 100644 --- a/salt/modules/saltutil.py +++ b/salt/modules/saltutil.py @@ -46,7 +46,7 @@ import salt.payload import salt.runner import salt.state import salt.transport -import salt.utils +import salt.utils # Can be removed once alias_function is moved import salt.utils.args import salt.utils.event import salt.utils.extmods @@ -988,7 +988,7 @@ def clear_cache(): salt '*' saltutil.clear_cache ''' - for root, dirs, files in salt.utils.safe_walk(__opts__['cachedir'], followlinks=False): + for root, dirs, files in salt.utils.files.safe_walk(__opts__['cachedir'], followlinks=False): for name in files: try: os.remove(os.path.join(root, name)) @@ -1014,7 +1014,7 @@ def clear_job_cache(hours=24): salt '*' saltutil.clear_job_cache hours=12 ''' threshold = time.time() - hours * 3600 - for root, dirs, files in salt.utils.safe_walk(os.path.join(__opts__['cachedir'], 'minion_jobs'), + for root, dirs, files in salt.utils.files.safe_walk(os.path.join(__opts__['cachedir'], 'minion_jobs'), followlinks=False): for name in dirs: try: diff --git a/salt/modules/shadow.py b/salt/modules/shadow.py index 785af50dd3..e93435b89d 100644 --- a/salt/modules/shadow.py +++ b/salt/modules/shadow.py @@ -19,7 +19,7 @@ except ImportError: pass # Import salt libs -import salt.utils # Can be removed when is_true is moved +import salt.utils.data import salt.utils.files import salt.utils.stringutils from salt.exceptions import CommandExecutionError @@ -278,7 +278,7 @@ def set_password(name, password, use_usermod=False): salt '*' shadow.set_password root '$1$UYCIxa628.9qXjpQCjM4a..' ''' - if not salt.utils.is_true(use_usermod): + if not salt.utils.data.is_true(use_usermod): # Edit the shadow file directly # ALT Linux uses tcb to store password hashes. More information found # in manpage (http://docs.altlinux.org/manpages/tcb.5.html) diff --git a/salt/modules/solaris_group.py b/salt/modules/solaris_group.py index 51392efbbb..623f429b77 100644 --- a/salt/modules/solaris_group.py +++ b/salt/modules/solaris_group.py @@ -14,7 +14,7 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils +import salt.utils.data log = logging.getLogger(__name__) @@ -49,7 +49,7 @@ def add(name, gid=None, **kwargs): salt '*' group.add foo 3456 ''' - if salt.utils.is_true(kwargs.pop('system', False)): + if salt.utils.data.is_true(kwargs.pop('system', False)): log.warning('solaris_group module does not support the \'system\' ' 'argument') if kwargs: diff --git a/salt/modules/solaris_user.py b/salt/modules/solaris_user.py index 276dcbe916..9db8b0b474 100644 --- a/salt/modules/solaris_user.py +++ b/salt/modules/solaris_user.py @@ -22,7 +22,7 @@ import copy import logging # Import salt libs -import salt.utils # Can be removed once is_true is moved +import salt.utils.data import salt.utils.user from salt.ext import six from salt.exceptions import CommandExecutionError @@ -112,7 +112,7 @@ def add(name, salt '*' user.add name ''' - if salt.utils.is_true(kwargs.pop('system', False)): + if salt.utils.data.is_true(kwargs.pop('system', False)): log.warning('solaris_user module does not support the \'system\' ' 'argument') if kwargs: @@ -169,7 +169,7 @@ def delete(name, remove=False, force=False): salt '*' user.delete name remove=True force=True ''' - if salt.utils.is_true(force): + if salt.utils.data.is_true(force): log.warning( 'userdel does not support force-deleting user while user is ' 'logged in' diff --git a/salt/modules/solarisips.py b/salt/modules/solarisips.py index 106f4d5f83..eb44512703 100644 --- a/salt/modules/solarisips.py +++ b/salt/modules/solarisips.py @@ -43,7 +43,7 @@ import logging # Import salt libs -import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils # Can be removed once alias_function is moved import salt.utils.data import salt.utils.path import salt.utils.pkg @@ -182,7 +182,7 @@ def list_upgrades(refresh=True, **kwargs): # pylint: disable=W0613 salt '*' pkg.list_upgrades salt '*' pkg.list_upgrades refresh=False ''' - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db(full=True) upgrades = {} # awk is in core-os package so we can use it without checking @@ -216,7 +216,7 @@ def upgrade(refresh=False, **kwargs): salt '*' pkg.upgrade ''' - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db() # Get a list of the packages before install so we can diff after to see @@ -257,7 +257,7 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs ''' # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} diff --git a/salt/modules/solarispkg.py b/salt/modules/solarispkg.py index e3928e8d3c..43da38c015 100644 --- a/salt/modules/solarispkg.py +++ b/salt/modules/solarispkg.py @@ -16,7 +16,7 @@ import os import logging # Import salt libs -import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils # Can be removed once alias_function is moved import salt.utils.data import salt.utils.files from salt.exceptions import CommandExecutionError, MinionError @@ -90,9 +90,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} @@ -317,7 +317,7 @@ def install(name=None, sources=None, saltenv='base', **kwargs): The ID declaration is ignored, as the package name is read from the ``sources`` parameter. ''' - if salt.utils.is_true(kwargs.get('refresh')): + if salt.utils.data.is_true(kwargs.get('refresh')): log.warning('\'refresh\' argument not implemented for solarispkg ' 'module') diff --git a/salt/modules/state.py b/salt/modules/state.py index e5616af7d7..050343dba2 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -28,10 +28,12 @@ import time import salt.config import salt.payload import salt.state -import salt.utils +import salt.utils # Can be removed once namespaced_function is moved +import salt.utils.args import salt.utils.data import salt.utils.event import salt.utils.files +import salt.utils.hashutils import salt.utils.jid import salt.utils.platform import salt.utils.url @@ -327,7 +329,7 @@ def _get_test_value(test=None, **kwargs): ''' ret = True if test is None: - if salt.utils.test_mode(test=test, **kwargs): + if salt.utils.args.test_mode(test=test, **kwargs): ret = True else: ret = __opts__.get('test', None) @@ -1775,7 +1777,7 @@ def pkg(pkg_path, # TODO - Add ability to download from salt master or other source if not os.path.isfile(pkg_path): return {} - if not salt.utils.get_hash(pkg_path, hash_type) == pkg_sum: + if not salt.utils.hashutils.get_hash(pkg_path, hash_type) == pkg_sum: return {} root = tempfile.mkdtemp() s_pkg = tarfile.open(pkg_path, 'r:gz') diff --git a/salt/modules/timezone.py b/salt/modules/timezone.py index d3f4b645b4..1f10dbde43 100644 --- a/salt/modules/timezone.py +++ b/salt/modules/timezone.py @@ -13,8 +13,8 @@ import re import string # Import salt libs -import salt.utils import salt.utils.files +import salt.utils.hashutils import salt.utils.itertools import salt.utils.path import salt.utils.platform @@ -118,7 +118,7 @@ def _get_zone_etc_localtime(): ) # Regular file. Try to match the hash. hash_type = __opts__.get('hash_type', 'md5') - tzfile_hash = salt.utils.get_hash(tzfile, hash_type) + tzfile_hash = salt.utils.hashutils.get_hash(tzfile, hash_type) # Not a link, just a copy of the tzdata file for root, dirs, files in os.walk(tzdir): for filename in files: @@ -127,7 +127,7 @@ def _get_zone_etc_localtime(): if olson_name[0] in string.ascii_lowercase: continue if tzfile_hash == \ - salt.utils.get_hash(full_path, hash_type): + salt.utils.hashutils.get_hash(full_path, hash_type): return olson_name raise CommandExecutionError('Unable to determine timezone') diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index ff23435800..3cae80a152 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -50,10 +50,10 @@ from salt.ext.six.moves.urllib.parse import urlparse as _urlparse from salt.exceptions import (CommandExecutionError, SaltInvocationError, SaltRenderError) -import salt.utils # Can be removed once is_true, get_hash are moved import salt.utils.args import salt.utils.data import salt.utils.files +import salt.utils.hashutils import salt.utils.pkg import salt.utils.platform import salt.utils.versions @@ -113,7 +113,7 @@ def latest_version(*names, **kwargs): saltenv = kwargs.get('saltenv', 'base') # Refresh before looking for the latest version available - refresh = salt.utils.is_true(kwargs.get('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.get('refresh', True)) # no need to call _refresh_db_conditional as list_pkgs will do it installed_pkgs = list_pkgs( @@ -190,7 +190,7 @@ def upgrade_available(name, **kwargs): saltenv = kwargs.get('saltenv', 'base') # Refresh before looking for the latest version available, # same default as latest_version - refresh = salt.utils.is_true(kwargs.get('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.get('refresh', True)) current = version(name, saltenv=saltenv, refresh=refresh).get(name) latest = latest_version(name, saltenv=saltenv, refresh=False) @@ -218,7 +218,7 @@ def list_upgrades(refresh=True, **kwargs): salt '*' pkg.list_upgrades ''' saltenv = kwargs.get('saltenv', 'base') - refresh = salt.utils.is_true(refresh) + refresh = salt.utils.data.is_true(refresh) _refresh_db_conditional(saltenv, force=refresh) installed_pkgs = list_pkgs(refresh=False, saltenv=saltenv) @@ -268,9 +268,9 @@ def list_available(*names, **kwargs): return '' saltenv = kwargs.get('saltenv', 'base') - refresh = salt.utils.is_true(kwargs.get('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.get('refresh', True)) return_dict_always = \ - salt.utils.is_true(kwargs.get('return_dict_always', False)) + salt.utils.data.is_true(kwargs.get('return_dict_always', False)) _refresh_db_conditional(saltenv, force=refresh) if len(names) == 1 and not return_dict_always: @@ -360,13 +360,13 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs salt '*' pkg.list_pkgs versions_as_list=True ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} saltenv = kwargs.get('saltenv', 'base') - refresh = salt.utils.is_true(kwargs.get('refresh', False)) + refresh = salt.utils.data.is_true(kwargs.get('refresh', False)) _refresh_db_conditional(saltenv, force=refresh) ret = {} @@ -488,8 +488,8 @@ def _refresh_db_conditional(saltenv, **kwargs): :codeauthor: Damon Atkins ''' - force = salt.utils.is_true(kwargs.pop('force', False)) - failhard = salt.utils.is_true(kwargs.pop('failhard', False)) + force = salt.utils.data.is_true(kwargs.pop('force', False)) + failhard = salt.utils.data.is_true(kwargs.pop('failhard', False)) expired_max = __opts__['winrepo_cache_expire_max'] expired_min = __opts__['winrepo_cache_expire_min'] @@ -570,8 +570,8 @@ def refresh_db(**kwargs): # Remove rtag file to keep multiple refreshes from happening in pkg states salt.utils.pkg.clear_rtag(__opts__) saltenv = kwargs.pop('saltenv', 'base') - verbose = salt.utils.is_true(kwargs.pop('verbose', False)) - failhard = salt.utils.is_true(kwargs.pop('failhard', True)) + verbose = salt.utils.data.is_true(kwargs.pop('verbose', False)) + failhard = salt.utils.data.is_true(kwargs.pop('failhard', True)) __context__.pop('winrepo.data', None) repo_details = _get_repo_details(saltenv) @@ -742,8 +742,8 @@ def genrepo(**kwargs): salt -G 'os:windows' pkg.genrepo saltenv=base ''' saltenv = kwargs.pop('saltenv', 'base') - verbose = salt.utils.is_true(kwargs.pop('verbose', False)) - failhard = salt.utils.is_true(kwargs.pop('failhard', True)) + verbose = salt.utils.data.is_true(kwargs.pop('verbose', False)) + failhard = salt.utils.data.is_true(kwargs.pop('failhard', True)) ret = {} successful_verbose = {} @@ -1079,7 +1079,7 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): ret = {} saltenv = kwargs.pop('saltenv', 'base') - refresh = salt.utils.is_true(refresh) + refresh = salt.utils.data.is_true(refresh) # no need to call _refresh_db_conditional as list_pkgs will do it # Make sure name or pkgs is passed @@ -1251,8 +1251,8 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): log.debug('Source {0} hash: {1}'.format(source_sum['hash_type'], source_sum['hsum'])) - cached_pkg_sum = salt.utils.get_hash(cached_pkg, - source_sum['hash_type']) + cached_pkg_sum = salt.utils.hashutils.get_hash(cached_pkg, + source_sum['hash_type']) log.debug('Package {0} hash: {1}'.format(source_sum['hash_type'], cached_pkg_sum)) @@ -1419,11 +1419,11 @@ def upgrade(**kwargs): salt '*' pkg.upgrade ''' log.warning('pkg.upgrade not implemented on Windows yet') - refresh = salt.utils.is_true(kwargs.get('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.get('refresh', True)) saltenv = kwargs.get('saltenv', 'base') # Uncomment the below once pkg.upgrade has been implemented - # if salt.utils.is_true(refresh): + # if salt.utils.data.is_true(refresh): # refresh_db() return {} @@ -1472,7 +1472,7 @@ def remove(name=None, pkgs=None, version=None, **kwargs): salt '*' pkg.remove pkgs='["foo", "bar"]' ''' saltenv = kwargs.get('saltenv', 'base') - refresh = salt.utils.is_true(kwargs.get('refresh', False)) + refresh = salt.utils.data.is_true(kwargs.get('refresh', False)) # no need to call _refresh_db_conditional as list_pkgs will do it ret = {} diff --git a/salt/modules/xbpspkg.py b/salt/modules/xbpspkg.py index 33e80be532..fd079dd79d 100644 --- a/salt/modules/xbpspkg.py +++ b/salt/modules/xbpspkg.py @@ -16,7 +16,6 @@ import logging import glob # Import salt libs -import salt.utils # Can be removed when is_true is moved import salt.utils.data import salt.utils.files import salt.utils.path @@ -91,9 +90,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} cmd = 'xbps-query -l' @@ -135,7 +134,7 @@ def list_upgrades(refresh=True): # fuse-2.9.4_4 update i686 http://repo.voidlinux.eu/current 298133 91688 # xtools-0.34_1 update noarch http://repo.voidlinux.eu/current 21424 10752 - refresh = salt.utils.is_true(refresh) + refresh = salt.utils.data.is_true(refresh) # Refresh repo index before checking for latest version available if refresh: @@ -196,7 +195,7 @@ def latest_version(*names, **kwargs): # xtools-0.34_1 update noarch http://repo.voidlinux.eu/current 21424 10752 # Package 'vim' is up to date. - refresh = salt.utils.is_true(kwargs.pop('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', True)) if len(names) == 0: return '' diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index 4d30a5a785..f6383343b8 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -41,7 +41,7 @@ from salt.ext.six.moves import configparser # pylint: enable=import-error,redefined-builtin # Import Salt libs -import salt.utils # Can be removed once alias_function, is_true, and fnmatch_multiple are moved +import salt.utils # Can be removed once alias_function, fnmatch_multiple are moved import salt.utils.args import salt.utils.data import salt.utils.decorators.path @@ -443,7 +443,7 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version disableexcludes=main salt '*' pkg.latest_version ... ''' - refresh = salt.utils.is_true(kwargs.pop('refresh', True)) + refresh = salt.utils.data.is_true(kwargs.pop('refresh', True)) if len(names) == 0: return '' @@ -631,9 +631,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs salt '*' pkg.list_pkgs attr='["version", "arch"]' ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} @@ -949,7 +949,7 @@ def list_upgrades(refresh=True, **kwargs): repo_arg = _get_repo_options(**kwargs) exclude_arg = _get_excludes_option(**kwargs) - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db(check_update=False, **kwargs) cmd = [_yum(), '--quiet'] @@ -1323,9 +1323,9 @@ def install(name=None, exclude_arg = _get_excludes_option(**kwargs) branch_arg = _get_branch_option(**kwargs) - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db(**kwargs) - reinstall = salt.utils.is_true(reinstall) + reinstall = salt.utils.data.is_true(reinstall) try: pkg_params, pkg_type = __salt__['pkg_resource.parse_targets']( @@ -1833,7 +1833,7 @@ def upgrade(name=None, branch_arg = _get_branch_option(**kwargs) extra_args = _get_extra_options(**kwargs) - if salt.utils.is_true(refresh): + if salt.utils.data.is_true(refresh): refresh_db(**kwargs) old = list_pkgs() diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py index 0e0a72f7d4..4dfbeabaae 100644 --- a/salt/modules/zypper.py +++ b/salt/modules/zypper.py @@ -32,7 +32,7 @@ from xml.dom import minidom as dom from xml.parsers.expat import ExpatError # Import salt libs -import salt.utils # Can be removed once alias_function, is_true are moved +import salt.utils # Can be removed once alias_function is moved import salt.utils.data import salt.utils.event import salt.utils.files @@ -687,9 +687,9 @@ def list_pkgs(versions_as_list=False, **kwargs): salt '*' pkg.list_pkgs salt '*' pkg.list_pkgs attr='["version", "arch"]' ''' - versions_as_list = salt.utils.is_true(versions_as_list) + versions_as_list = salt.utils.data.is_true(versions_as_list) # not yet implemented or not applicable - if any([salt.utils.is_true(kwargs.get(x)) + if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]): return {} diff --git a/salt/pillar/s3.py b/salt/pillar/s3.py index e65dfd8b36..a0183f5723 100644 --- a/salt/pillar/s3.py +++ b/salt/pillar/s3.py @@ -105,8 +105,8 @@ from salt.ext.six.moves.urllib.parse import quote as _quote # Import salt libs from salt.pillar import Pillar -import salt.utils import salt.utils.files +import salt.utils.hashutils # Set up logging log = logging.getLogger(__name__) @@ -403,7 +403,7 @@ def _get_file_from_s3(creds, metadata, saltenv, bucket, path, file_md5 = "".join(list(filter(str.isalnum, file_meta['ETag']))) \ if file_meta else None - cached_md5 = salt.utils.get_hash(cached_file_path, 'md5') + cached_md5 = salt.utils.hashutils.get_hash(cached_file_path, 'md5') log.debug("Cached file: path={0}, md5={1}, etag={2}".format(cached_file_path, cached_md5, file_md5)) diff --git a/salt/runners/cache.py b/salt/runners/cache.py index a821d2d7a6..459ff325ea 100644 --- a/salt/runners/cache.py +++ b/salt/runners/cache.py @@ -12,8 +12,8 @@ import os import salt.config from salt.ext import six import salt.log -import salt.utils import salt.utils.args +import salt.utils.gitfs import salt.utils.master import salt.utils.versions import salt.payload @@ -323,7 +323,7 @@ def clear_git_lock(role, remote=None, **kwargs): salt-run cache.clear_git_lock git_pillar ''' kwargs = salt.utils.args.clean_kwargs(**kwargs) - type_ = salt.utils.split_input(kwargs.pop('type', ['update', 'checkout'])) + type_ = salt.utils.args.split_input(kwargs.pop('type', ['update', 'checkout'])) if kwargs: salt.utils.args.invalid_kwargs(kwargs) diff --git a/salt/runners/jobs.py b/salt/runners/jobs.py index 605839eee4..0ce3565bf3 100644 --- a/salt/runners/jobs.py +++ b/salt/runners/jobs.py @@ -12,7 +12,7 @@ import os # Import salt libs import salt.client import salt.payload -import salt.utils +import salt.utils.args import salt.utils.files import salt.utils.jid import salt.minion @@ -326,14 +326,14 @@ def list_jobs(ext_source=None, if isinstance(targets, six.string_types): targets = [targets] for target in targets: - for key in salt.utils.split_input(search_target): + for key in salt.utils.args.split_input(search_target): if fnmatch.fnmatch(target, key): _match = True if search_function and _match: _match = False if 'Function' in ret[item]: - for key in salt.utils.split_input(search_function): + for key in salt.utils.args.split_input(search_function): if fnmatch.fnmatch(ret[item]['Function'], key): _match = True diff --git a/salt/states/aptpkg.py b/salt/states/aptpkg.py index f72be3f57c..5264649e26 100644 --- a/salt/states/aptpkg.py +++ b/salt/states/aptpkg.py @@ -9,7 +9,7 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils +import salt.utils.data log = logging.getLogger(__name__) @@ -40,7 +40,7 @@ def held(name): ) if not state: ret.update(comment='Package {0} does not have a state'.format(name)) - elif not salt.utils.is_true(state.get('hold', False)): + elif not salt.utils.data.is_true(state.get('hold', False)): if not __opts__['test']: result = __salt__['pkg.set_selections']( selection={'hold': [name]} diff --git a/salt/states/archive.py b/salt/states/archive.py index 8bea1e17b2..65ee7550db 100644 --- a/salt/states/archive.py +++ b/salt/states/archive.py @@ -22,9 +22,9 @@ from salt.ext.six.moves import shlex_quote as _cmd_quote from salt.ext.six.moves.urllib.parse import urlparse as _urlparse # pylint: disable=no-name-in-module # Import Salt libs -import salt.utils import salt.utils.args import salt.utils.files +import salt.utils.hashutils import salt.utils.path import salt.utils.platform import salt.utils.url @@ -60,7 +60,7 @@ def _add_explanation(ret, source_hash_trigger, contents_missing): def _gen_checksum(path): - return {'hsum': salt.utils.get_hash(path, form=__opts__['hash_type']), + return {'hsum': salt.utils.hashutils.get_hash(path, form=__opts__['hash_type']), 'hash_type': __opts__['hash_type']} diff --git a/salt/states/file.py b/salt/states/file.py index 7724acf5c4..06011f3fb9 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -285,6 +285,7 @@ import salt.payload import salt.utils import salt.utils.dictupdate import salt.utils.files +import salt.utils.hashutils import salt.utils.platform import salt.utils.templates import salt.utils.url @@ -902,8 +903,8 @@ def _check_dir_meta(name, and group != stats.get('gid')): changes['group'] = group # Normalize the dir mode - smode = salt.utils.normalize_mode(stats['mode']) - mode = salt.utils.normalize_mode(mode) + smode = salt.utils.files.normalize_mode(stats['mode']) + mode = salt.utils.files.normalize_mode(mode) if mode is not None and mode != smode: changes['mode'] = mode return changes @@ -1256,7 +1257,7 @@ def symlink( name = os.path.expanduser(name) # Make sure that leading zeros stripped by YAML loader are added back - mode = salt.utils.normalize_mode(mode) + mode = salt.utils.files.normalize_mode(mode) user = _test_owner(kwargs, user=user) ret = {'name': name, @@ -2121,7 +2122,7 @@ def managed(name, keep_mode = False # Make sure that any leading zeros stripped by YAML loader are added back - mode = salt.utils.normalize_mode(mode) + mode = salt.utils.files.normalize_mode(mode) contents_count = len( [x for x in (contents, contents_pillar, contents_grains) @@ -2814,8 +2815,8 @@ def directory(name, file_mode = dir_mode # Make sure that leading zeros stripped by YAML loader are added back - dir_mode = salt.utils.normalize_mode(dir_mode) - file_mode = salt.utils.normalize_mode(file_mode) + dir_mode = salt.utils.files.normalize_mode(dir_mode) + file_mode = salt.utils.files.normalize_mode(file_mode) if salt.utils.platform.is_windows(): # Verify win_owner is valid on the target system @@ -3268,7 +3269,7 @@ def recurse(name, return _error(ret, 'mode management is not supported on Windows') # Make sure that leading zeros stripped by YAML loader are added back - dir_mode = salt.utils.normalize_mode(dir_mode) + dir_mode = salt.utils.files.normalize_mode(dir_mode) try: keep_mode = file_mode.lower() == 'keep' @@ -3278,7 +3279,7 @@ def recurse(name, except AttributeError: keep_mode = False - file_mode = salt.utils.normalize_mode(file_mode) + file_mode = salt.utils.files.normalize_mode(file_mode) u_check = _check_user(user, group) if u_check: @@ -4385,7 +4386,7 @@ def comment(name, regex, char='#', backup='.bak'): ret['result'] = __salt__['file.search'](name, unanchor_regex, multiline=True) if slines != nlines: - if not __utils__['files.is_text_file'](name): + if not __utils__['files.is_text'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4497,7 +4498,7 @@ def uncomment(name, regex, char='#', backup='.bak'): ) if slines != nlines: - if not __utils__['files.is_text_file'](name): + if not __utils__['files.is_text'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4740,7 +4741,7 @@ def append(name, nlines = list(slines) nlines.extend(append_lines) if slines != nlines: - if not __utils__['files.is_text_file'](name): + if not __utils__['files.is_text'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4765,7 +4766,7 @@ def append(name, nlines = nlines.splitlines() if slines != nlines: - if not __utils__['files.is_text_file'](name): + if not __utils__['files.is_text'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4933,7 +4934,7 @@ def prepend(name, if __opts__['test']: nlines = test_lines + slines if slines != nlines: - if not __utils__['files.is_text_file'](name): + if not __utils__['files.is_text'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -4976,7 +4977,7 @@ def prepend(name, nlines = nlines.splitlines(True) if slines != nlines: - if not __utils__['files.is_text_file'](name): + if not __utils__['files.is_text'](name): ret['changes']['diff'] = 'Replace binary file' else: # Changes happened, add them @@ -5314,8 +5315,8 @@ def copy( if os.path.lexists(source) and os.path.lexists(name): # if this is a file which did not change, do not update if force and os.path.isfile(name): - hash1 = salt.utils.get_hash(name) - hash2 = salt.utils.get_hash(source) + hash1 = salt.utils.hashutils.get_hash(name) + hash2 = salt.utils.hashutils.get_hash(source) if hash1 == hash2: changed = True ret['comment'] = ' '.join([ret['comment'], '- files are identical but force flag is set']) @@ -5789,7 +5790,7 @@ def serialize(name, contents += '\n' # Make sure that any leading zeros stripped by YAML loader are added back - mode = salt.utils.normalize_mode(mode) + mode = salt.utils.files.normalize_mode(mode) if __opts__['test']: ret['changes'] = __salt__['file.check_managed_changes']( @@ -6653,7 +6654,7 @@ def cached(name, # it cause any trouble, and just return True. return True try: - return salt.utils.get_hash(path, form=form) != checksum + return salt.utils.hashutils.get_hash(path, form=form) != checksum except (IOError, OSError, ValueError): # Again, shouldn't happen, but don't let invalid input/permissions # in the call to get_hash blow this up. diff --git a/salt/states/mysql_user.py b/salt/states/mysql_user.py index 0a14b83af9..adcf81764f 100644 --- a/salt/states/mysql_user.py +++ b/salt/states/mysql_user.py @@ -46,7 +46,7 @@ from __future__ import absolute_import import sys # Import salt libs -import salt.utils +import salt.utils.data def __virtual__(): @@ -121,7 +121,7 @@ def present(name, # check if user exists with the same password (or passwordless login) if passwordless: - if not salt.utils.is_true(allow_passwordless): + if not salt.utils.data.is_true(allow_passwordless): ret['comment'] = 'Either password or password_hash must be ' \ 'specified, unless allow_passwordless is True' ret['result'] = False @@ -161,7 +161,7 @@ def present(name, ret['result'] = None if passwordless: ret['comment'] += 'cleared' - if not salt.utils.is_true(allow_passwordless): + if not salt.utils.data.is_true(allow_passwordless): ret['comment'] += ', but allow_passwordless != True' ret['result'] = False else: @@ -185,7 +185,7 @@ def present(name, err = _get_mysql_error() if err is not None: ret['comment'] += ' ({0})'.format(err) - if passwordless and not salt.utils.is_true(allow_passwordless): + if passwordless and not salt.utils.data.is_true(allow_passwordless): ret['comment'] += '. Note: allow_passwordless must be True ' \ 'to permit passwordless login.' ret['result'] = False @@ -204,7 +204,7 @@ def present(name, ret['result'] = None if passwordless: ret['comment'] += ' with passwordless login' - if not salt.utils.is_true(allow_passwordless): + if not salt.utils.data.is_true(allow_passwordless): ret['comment'] += ', but allow_passwordless != True' ret['result'] = False return ret diff --git a/salt/states/pkgrepo.py b/salt/states/pkgrepo.py index dd19a30bb3..e7efca0dc1 100644 --- a/salt/states/pkgrepo.py +++ b/salt/states/pkgrepo.py @@ -93,7 +93,7 @@ import sys from salt.exceptions import CommandExecutionError, SaltInvocationError from salt.modules.aptpkg import _strip_uri from salt.state import STATE_INTERNAL_KEYWORDS as _STATE_INTERNAL_KEYWORDS -import salt.utils +import salt.utils.data import salt.utils.files import salt.utils.pkg.deb import salt.utils.pkg.rpm @@ -361,9 +361,9 @@ def managed(name, ppa=None, **kwargs): except TypeError: repo = ':'.join(('ppa', str(ppa))) - kwargs['disabled'] = not salt.utils.is_true(enabled) \ + kwargs['disabled'] = not salt.utils.data.is_true(enabled) \ if enabled is not None \ - else salt.utils.is_true(disabled) + else salt.utils.data.is_true(disabled) elif os_family in ('redhat', 'suse'): if 'humanname' in kwargs: @@ -372,15 +372,15 @@ def managed(name, ppa=None, **kwargs): # Fall back to the repo name if humanname not provided kwargs['name'] = repo - kwargs['enabled'] = not salt.utils.is_true(disabled) \ + kwargs['enabled'] = not salt.utils.data.is_true(disabled) \ if disabled is not None \ - else salt.utils.is_true(enabled) + else salt.utils.data.is_true(enabled) elif os_family == 'nilinuxrt': # opkg is the pkg virtual - kwargs['enabled'] = not salt.utils.is_true(disabled) \ + kwargs['enabled'] = not salt.utils.data.is_true(disabled) \ if disabled is not None \ - else salt.utils.is_true(enabled) + else salt.utils.data.is_true(enabled) for kwarg in _STATE_INTERNAL_KEYWORDS: kwargs.pop(kwarg, None) @@ -417,7 +417,7 @@ def managed(name, ppa=None, **kwargs): # if it's desired to be enabled and the 'enabled' key is # missing from the repo definition if os_family == 'redhat': - if not salt.utils.is_true(sanitizedkwargs[kwarg]): + if not salt.utils.data.is_true(sanitizedkwargs[kwarg]): break else: break @@ -452,8 +452,8 @@ def managed(name, ppa=None, **kwargs): and any(isinstance(x, bool) for x in (sanitizedkwargs[kwarg], pre[kwarg])): # This check disambiguates 1/0 from True/False - if salt.utils.is_true(sanitizedkwargs[kwarg]) != \ - salt.utils.is_true(pre[kwarg]): + if salt.utils.data.is_true(sanitizedkwargs[kwarg]) != \ + salt.utils.data.is_true(pre[kwarg]): break else: if str(sanitizedkwargs[kwarg]) != str(pre[kwarg]): diff --git a/salt/states/service.py b/salt/states/service.py index 0b0850d9a3..3b0b3318d5 100644 --- a/salt/states/service.py +++ b/salt/states/service.py @@ -62,7 +62,7 @@ from __future__ import absolute_import import time # Import Salt libs -import salt.utils +import salt.utils.data import salt.utils.platform from salt.utils.args import get_function_argspec as _argspec from salt.exceptions import CommandExecutionError @@ -395,7 +395,7 @@ def running(name, # Convert enable to boolean in case user passed a string value if isinstance(enable, six.string_types): - enable = salt.utils.is_true(enable) + enable = salt.utils.data.is_true(enable) # Check if the service is available try: @@ -534,7 +534,7 @@ def dead(name, # Convert enable to boolean in case user passed a string value if isinstance(enable, six.string_types): - enable = salt.utils.is_true(enable) + enable = salt.utils.data.is_true(enable) # Check if the service is available try: diff --git a/salt/states/win_wua.py b/salt/states/win_wua.py index 798853d5ca..8f60934595 100644 --- a/salt/states/win_wua.py +++ b/salt/states/win_wua.py @@ -55,7 +55,7 @@ import logging # Import Salt libs from salt.ext import six -import salt.utils +import salt.utils.data import salt.utils.platform import salt.utils.win_update @@ -151,14 +151,14 @@ def installed(name, updates=None): # List of updates to download download = salt.utils.win_update.Updates() for item in install_list.updates: - if not salt.utils.is_true(item.IsDownloaded): + if not salt.utils.data.is_true(item.IsDownloaded): download.updates.Add(item) # List of updates to install install = salt.utils.win_update.Updates() installed_updates = [] for item in install_list.updates: - if not salt.utils.is_true(item.IsInstalled): + if not salt.utils.data.is_true(item.IsInstalled): install.updates.Add(item) else: installed_updates.extend('KB' + kb for kb in item.KBArticleIDs) @@ -190,7 +190,7 @@ def installed(name, updates=None): # Verify the installation for item in install.list(): - if not salt.utils.is_true(post_info[item]['Installed']): + if not salt.utils.data.is_true(post_info[item]['Installed']): ret['changes']['failed'] = { item: {'Title': post_info[item]['Title'][:40] + '...', 'KBs': post_info[item]['KBs']} @@ -285,7 +285,7 @@ def removed(name, updates=None): uninstall = salt.utils.win_update.Updates() removed_updates = [] for item in updates.updates: - if salt.utils.is_true(item.IsInstalled): + if salt.utils.data.is_true(item.IsInstalled): uninstall.updates.Add(item) else: removed_updates.extend('KB' + kb for kb in item.KBArticleIDs) @@ -314,7 +314,7 @@ def removed(name, updates=None): # Verify the installation for item in uninstall.list(): - if salt.utils.is_true(post_info[item]['Installed']): + if salt.utils.data.is_true(post_info[item]['Installed']): ret['changes']['failed'] = { item: {'Title': post_info[item]['Title'][:40] + '...', 'KBs': post_info[item]['KBs']} @@ -451,13 +451,13 @@ def uptodate(name, # List of updates to download download = salt.utils.win_update.Updates() for item in install_list.updates: - if not salt.utils.is_true(item.IsDownloaded): + if not salt.utils.data.is_true(item.IsDownloaded): download.updates.Add(item) # List of updates to install install = salt.utils.win_update.Updates() for item in install_list.updates: - if not salt.utils.is_true(item.IsInstalled): + if not salt.utils.data.is_true(item.IsInstalled): install.updates.Add(item) # Return comment of changes if test. @@ -483,7 +483,7 @@ def uptodate(name, # Verify the installation for item in install.list(): - if not salt.utils.is_true(post_info[item]['Installed']): + if not salt.utils.data.is_true(post_info[item]['Installed']): ret['changes']['failed'] = { item: {'Title': post_info[item]['Title'][:40] + '...', 'KBs': post_info[item]['KBs']} diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 85da33b01c..0fa4232bbf 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -235,22 +235,6 @@ def output_profile(pr, stats_path='/tmp/stats', stop=False, id_=None): return pr -@jinja_filter('list_files') -def list_files(directory): - ''' - Return a list of all files found under directory - ''' - ret = set() - ret.add(directory) - for root, dirs, files in safe_walk(directory): - for name in files: - ret.add(os.path.join(root, name)) - for name in dirs: - ret.add(os.path.join(root, name)) - - return list(ret) - - def required_module_list(docstring=None): ''' Return a list of python modules required by a salt module that aren't @@ -346,35 +330,6 @@ def backup_minion(path, bkroot): os.chmod(bkpath, fstat.st_mode) -def pem_finger(path=None, key=None, sum_type='sha256'): - ''' - Pass in either a raw pem string, or the path on disk to the location of a - pem file, and the type of cryptographic hash to use. The default is SHA256. - The fingerprint of the pem will be returned. - - If neither a key nor a path are passed in, a blank string will be returned. - ''' - # Late import to avoid circular import. - import salt.utils.files - - if not key: - if not os.path.isfile(path): - return '' - - with salt.utils.files.fopen(path, 'rb') as fp_: - key = b''.join([x for x in fp_.readlines() if x.strip()][1:-1]) - - pre = getattr(hashlib, sum_type)(key).hexdigest() - finger = '' - for ind in range(len(pre)): - if ind % 2: - # Is odd - finger += '{0}:'.format(pre[ind]) - else: - finger += pre[ind] - return finger.rstrip(':') - - def build_whitespace_split_regex(text): ''' Create a regular expression at runtime which should match ignoring the @@ -565,21 +520,6 @@ def format_call(fun, return ret -@jinja_filter('sorted_ignorecase') -def isorted(to_sort): - ''' - Sort a list of strings ignoring case. - - >>> L = ['foo', 'Foo', 'bar', 'Bar'] - >>> sorted(L) - ['Bar', 'Foo', 'bar', 'foo'] - >>> sorted(L, key=lambda x: x.lower()) - ['bar', 'Bar', 'foo', 'Foo'] - >>> - ''' - return sorted(to_sort, key=lambda x: x.lower()) - - @jinja_filter('mysql_to_dict') def mysql_to_dict(data, key): ''' @@ -752,79 +692,6 @@ def check_include_exclude(path_str, include_pat=None, exclude_pat=None): return ret -def st_mode_to_octal(mode): - ''' - Convert the st_mode value from a stat(2) call (as returned from os.stat()) - to an octal mode. - ''' - try: - return oct(mode)[-4:] - except (TypeError, IndexError): - return '' - - -def normalize_mode(mode): - ''' - Return a mode value, normalized to a string and containing a leading zero - if it does not have one. - - Allow "keep" as a valid mode (used by file state/module to preserve mode - from the Salt fileserver in file states). - ''' - if mode is None: - return None - if not isinstance(mode, six.string_types): - mode = str(mode) - if six.PY3: - mode = mode.replace('0o', '0') - # Strip any quotes any initial zeroes, then though zero-pad it up to 4. - # This ensures that somethign like '00644' is normalized to '0644' - return mode.strip('"').strip('\'').lstrip('0').zfill(4) - - -def test_mode(**kwargs): - ''' - Examines the kwargs passed and returns True if any kwarg which matching - "Test" in any variation on capitalization (i.e. "TEST", "Test", "TeSt", - etc) contains a True value (as determined by salt.utils.is_true). - ''' - for arg, value in six.iteritems(kwargs): - try: - if arg.lower() == 'test' and is_true(value): - return True - except AttributeError: - continue - return False - - -def is_true(value=None): - ''' - Returns a boolean value representing the "truth" of the value passed. The - rules for what is a "True" value are: - - 1. Integer/float values greater than 0 - 2. The string values "True" and "true" - 3. Any object for which bool(obj) returns True - ''' - # First, try int/float conversion - try: - value = int(value) - except (ValueError, TypeError): - pass - try: - value = float(value) - except (ValueError, TypeError): - pass - - # Now check for truthiness - if isinstance(value, (six.integer_types, float)): - return value > 0 - elif isinstance(value, six.string_types): - return str(value).lower() == 'true' - else: - return bool(value) - - def option(value, default='', opts=None, pillar=None): ''' Pass in a generic option and receive the value that will be assigned @@ -872,83 +739,6 @@ def print_cli(msg, retries=10, step=0.01): break -def safe_walk(top, topdown=True, onerror=None, followlinks=True, _seen=None): - ''' - A clone of the python os.walk function with some checks for recursive - symlinks. Unlike os.walk this follows symlinks by default. - ''' - islink, join, isdir = os.path.islink, os.path.join, os.path.isdir - if _seen is None: - _seen = set() - - # We may not have read permission for top, in which case we can't - # get a list of the files the directory contains. os.path.walk - # always suppressed the exception then, rather than blow up for a - # minor reason when (say) a thousand readable directories are still - # left to visit. That logic is copied here. - try: - # Note that listdir and error are globals in this module due - # to earlier import-*. - names = os.listdir(top) - except os.error as err: - if onerror is not None: - onerror(err) - return - - if followlinks: - status = os.stat(top) - # st_ino is always 0 on some filesystems (FAT, NTFS); ignore them - if status.st_ino != 0: - node = (status.st_dev, status.st_ino) - if node in _seen: - return - _seen.add(node) - - dirs, nondirs = [], [] - for name in names: - full_path = join(top, name) - if isdir(full_path): - dirs.append(name) - else: - nondirs.append(name) - - if topdown: - yield top, dirs, nondirs - for name in dirs: - new_path = join(top, name) - if followlinks or not islink(new_path): - for x in safe_walk(new_path, topdown, onerror, followlinks, _seen): - yield x - if not topdown: - yield top, dirs, nondirs - - -@jinja_filter('file_hashsum') -def get_hash(path, form='sha256', chunk_size=65536): - ''' - Get the hash sum of a file - - This is better than ``get_sum`` for the following reasons: - - It does not read the entire file into memory. - - It does not return a string on error. The returned value of - ``get_sum`` cannot really be trusted since it is vulnerable to - collisions: ``get_sum(..., 'xyz') == 'Hash xyz not supported'`` - ''' - # Late import to avoid circular import. - import salt.utils.files - - hash_type = hasattr(hashlib, form) and getattr(hashlib, form) or None - if hash_type is None: - raise ValueError('Invalid hash type: {0}'.format(form)) - - with salt.utils.files.fopen(path, 'rb') as ifile: - hash_obj = hash_type() - # read the file in in chunks, not the entire file - for chunk in iter(lambda: ifile.read(chunk_size), b''): - hash_obj.update(chunk) - return hash_obj.hexdigest() - - def namespaced_function(function, global_dict, defaults=None, preserve_context=False): ''' Redefine (clone) a function under a different globals() namespace scope @@ -1094,31 +884,6 @@ def find_json(raw): raise ValueError -@jinja_filter('is_bin_file') -def is_bin_file(path): - ''' - Detects if the file is a binary, returns bool. Returns True if the file is - a bin, False if the file is not and None if the file is not available. - ''' - # Late import to avoid circular import. - import salt.utils.files - import salt.utils.stringutils - - if not os.path.isfile(path): - return False - try: - with salt.utils.files.fopen(path, 'rb') as fp_: - try: - data = fp_.read(2048) - if six.PY3: - data = data.decode(__salt_system_encoding__) - return salt.utils.stringutils.is_binary(data) - except UnicodeDecodeError: - return True - except os.error: - return False - - def total_seconds(td): ''' Takes a timedelta and returns the total number of seconds @@ -1141,67 +906,6 @@ def import_json(): continue -def human_size_to_bytes(human_size): - ''' - Convert human-readable units to bytes - ''' - size_exp_map = {'K': 1, 'M': 2, 'G': 3, 'T': 4, 'P': 5} - human_size_str = str(human_size) - match = re.match(r'^(\d+)([KMGTP])?$', human_size_str) - if not match: - raise ValueError( - 'Size must be all digits, with an optional unit type ' - '(K, M, G, T, or P)' - ) - size_num = int(match.group(1)) - unit_multiplier = 1024 ** size_exp_map.get(match.group(2), 0) - return size_num * unit_multiplier - - -@jinja_filter('is_list') -def is_list(value): - ''' - Check if a variable is a list. - ''' - return isinstance(value, list) - - -@jinja_filter('is_iter') -def is_iter(y, ignore=six.string_types): - ''' - Test if an object is iterable, but not a string type. - - Test if an object is an iterator or is iterable itself. By default this - does not return True for string objects. - - The `ignore` argument defaults to a list of string types that are not - considered iterable. This can be used to also exclude things like - dictionaries or named tuples. - - Based on https://bitbucket.org/petershinners/yter - ''' - - if ignore and isinstance(y, ignore): - return False - try: - iter(y) - return True - except TypeError: - return False - - -def split_input(val): - ''' - Take an input value and split it into a list, returning the resulting list - ''' - if isinstance(val, list): - return val - try: - return [x.strip() for x in val.split(',')] - except AttributeError: - return [x.strip() for x in str(val).split(',')] - - def simple_types_filter(data): ''' Convert the data list, dictionary into simple types, i.e., int, float, string, @@ -1317,6 +1021,19 @@ def reinit_crypto(): return salt.utils.crypt.reinit_crypto() +def pem_finger(path=None, key=None, sum_type='sha256'): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.crypt + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.pem_finger\' detected. This function has been ' + 'moved to \'salt.utils.crypt.pem_finger\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.crypt.pem_finger(path, key, sum_type) + + def to_bytes(s, encoding=None): # Late import to avoid circular import. import salt.utils.versions @@ -1512,6 +1229,32 @@ def argspec_report(functions, module=''): return salt.utils.args.argspec_report(functions, module=module) +def split_input(val): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.args + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.split_input\' detected. This function has been ' + 'moved to \'salt.utils.args.split_input\' as of Salt Oxygen. This ' + 'warning will be removed in Salt Neon.' + ) + return salt.utils.args.split_input(val) + + +def test_mode(**kwargs): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.args + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.test_mode\' detected. This function has been ' + 'moved to \'salt.utils.args.test_mode\' as of Salt Oxygen. This ' + 'warning will be removed in Salt Neon.' + ) + return salt.utils.args.test_mode(**kwargs) + + def which(exe=None): # Late import to avoid circular import. import salt.utils.versions @@ -1564,6 +1307,19 @@ def rand_str(size=9999999999, hash_type=None): return salt.utils.hashutils.random_hash(size, hash_type) +def get_hash(path, form='sha256', chunk_size=65536): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.hashutils + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_hash\' detected. This function has been ' + 'moved to \'salt.utils.hashutils.get_hash\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.hashutils.get_hash(path, form, chunk_size) + + def is_windows(): # Late import to avoid circular import. import salt.utils.versions @@ -1814,9 +1570,9 @@ def mkstemp(*args, **kwargs): return salt.utils.files.mkstemp(*args, **kwargs) -@jinja_filter('is_text_file') def istextfile(fp_, blocksize=512): # Late import to avoid circular import. + import salt.utils.versions import salt.utils.files salt.utils.versions.warn_until( @@ -1828,6 +1584,90 @@ def istextfile(fp_, blocksize=512): return salt.utils.files.is_text_file(fp_, blocksize=blocksize) +def is_bin_file(path): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.files + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.is_bin_file\' detected. This function has been moved ' + 'to \'salt.utils.files.is_binary\' as of Salt Oxygen. This warning will ' + 'be removed in Salt Neon.' + ) + return salt.utils.files.is_binary(path) + + +def list_files(directory): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.files + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.list_files\' detected. This function has been moved ' + 'to \'salt.utils.files.list_files\' as of Salt Oxygen. This warning will ' + 'be removed in Salt Neon.' + ) + return salt.utils.files.list_files(directory) + + +def safe_walk(top, topdown=True, onerror=None, followlinks=True, _seen=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.files + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.safe_walk\' detected. This function has been moved ' + 'to \'salt.utils.files.safe_walk\' as of Salt Oxygen. This warning will ' + 'be removed in Salt Neon.' + ) + return salt.utils.files.safe_walk(top, topdown, onerror, followlinks, _seen) + + +def st_mode_to_octal(mode): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.files + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.st_mode_to_octal\' detected. This function has ' + 'been moved to \'salt.utils.files.st_mode_to_octal\' as of Salt ' + 'Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.files.st_mode_to_octal(mode) + + +def normalize_mode(mode): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.files + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.normalize_mode\' detected. This function has ' + 'been moved to \'salt.utils.files.normalize_mode\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.files.normalize_mode(mode) + + +def human_size_to_bytes(human_size): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.files + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.human_size_to_bytes\' detected. This function has ' + 'been moved to \'salt.utils.files.human_size_to_bytes\' as of Salt ' + 'Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.files.human_size_to_bytes(human_size) + + def str_version_to_evr(verstring): # Late import to avoid circular import. import salt.utils.versions @@ -1931,8 +1771,8 @@ def kwargs_warn_until(kwargs, def get_color_theme(theme): # Late import to avoid circular import. - import salt.utils.color import salt.utils.versions + import salt.utils.color salt.utils.versions.warn_until( 'Neon', @@ -1945,8 +1785,8 @@ def get_color_theme(theme): def get_colors(use=True, theme=None): # Late import to avoid circular import. - import salt.utils.color import salt.utils.versions + import salt.utils.color salt.utils.versions.warn_until( 'Neon', @@ -2328,6 +2168,58 @@ def exactly_one(l): return salt.utils.data.exactly_one(l) +def is_list(value): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.is_list\' detected. This function ' + 'has been moved to \'salt.utils.data.is_list\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.is_list(value) + + +def is_iter(y, ignore=six.string_types): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.is_iter\' detected. This function ' + 'has been moved to \'salt.utils.data.is_iter\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.is_iter(y, ignore) + + +def isorted(to_sort): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.isorted\' detected. This function ' + 'has been moved to \'salt.utils.data.sorted_ignorecase\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.sorted_ignorecase(to_sort) + + +def is_true(value=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.is_true\' detected. This function ' + 'has been moved to \'salt.utils.data.is_true\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.is_true(value) + + def ip_bracket(addr): # Late import to avoid circular import. import salt.utils.versions diff --git a/salt/utils/args.py b/salt/utils/args.py index 7d47c52862..484359114f 100644 --- a/salt/utils/args.py +++ b/salt/utils/args.py @@ -14,6 +14,7 @@ import shlex from salt.exceptions import SaltInvocationError from salt.ext import six from salt.ext.six.moves import zip # pylint: disable=import-error,redefined-builtin +import salt.utils.data import salt.utils.jid @@ -327,3 +328,32 @@ def argspec_report(functions, module=''): ret[fun]['kwargs'] = True if kwargs else None return ret + + +def split_input(val): + ''' + Take an input value and split it into a list, returning the resulting list + ''' + if isinstance(val, list): + return val + try: + return [x.strip() for x in val.split(',')] + except AttributeError: + return [x.strip() for x in str(val).split(',')] + + +def test_mode(**kwargs): + ''' + Examines the kwargs passed and returns True if any kwarg which matching + "Test" in any variation on capitalization (i.e. "TEST", "Test", "TeSt", + etc) contains a True value (as determined by salt.utils.data.is_true). + ''' + # Once is_true is moved, remove this import and fix the ref below + import salt.utils + for arg, value in six.iteritems(kwargs): + try: + if arg.lower() == 'test' and salt.utils.data.is_true(value): + return True + except AttributeError: + continue + return False diff --git a/salt/utils/cloud.py b/salt/utils/cloud.py index b013c48f30..9fd3aee822 100644 --- a/salt/utils/cloud.py +++ b/salt/utils/cloud.py @@ -46,8 +46,8 @@ import salt.client import salt.config import salt.loader import salt.template -import salt.utils # Can be removed when pem_finger is moved import salt.utils.compat +import salt.utils.crypt import salt.utils.event import salt.utils.files import salt.utils.platform @@ -2675,7 +2675,7 @@ def request_minion_cachedir( base = __opts__['cachedir'] if not fingerprint and pubkey is not None: - fingerprint = salt.utils.pem_finger(key=pubkey, sum_type=(opts and opts.get('hash_type') or 'sha256')) + fingerprint = salt.utils.crypt.pem_finger(key=pubkey, sum_type=(opts and opts.get('hash_type') or 'sha256')) init_cachedir(base) diff --git a/salt/utils/crypt.py b/salt/utils/crypt.py index 10d5e800ea..1f643c8127 100644 --- a/salt/utils/crypt.py +++ b/salt/utils/crypt.py @@ -3,12 +3,15 @@ from __future__ import absolute_import # Import Python libs +import hashlib import logging +import os log = logging.getLogger(__name__) # Import Salt libs import salt.loader +import salt.utils.files from salt.exceptions import SaltInvocationError try: @@ -106,3 +109,29 @@ def reinit_crypto(): ''' if HAS_CRYPTO: Crypto.Random.atfork() + + +def pem_finger(path=None, key=None, sum_type='sha256'): + ''' + Pass in either a raw pem string, or the path on disk to the location of a + pem file, and the type of cryptographic hash to use. The default is SHA256. + The fingerprint of the pem will be returned. + + If neither a key nor a path are passed in, a blank string will be returned. + ''' + if not key: + if not os.path.isfile(path): + return '' + + with salt.utils.files.fopen(path, 'rb') as fp_: + key = b''.join([x for x in fp_.readlines() if x.strip()][1:-1]) + + pre = getattr(hashlib, sum_type)(key).hexdigest() + finger = '' + for ind, _ in enumerate(pre): + if ind % 2: + # Is odd + finger += '{0}:'.format(pre[ind]) + else: + finger += pre[ind] + return finger.rstrip(':') diff --git a/salt/utils/data.py b/salt/utils/data.py index f20173badf..286095b642 100644 --- a/salt/utils/data.py +++ b/salt/utils/data.py @@ -420,3 +420,78 @@ def repack_dictlist(data, else: ret[key_cb(key)] = val_cb(key, val) return ret + + +@jinja_filter('is_list') +def is_list(value): + ''' + Check if a variable is a list. + ''' + return isinstance(value, list) + + +@jinja_filter('is_iter') +def is_iter(y, ignore=six.string_types): + ''' + Test if an object is iterable, but not a string type. + + Test if an object is an iterator or is iterable itself. By default this + does not return True for string objects. + + The `ignore` argument defaults to a list of string types that are not + considered iterable. This can be used to also exclude things like + dictionaries or named tuples. + + Based on https://bitbucket.org/petershinners/yter + ''' + + if ignore and isinstance(y, ignore): + return False + try: + iter(y) + return True + except TypeError: + return False + + +@jinja_filter('sorted_ignorecase') +def sorted_ignorecase(to_sort): + ''' + Sort a list of strings ignoring case. + + >>> L = ['foo', 'Foo', 'bar', 'Bar'] + >>> sorted(L) + ['Bar', 'Foo', 'bar', 'foo'] + >>> sorted(L, key=lambda x: x.lower()) + ['bar', 'Bar', 'foo', 'Foo'] + >>> + ''' + return sorted(to_sort, key=lambda x: x.lower()) + + +def is_true(value=None): + ''' + Returns a boolean value representing the "truth" of the value passed. The + rules for what is a "True" value are: + + 1. Integer/float values greater than 0 + 2. The string values "True" and "true" + 3. Any object for which bool(obj) returns True + ''' + # First, try int/float conversion + try: + value = int(value) + except (ValueError, TypeError): + pass + try: + value = float(value) + except (ValueError, TypeError): + pass + + # Now check for truthiness + if isinstance(value, (six.integer_types, float)): + return value > 0 + elif isinstance(value, six.string_types): + return str(value).lower() == 'true' + else: + return bool(value) diff --git a/salt/utils/extmods.py b/salt/utils/extmods.py index 5d4263b80f..e4b2d46553 100644 --- a/salt/utils/extmods.py +++ b/salt/utils/extmods.py @@ -11,6 +11,7 @@ import shutil # Import salt libs import salt.fileclient +import salt.utils.hashutils import salt.utils.url # Import 3rd-party libs @@ -111,8 +112,8 @@ def sync(opts, form, saltenv=None, extmod_whitelist=None, extmod_blacklist=None) if os.path.isfile(dest): # The file is present, if the sum differs replace it hash_type = opts.get('hash_type', 'md5') - src_digest = salt.utils.get_hash(fn_, hash_type) - dst_digest = salt.utils.get_hash(dest, hash_type) + src_digest = salt.utils.hashutils.get_hash(fn_, hash_type) + dst_digest = salt.utils.hashutils.get_hash(dest, hash_type) if src_digest != dst_digest: # The downloaded file differs, replace! shutil.copyfile(fn_, dest) diff --git a/salt/utils/files.py b/salt/utils/files.py index 207e29b5ca..aae01211b2 100644 --- a/salt/utils/files.py +++ b/salt/utils/files.py @@ -19,6 +19,7 @@ import urllib import salt.utils # Can be removed when backup_minion is moved import salt.utils.path import salt.utils.platform +import salt.utils.stringutils import salt.modules.selinux from salt.exceptions import CommandExecutionError, FileLockError, MinionError from salt.utils.decorators.jinja import jinja_filter @@ -414,6 +415,56 @@ def fpopen(*args, **kwargs): yield f_handle +def safe_walk(top, topdown=True, onerror=None, followlinks=True, _seen=None): + ''' + A clone of the python os.walk function with some checks for recursive + symlinks. Unlike os.walk this follows symlinks by default. + ''' + if _seen is None: + _seen = set() + + # We may not have read permission for top, in which case we can't + # get a list of the files the directory contains. os.path.walk + # always suppressed the exception then, rather than blow up for a + # minor reason when (say) a thousand readable directories are still + # left to visit. That logic is copied here. + try: + # Note that listdir and error are globals in this module due + # to earlier import-*. + names = os.listdir(top) + except os.error as err: + if onerror is not None: + onerror(err) + return + + if followlinks: + status = os.stat(top) + # st_ino is always 0 on some filesystems (FAT, NTFS); ignore them + if status.st_ino != 0: + node = (status.st_dev, status.st_ino) + if node in _seen: + return + _seen.add(node) + + dirs, nondirs = [], [] + for name in names: + full_path = os.path.join(top, name) + if os.path.isdir(full_path): + dirs.append(name) + else: + nondirs.append(name) + + if topdown: + yield top, dirs, nondirs + for name in dirs: + new_path = os.path.join(top, name) + if followlinks or not os.path.islink(new_path): + for x in safe_walk(new_path, topdown, onerror, followlinks, _seen): + yield x + if not topdown: + yield top, dirs, nondirs + + def safe_rm(tgt): ''' Safely remove a file @@ -523,7 +574,7 @@ def safe_filepath(file_path_name, dir_sep=None): @jinja_filter('is_text_file') -def is_text_file(fp_, blocksize=512): +def is_text(fp_, blocksize=512): ''' Uses heuristics to guess whether the given file is text or binary, by reading a single block of bytes from the file. @@ -561,6 +612,27 @@ def is_text_file(fp_, blocksize=512): return float(len(nontext)) / len(block) <= 0.30 +@jinja_filter('is_bin_file') +def is_binary(path): + ''' + Detects if the file is a binary, returns bool. Returns True if the file is + a bin, False if the file is not and None if the file is not available. + ''' + if not os.path.isfile(path): + return False + try: + with fopen(path, 'rb') as fp_: + try: + data = fp_.read(2048) + if six.PY3: + data = data.decode(__salt_system_encoding__) + return salt.utils.stringutils.is_binary(data) + except UnicodeDecodeError: + return True + except os.error: + return False + + def remove(path): ''' Runs os.remove(path) and suppresses the OSError if the file doesn't exist @@ -570,3 +642,66 @@ def remove(path): except OSError as exc: if exc.errno != errno.ENOENT: raise + + +@jinja_filter('list_files') +def list_files(directory): + ''' + Return a list of all files found under directory (and its subdirectories) + ''' + ret = set() + ret.add(directory) + for root, dirs, files in safe_walk(directory): + for name in files: + ret.add(os.path.join(root, name)) + for name in dirs: + ret.add(os.path.join(root, name)) + + return list(ret) + + +def st_mode_to_octal(mode): + ''' + Convert the st_mode value from a stat(2) call (as returned from os.stat()) + to an octal mode. + ''' + try: + return oct(mode)[-4:] + except (TypeError, IndexError): + return '' + + +def normalize_mode(mode): + ''' + Return a mode value, normalized to a string and containing a leading zero + if it does not have one. + + Allow "keep" as a valid mode (used by file state/module to preserve mode + from the Salt fileserver in file states). + ''' + if mode is None: + return None + if not isinstance(mode, six.string_types): + mode = str(mode) + if six.PY3: + mode = mode.replace('0o', '0') + # Strip any quotes any initial zeroes, then though zero-pad it up to 4. + # This ensures that somethign like '00644' is normalized to '0644' + return mode.strip('"').strip('\'').lstrip('0').zfill(4) + + +def human_size_to_bytes(human_size): + ''' + Convert human-readable units to bytes + ''' + size_exp_map = {'K': 1, 'M': 2, 'G': 3, 'T': 4, 'P': 5} + human_size_str = str(human_size) + match = re.match(r'^(\d+)([KMGTP])?$', human_size_str) + if not match: + raise ValueError( + 'Size must be all digits, with an optional unit type ' + '(K, M, G, T, or P)' + ) + size_num = int(match.group(1)) + unit_multiplier = 1024 ** size_exp_map.get(match.group(2), 0) + return size_num * unit_multiplier diff --git a/salt/utils/find.py b/salt/utils/find.py index 565ee996c3..ee641cd2bc 100644 --- a/salt/utils/find.py +++ b/salt/utils/find.py @@ -105,8 +105,8 @@ except ImportError: from salt.ext import six # Import salt libs -import salt.utils import salt.utils.args +import salt.utils.hashutils import salt.utils.stringutils import salt.defaults.exitcodes from salt.utils.filebuffer import BufferedReader @@ -510,7 +510,7 @@ class PrintOption(Option): result.append(gid) elif arg == 'md5': if stat.S_ISREG(fstat[stat.ST_MODE]): - md5digest = salt.utils.get_hash(fullpath, 'md5') + md5digest = salt.utils.hashutils.get_hash(fullpath, 'md5') result.append(md5digest) else: result.append('') diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py index e4310a1519..5b466ecc6f 100644 --- a/salt/utils/gitfs.py +++ b/salt/utils/gitfs.py @@ -20,11 +20,12 @@ import time from datetime import datetime # Import salt libs -import salt.utils # Can be removed once check_whitelist_blacklist, get_hash, is_bin_file, repack_dictlist are moved +import salt.utils # Can be removed once check_whitelist_blacklist is moved import salt.utils.configparser import salt.utils.data import salt.utils.files import salt.utils.gzip_util +import salt.utils.hashutils import salt.utils.itertools import salt.utils.path import salt.utils.platform @@ -2649,7 +2650,7 @@ class GitFS(GitBase): if not os.path.isfile(hashdest): if not os.path.exists(os.path.dirname(hashdest)): os.makedirs(os.path.dirname(hashdest)) - ret['hsum'] = salt.utils.get_hash(path, self.opts['hash_type']) + ret['hsum'] = salt.utils.hashutils.get_hash(path, self.opts['hash_type']) with salt.utils.files.fopen(hashdest, 'w+') as fp_: fp_.write(ret['hsum']) return ret diff --git a/salt/utils/hashutils.py b/salt/utils/hashutils.py index 341058e80f..f24ef42185 100644 --- a/salt/utils/hashutils.py +++ b/salt/utils/hashutils.py @@ -12,6 +12,7 @@ import random # Import Salt libs from salt.ext import six +import salt.utils.files import salt.utils.stringutils from salt.utils.decorators.jinja import jinja_filter @@ -139,3 +140,26 @@ def random_hash(size=9999999999, hash_type=None): hash_type = 'md5' hasher = getattr(hashlib, hash_type) return hasher(salt.utils.stringutils.to_bytes(str(random.SystemRandom().randint(0, size)))).hexdigest() + + +@jinja_filter('file_hashsum') +def get_hash(path, form='sha256', chunk_size=65536): + ''' + Get the hash sum of a file + + This is better than ``get_sum`` for the following reasons: + - It does not read the entire file into memory. + - It does not return a string on error. The returned value of + ``get_sum`` cannot really be trusted since it is vulnerable to + collisions: ``get_sum(..., 'xyz') == 'Hash xyz not supported'`` + ''' + hash_type = hasattr(hashlib, form) and getattr(hashlib, form) or None + if hash_type is None: + raise ValueError('Invalid hash type: {0}'.format(form)) + + with salt.utils.files.fopen(path, 'rb') as ifile: + hash_obj = hash_type() + # read the file in in chunks, not the entire file + for chunk in iter(lambda: ifile.read(chunk_size), b''): + hash_obj.update(chunk) + return hash_obj.hexdigest() diff --git a/salt/utils/minions.py b/salt/utils/minions.py index ab30e31d1c..1f5466bbd1 100644 --- a/salt/utils/minions.py +++ b/salt/utils/minions.py @@ -13,7 +13,7 @@ import logging # Import salt libs import salt.payload -import salt.utils +import salt.utils # Can be removed once expr_match is moved import salt.utils.data import salt.utils.files import salt.utils.network @@ -236,7 +236,7 @@ class CkMinions(object): with salt.utils.files.fopen(pki_cache_fn) as fn_: return self.serial.load(fn_) else: - for fn_ in salt.utils.isorted(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): if not fn_.startswith('.') and os.path.isfile(os.path.join(self.opts['pki_dir'], self.acc, fn_)): minions.append(fn_) return minions @@ -263,7 +263,7 @@ class CkMinions(object): if greedy: minions = [] - for fn_ in salt.utils.isorted(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): if not fn_.startswith('.') and os.path.isfile(os.path.join(self.opts['pki_dir'], self.acc, fn_)): minions.append(fn_) elif cache_enabled: @@ -420,7 +420,7 @@ class CkMinions(object): cache_enabled = self.opts.get('minion_data_cache', False) if greedy: mlist = [] - for fn_ in salt.utils.isorted(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): if not fn_.startswith('.') and os.path.isfile(os.path.join(self.opts['pki_dir'], self.acc, fn_)): mlist.append(fn_) return {'minions': mlist, @@ -636,7 +636,7 @@ class CkMinions(object): Return a list of all minions that have auth'd ''' mlist = [] - for fn_ in salt.utils.isorted(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): + for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(os.path.join(self.opts['pki_dir'], self.acc))): if not fn_.startswith('.') and os.path.isfile(os.path.join(self.opts['pki_dir'], self.acc, fn_)): mlist.append(fn_) return {'minions': mlist, 'missing': []} diff --git a/salt/utils/pkg/__init__.py b/salt/utils/pkg/__init__.py index 68e5864676..a764560816 100644 --- a/salt/utils/pkg/__init__.py +++ b/salt/utils/pkg/__init__.py @@ -10,7 +10,7 @@ import os import re # Import Salt libs -import salt.utils # Can be removed once is_true is moved +import salt.utils.data import salt.utils.files import salt.utils.versions @@ -62,7 +62,7 @@ def check_refresh(opts, refresh=None): - A boolean if refresh is not False and the rtag file exists ''' return bool( - salt.utils.is_true(refresh) or + salt.utils.data.is_true(refresh) or (os.path.isfile(rtag(opts)) and refresh is not False) ) diff --git a/salt/utils/s3.py b/salt/utils/s3.py index 0bb198d3a6..236d8a48b6 100644 --- a/salt/utils/s3.py +++ b/salt/utils/s3.py @@ -17,9 +17,9 @@ except ImportError: HAS_REQUESTS = False # pylint: disable=W0612 # Import Salt libs -import salt.utils import salt.utils.aws import salt.utils.files +import salt.utils.hashutils import salt.utils.xmlutil as xml from salt._compat import ElementTree as ET from salt.exceptions import CommandExecutionError @@ -119,7 +119,7 @@ def query(key, keyid, method='GET', params=None, headers=None, payload_hash = None if method == 'PUT': if local_file: - payload_hash = salt.utils.get_hash(local_file, form='sha256') + payload_hash = salt.utils.hashutils.get_hash(local_file, form='sha256') if path is None: path = '' diff --git a/salt/utils/templates.py b/salt/utils/templates.py index bbb834bb41..f8e7682538 100644 --- a/salt/utils/templates.py +++ b/salt/utils/templates.py @@ -152,7 +152,7 @@ def wrap_tmpl_func(render_str): ValueError, OSError, IOError) as exc: - if salt.utils.is_bin_file(tmplsrc): + if salt.utils.files.is_binary(tmplsrc): # Template is a bin file, return the raw file return dict(result=True, data=tmplsrc) log.error( diff --git a/salt/utils/thin.py b/salt/utils/thin.py index 2dca207140..bd13b9f11c 100644 --- a/salt/utils/thin.py +++ b/salt/utils/thin.py @@ -72,6 +72,7 @@ except ImportError: import salt import salt.utils import salt.utils.files +import salt.utils.hashutils import salt.exceptions SALTCALL = ''' @@ -341,7 +342,7 @@ def thin_sum(cachedir, form='sha1'): Return the checksum of the current thin tarball ''' thintar = gen_thin(cachedir) - return salt.utils.get_hash(thintar, form) + return salt.utils.hashutils.get_hash(thintar, form) def gen_min(cachedir, extra_mods='', overwrite=False, so_mods='', @@ -625,4 +626,4 @@ def min_sum(cachedir, form='sha1'): Return the checksum of the current thin tarball ''' mintar = gen_min(cachedir) - return salt.utils.get_hash(mintar, form) + return salt.utils.hashutils.get_hash(mintar, form) diff --git a/salt/utils/win_update.py b/salt/utils/win_update.py index fac45d9276..e16bfc7b57 100644 --- a/salt/utils/win_update.py +++ b/salt/utils/win_update.py @@ -8,8 +8,8 @@ import logging import subprocess # Import Salt libs -import salt.utils import salt.utils.args +import salt.utils.data from salt.ext import six from salt.ext.six.moves import range from salt.exceptions import CommandExecutionError @@ -199,17 +199,17 @@ class Updates(object): results['Total'] += 1 # Updates available for download - if not salt.utils.is_true(update.IsDownloaded) \ - and not salt.utils.is_true(update.IsInstalled): + if not salt.utils.data.is_true(update.IsDownloaded) \ + and not salt.utils.data.is_true(update.IsInstalled): results['Available'] += 1 # Updates downloaded awaiting install - if salt.utils.is_true(update.IsDownloaded) \ - and not salt.utils.is_true(update.IsInstalled): + if salt.utils.data.is_true(update.IsDownloaded) \ + and not salt.utils.data.is_true(update.IsInstalled): results['Downloaded'] += 1 # Updates installed - if salt.utils.is_true(update.IsInstalled): + if salt.utils.data.is_true(update.IsInstalled): results['Installed'] += 1 # Add Categories and increment total for each one @@ -437,16 +437,16 @@ class WindowsUpdateAgent(object): for update in self._updates: - if salt.utils.is_true(update.IsHidden) and skip_hidden: + if salt.utils.data.is_true(update.IsHidden) and skip_hidden: continue - if salt.utils.is_true(update.IsInstalled) and skip_installed: + if salt.utils.data.is_true(update.IsInstalled) and skip_installed: continue - if salt.utils.is_true(update.IsMandatory) and skip_mandatory: + if salt.utils.data.is_true(update.IsMandatory) and skip_mandatory: continue - if salt.utils.is_true( + if salt.utils.data.is_true( update.InstallationBehavior.RebootBehavior) and skip_reboot: continue @@ -587,12 +587,12 @@ class WindowsUpdateAgent(object): bool(update.IsDownloaded) # Accept EULA - if not salt.utils.is_true(update.EulaAccepted): + if not salt.utils.data.is_true(update.EulaAccepted): log.debug('Accepting EULA: {0}'.format(update.Title)) update.AcceptEula() # pylint: disable=W0104 # Update already downloaded - if not salt.utils.is_true(update.IsDownloaded): + if not salt.utils.data.is_true(update.IsDownloaded): log.debug('To Be Downloaded: {0}'.format(uid)) log.debug('\tTitle: {0}'.format(update.Title)) download_list.Add(update) @@ -697,7 +697,7 @@ class WindowsUpdateAgent(object): ret['Updates'][uid]['AlreadyInstalled'] = bool(update.IsInstalled) # Make sure the update has actually been installed - if not salt.utils.is_true(update.IsInstalled): + if not salt.utils.data.is_true(update.IsInstalled): log.debug('To Be Installed: {0}'.format(uid)) log.debug('\tTitle: {0}'.format(update.Title)) install_list.Add(update) @@ -817,7 +817,7 @@ class WindowsUpdateAgent(object): not bool(update.IsInstalled) # Make sure the update has actually been Uninstalled - if salt.utils.is_true(update.IsInstalled): + if salt.utils.data.is_true(update.IsInstalled): log.debug('To Be Uninstalled: {0}'.format(uid)) log.debug('\tTitle: {0}'.format(update.Title)) uninstall_list.Add(update) @@ -1003,4 +1003,4 @@ def needs_reboot(): # Create an AutoUpdate object obj_sys = win32com.client.Dispatch('Microsoft.Update.SystemInfo') - return salt.utils.is_true(obj_sys.RebootRequired) + return salt.utils.data.is_true(obj_sys.RebootRequired) diff --git a/salt/wheel/file_roots.py b/salt/wheel/file_roots.py index 8df2eee4b8..1f2d7ab68d 100644 --- a/salt/wheel/file_roots.py +++ b/salt/wheel/file_roots.py @@ -27,7 +27,7 @@ def find(path, saltenv='base'): if os.path.isfile(full): # Add it to the dict with salt.utils.files.fopen(full, 'rb') as fp_: - if salt.utils.files.is_text_file(fp_): + if salt.utils.files.is_text(fp_): ret.append({full: 'txt'}) else: ret.append({full: 'bin'}) diff --git a/salt/wheel/key.py b/salt/wheel/key.py index 9f1a17bfa7..a4dcf7a787 100644 --- a/salt/wheel/key.py +++ b/salt/wheel/key.py @@ -36,7 +36,7 @@ import logging # Import salt libs from salt.key import get_key import salt.crypt -import salt.utils # Can be removed once pem_finger is moved +import salt.utils.crypt import salt.utils.files import salt.utils.platform from salt.utils.sanitizers import clean @@ -315,7 +315,7 @@ def finger_master(hash_type=None): if hash_type is None: hash_type = __opts__['hash_type'] - fingerprint = salt.utils.pem_finger( + fingerprint = salt.utils.crypt.pem_finger( os.path.join(__opts__['pki_dir'], keyname), sum_type=hash_type) return {'local': {keyname: fingerprint}} diff --git a/salt/wheel/pillar_roots.py b/salt/wheel/pillar_roots.py index 65790e17d9..b5464a23ca 100644 --- a/salt/wheel/pillar_roots.py +++ b/salt/wheel/pillar_roots.py @@ -28,7 +28,7 @@ def find(path, saltenv='base'): if os.path.isfile(full): # Add it to the dict with salt.utils.files.fopen(full, 'rb') as fp_: - if salt.utils.files.is_text_file(fp_): + if salt.utils.files.is_text(fp_): ret.append({full: 'txt'}) else: ret.append({full: 'bin'}) diff --git a/tests/integration/states/test_file.py b/tests/integration/states/test_file.py index d145efdbb1..a720a9e356 100644 --- a/tests/integration/states/test_file.py +++ b/tests/integration/states/test_file.py @@ -32,7 +32,6 @@ from tests.support.helpers import ( from tests.support.mixins import SaltReturnAssertsMixin # Import Salt libs -import salt.utils # Can be removed once normalize_mode is moved import salt.utils.files import salt.utils.path import salt.utils.platform @@ -615,7 +614,7 @@ class FileTest(ModuleCase, SaltReturnAssertsMixin): ''' Return a string octal representation of the permissions for name ''' - return salt.utils.normalize_mode(oct(os.stat(name).st_mode & 0o777)) + return salt.utils.files.normalize_mode(oct(os.stat(name).st_mode & 0o777)) top = os.path.join(TMP, 'top_dir') sub = os.path.join(top, 'sub_dir') diff --git a/tests/unit/modules/test_file.py b/tests/unit/modules/test_file.py index d580667923..8f1b3a97a0 100644 --- a/tests/unit/modules/test_file.py +++ b/tests/unit/modules/test_file.py @@ -48,7 +48,7 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): 'grains': {}, }, '__grains__': {'kernel': 'Linux'}, - '__utils__': {'files.is_text_file': MagicMock(return_value=True)}, + '__utils__': {'files.is_text': MagicMock(return_value=True)}, } } @@ -218,7 +218,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): 'grains': {}, }, '__grains__': {'kernel': 'Linux'}, - '__utils__': {'files.is_text_file': MagicMock(return_value=True)}, + '__utils__': {'files.is_text': MagicMock(return_value=True)}, } } diff --git a/tests/unit/modules/test_key.py b/tests/unit/modules/test_key.py index fc5cb27e66..5c540fffef 100644 --- a/tests/unit/modules/test_key.py +++ b/tests/unit/modules/test_key.py @@ -18,7 +18,7 @@ from tests.support.mock import ( ) # Import Salt Libs -import salt.utils +import salt.utils.crypt import salt.modules.key as key @@ -35,7 +35,7 @@ class KeyTestCase(TestCase, LoaderModuleMockMixin): Test for finger ''' with patch.object(os.path, 'join', return_value='A'): - with patch.object(salt.utils, + with patch.object(salt.utils.crypt, 'pem_finger', return_value='A'): with patch.dict(key.__opts__, {'pki_dir': MagicMock(return_value='A'), 'hash_type': 'sha256'}): @@ -46,7 +46,7 @@ class KeyTestCase(TestCase, LoaderModuleMockMixin): Test for finger ''' with patch.object(os.path, 'join', return_value='A'): - with patch.object(salt.utils, + with patch.object(salt.utils.crypt, 'pem_finger', return_value='A'): with patch.dict(key.__opts__, {'pki_dir': 'A', 'hash_type': 'sha256'}): diff --git a/tests/unit/modules/test_pkg_resource.py b/tests/unit/modules/test_pkg_resource.py index e00d8cda77..3ac5a6f7d8 100644 --- a/tests/unit/modules/test_pkg_resource.py +++ b/tests/unit/modules/test_pkg_resource.py @@ -18,7 +18,7 @@ from tests.support.mock import ( ) # Import Salt Libs -import salt.utils +import salt.utils.data import salt.modules.pkg_resource as pkg_resource from salt.ext import six @@ -99,7 +99,7 @@ class PkgresTestCase(TestCase, LoaderModuleMockMixin): Test to Common interface for obtaining the version of installed packages. ''' - with patch.object(salt.utils, 'is_true', return_value=True): + with patch.object(salt.utils.data, 'is_true', return_value=True): mock = MagicMock(return_value={'A': 'B'}) with patch.dict(pkg_resource.__salt__, {'pkg.list_pkgs': mock}): diff --git a/tests/unit/modules/test_state.py b/tests/unit/modules/test_state.py index 7d9b78ead1..31e0b7385c 100644 --- a/tests/unit/modules/test_state.py +++ b/tests/unit/modules/test_state.py @@ -21,7 +21,7 @@ from tests.support.mock import ( # Import Salt Libs import salt.config import salt.loader -import salt.utils +import salt.utils.hashutils import salt.utils.odict import salt.utils.platform import salt.modules.state as state @@ -969,7 +969,7 @@ class StateTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(state.pkg(tar_file, "", "md5"), {}) mock = MagicMock(side_effect=[False, 0, 0, 0, 0]) - with patch.object(salt.utils, 'get_hash', mock): + with patch.object(salt.utils.hashutils, 'get_hash', mock): # Verify hash self.assertDictEqual(state.pkg(tar_file, "", "md5"), {}) diff --git a/tests/unit/states/test_file.py b/tests/unit/states/test_file.py index 7aa2c0d0f7..77ffe469bd 100644 --- a/tests/unit/states/test_file.py +++ b/tests/unit/states/test_file.py @@ -1182,7 +1182,7 @@ class TestFileState(TestCase, LoaderModuleMockMixin): ret.update({'name': name}) with patch.object(salt.utils.files, 'fopen', MagicMock(mock_open(read_data=''))): - with patch.dict(filestate.__utils__, {'files.is_text_file': mock_f}): + with patch.dict(filestate.__utils__, {'files.is_text': mock_f}): with patch.dict(filestate.__opts__, {'test': True}): change = {'diff': 'Replace binary file'} comt = ('File {0} is set to be updated' diff --git a/tests/unit/states/test_mysql_user.py b/tests/unit/states/test_mysql_user.py index 862e165c18..b34f3f97bb 100644 --- a/tests/unit/states/test_mysql_user.py +++ b/tests/unit/states/test_mysql_user.py @@ -16,7 +16,7 @@ from tests.support.mock import ( # Import Salt Libs import salt.states.mysql_user as mysql_user -import salt.utils +import salt.utils.data @skipIf(NO_MOCK, NO_MOCK_REASON) @@ -49,7 +49,7 @@ class MysqlUserTestCase(TestCase, LoaderModuleMockMixin): mock_str = MagicMock(return_value='salt') mock_none = MagicMock(return_value=None) mock_sn = MagicMock(side_effect=[None, 'salt', None, None, None]) - with patch.object(salt.utils, 'is_true', mock_f): + with patch.object(salt.utils.data, 'is_true', mock_f): comt = ('Either password or password_hash must be specified,' ' unless allow_passwordless is True') ret.update({'comment': comt}) @@ -57,7 +57,7 @@ class MysqlUserTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(mysql_user.__salt__, {'mysql.user_exists': mock, 'mysql.user_chpass': mock_t}): - with patch.object(salt.utils, 'is_true', mock_t): + with patch.object(salt.utils.data, 'is_true', mock_t): comt = ('User frank@localhost is already present' ' with passwordless login') ret.update({'comment': comt, 'result': True}) diff --git a/tests/unit/utils/test_args.py b/tests/unit/utils/test_args.py index 7389978265..9fdbde7c82 100644 --- a/tests/unit/utils/test_args.py +++ b/tests/unit/utils/test_args.py @@ -94,3 +94,8 @@ class ArgsTestCase(TestCase): ret = salt.utils.args.argspec_report(test_functions, 'test_module.test_spec') self.assertDictEqual(ret, {'test_module.test_spec': {'kwargs': True, 'args': None, 'defaults': None, 'varargs': True}}) + + def test_test_mode(self): + self.assertTrue(salt.utils.args.test_mode(test=True)) + self.assertTrue(salt.utils.args.test_mode(Test=True)) + self.assertTrue(salt.utils.args.test_mode(tEsT=True)) diff --git a/tests/unit/utils/test_data.py b/tests/unit/utils/test_data.py new file mode 100644 index 0000000000..be7ddf9f52 --- /dev/null +++ b/tests/unit/utils/test_data.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +''' +Tests for salt.utils.data +''' + +# Import Python libs +from __future__ import absolute_import + +# Import Salt libs +import salt.utils.data +from tests.support.unit import TestCase + + +class DataTestCase(TestCase): + def test_sorted_ignorecase(self): + test_list = ['foo', 'Foo', 'bar', 'Bar'] + expected_list = ['bar', 'Bar', 'foo', 'Foo'] + self.assertEqual( + salt.utils.data.sorted_ignorecase(test_list), expected_list) diff --git a/tests/unit/utils/test_hashutils.py b/tests/unit/utils/test_hashutils.py new file mode 100644 index 0000000000..5d5ce2596e --- /dev/null +++ b/tests/unit/utils/test_hashutils.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +# Import python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from tests.support.unit import TestCase + +# Import Salt libs +import salt.utils.hashutils + + +class HashutilsTestCase(TestCase): + + def test_get_hash_exception(self): + self.assertRaises( + ValueError, + salt.utils.hashutils.get_hash, + '/tmp/foo/', + form='INVALID') diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index cfff130667..06d286eeff 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -78,11 +78,6 @@ class UtilsTestCase(TestCase): ret = salt.utils.build_whitespace_split_regex(' '.join(LOREM_IPSUM.split()[:5])) self.assertEqual(ret, expected_regex) - def test_isorted(self): - test_list = ['foo', 'Foo', 'bar', 'Bar'] - expected_list = ['bar', 'Bar', 'foo', 'Foo'] - self.assertEqual(salt.utils.isorted(test_list), expected_list) - def test_mysql_to_dict(self): test_mysql_output = ['+----+------+-----------+------+---------+------+-------+------------------+', '| Id | User | Host | db | Command | Time | State | Info |', @@ -247,11 +242,6 @@ class UtilsTestCase(TestCase): with patch('zmq.IPC_PATH_MAX_LEN', 1): self.assertRaises(SaltSystemExit, salt.utils.zeromq.check_ipc_path_max_len, '1' * 1024) - def test_test_mode(self): - self.assertTrue(salt.utils.test_mode(test=True)) - self.assertTrue(salt.utils.test_mode(Test=True)) - self.assertTrue(salt.utils.test_mode(tEsT=True)) - def test_option(self): test_two_level_dict = {'foo': {'bar': 'baz'}} @@ -260,9 +250,6 @@ class UtilsTestCase(TestCase): self.assertEqual('baz', salt.utils.option('foo:bar', {'not_found': 'nope'}, pillar={'master': test_two_level_dict})) self.assertEqual('baz', salt.utils.option('foo:bar', {'not_found': 'nope'}, pillar=test_two_level_dict)) - def test_get_hash_exception(self): - self.assertRaises(ValueError, salt.utils.get_hash, '/tmp/foo/', form='INVALID') - @skipIf(NO_MOCK, NO_MOCK_REASON) def test_date_cast(self): now = datetime.datetime.now() From 2d4d8fa9c8c12783e024890d08cdc6e45738ddd7 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 12 Oct 2017 18:51:07 -0500 Subject: [PATCH 594/633] Convert "Can be removed" lines into TODO lines --- salt/cli/batch.py | 2 +- salt/cli/caller.py | 2 +- salt/cli/run.py | 2 +- salt/cli/salt.py | 2 +- salt/client/mixins.py | 2 +- salt/cloud/clouds/openstack.py | 2 +- salt/daemons/masterapi.py | 2 +- salt/key.py | 2 +- salt/master.py | 2 +- salt/modules/aptpkg.py | 2 +- salt/modules/ebuild.py | 2 +- salt/modules/freebsdpkg.py | 2 +- salt/modules/mac_brew.py | 2 +- salt/modules/mac_ports.py | 2 +- salt/modules/network.py | 2 +- salt/modules/pacman.py | 2 +- salt/modules/pillar.py | 2 +- salt/modules/pkgin.py | 2 +- salt/modules/pkgng.py | 2 +- salt/modules/pkgutil.py | 2 +- salt/modules/rpmbuild.py | 2 +- salt/modules/saltutil.py | 2 +- salt/modules/solarisips.py | 2 +- salt/modules/solarispkg.py | 2 +- salt/modules/state.py | 2 +- salt/modules/yumpkg.py | 2 +- salt/modules/zypper.py | 2 +- salt/states/user.py | 2 +- salt/states/virtualenv_mod.py | 2 +- salt/utils/files.py | 2 +- salt/utils/gitfs.py | 2 +- salt/utils/minions.py | 2 +- tests/support/mixins.py | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/salt/cli/batch.py b/salt/cli/batch.py index 0bd4a7ace6..6e0c5b7ac6 100644 --- a/salt/cli/batch.py +++ b/salt/cli/batch.py @@ -11,7 +11,7 @@ import copy from datetime import datetime, timedelta # Import salt libs -import salt.utils # Can be removed once print_cli is moved +import salt.utils # TODO: Remove this once print_cli is moved import salt.client import salt.output import salt.exceptions diff --git a/salt/cli/caller.py b/salt/cli/caller.py index 5c04bab11c..dca0b70edf 100644 --- a/salt/cli/caller.py +++ b/salt/cli/caller.py @@ -20,7 +20,7 @@ import salt.minion import salt.output import salt.payload import salt.transport -import salt.utils # Can be removed once print_cli, activate_profile, and output_profile are moved +import salt.utils # TODO: Remove this once print_cli, activate_profile, and output_profile are moved import salt.utils.args import salt.utils.files import salt.utils.jid diff --git a/salt/cli/run.py b/salt/cli/run.py index c780681368..f54e984252 100644 --- a/salt/cli/run.py +++ b/salt/cli/run.py @@ -2,7 +2,7 @@ from __future__ import print_function from __future__ import absolute_import -import salt.utils # Can be removed once activate_profile and output_profile are moved +import salt.utils # TODO: Remove this once activate_profile and output_profile are moved import salt.utils.parsers from salt.utils.verify import check_user, verify_log from salt.exceptions import SaltClientError diff --git a/salt/cli/salt.py b/salt/cli/salt.py index 24e3a2ecd4..814140ff12 100644 --- a/salt/cli/salt.py +++ b/salt/cli/salt.py @@ -7,7 +7,7 @@ sys.modules['pkg_resources'] = None import os # Import Salt libs -import salt.utils # Can be removed once print_cli is moved +import salt.utils # TODO: Remove this once print_cli is moved import salt.utils.parsers from salt.utils.args import yamlify_arg from salt.utils.verify import verify_log diff --git a/salt/client/mixins.py b/salt/client/mixins.py index 0df7cf7dd1..9b851c6ad9 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -16,7 +16,7 @@ import copy as pycopy # Import Salt libs import salt.exceptions import salt.minion -import salt.utils # Can be removed once format_call is moved +import salt.utils # TODO: Remove this once format_call is moved import salt.utils.args import salt.utils.doc import salt.utils.error diff --git a/salt/cloud/clouds/openstack.py b/salt/cloud/clouds/openstack.py index b26a36a47c..a073ba29e9 100644 --- a/salt/cloud/clouds/openstack.py +++ b/salt/cloud/clouds/openstack.py @@ -185,7 +185,7 @@ except Exception: from salt.cloud.libcloudfuncs import * # pylint: disable=W0614,W0401 # Import salt libs -import salt.utils # Can be removed once namespaced_function has been moved +import salt.utils # TODO: Remove this once namespaced_function has been moved import salt.utils.cloud import salt.utils.files import salt.utils.pycrypto diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index ad63e43e56..4d5a8a1c06 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -15,7 +15,7 @@ import stat # Import salt libs import salt.crypt -import salt.utils # Can be removed once check_whitelist_blacklist, expr_match, get_values_of_matching_keys are moved +import salt.utils # TODO: Remove this once check_whitelist_blacklist, expr_match, get_values_of_matching_keys are moved import salt.cache import salt.client import salt.payload diff --git a/salt/key.py b/salt/key.py index 97904971b6..a4f1eefa0f 100644 --- a/salt/key.py +++ b/salt/key.py @@ -22,7 +22,7 @@ import salt.crypt import salt.daemons.masterapi import salt.exceptions import salt.minion -import salt.utils # Can be removed once get_master_key is moved +import salt.utils # TODO: Remove this once get_master_key is moved import salt.utils.args import salt.utils.crypt import salt.utils.data diff --git a/salt/master.py b/salt/master.py index f24f78330c..05c0cc704b 100644 --- a/salt/master.py +++ b/salt/master.py @@ -47,7 +47,7 @@ import tornado.gen # pylint: disable=F0401 # Import salt libs import salt.crypt -import salt.utils # Can be removed once get_values_of_matching_keys is moved +import salt.utils # TODO: Remove this once get_values_of_matching_keys is moved import salt.client import salt.payload import salt.pillar diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py index 9fb042a9cc..591b7ab2dd 100644 --- a/salt/modules/aptpkg.py +++ b/salt/modules/aptpkg.py @@ -38,7 +38,7 @@ from salt.ext.six.moves.urllib.request import Request as _Request, urlopen as _u import salt.config import salt.syspaths from salt.modules.cmdmod import _parse_env -import salt.utils # Can be removed when alias_function is moved +import salt.utils # TODO: Remove this when alias_function is moved import salt.utils.args import salt.utils.data import salt.utils.files diff --git a/salt/modules/ebuild.py b/salt/modules/ebuild.py index 34ee1f147c..facde056bd 100644 --- a/salt/modules/ebuild.py +++ b/salt/modules/ebuild.py @@ -21,7 +21,7 @@ import logging import re # Import salt libs -import salt.utils # Can be removed once alias_function is moved +import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.args import salt.utils.data import salt.utils.path diff --git a/salt/modules/freebsdpkg.py b/salt/modules/freebsdpkg.py index 28806aa338..de37a4218e 100644 --- a/salt/modules/freebsdpkg.py +++ b/salt/modules/freebsdpkg.py @@ -80,7 +80,7 @@ import logging import re # Import salt libs -import salt.utils # Can be removed when alias_function is moved +import salt.utils # TODO: Remove this when alias_function is moved import salt.utils.data import salt.utils.pkg from salt.exceptions import CommandExecutionError, MinionError diff --git a/salt/modules/mac_brew.py b/salt/modules/mac_brew.py index 0aa63efeef..08fe63105a 100644 --- a/salt/modules/mac_brew.py +++ b/salt/modules/mac_brew.py @@ -17,7 +17,7 @@ import json import logging # Import salt libs -import salt.utils # Can be removed when alias_function is moved +import salt.utils # TODO: Remove this when alias_function is moved import salt.utils.data import salt.utils.path import salt.utils.pkg diff --git a/salt/modules/mac_ports.py b/salt/modules/mac_ports.py index b50d5320ea..80e5952395 100644 --- a/salt/modules/mac_ports.py +++ b/salt/modules/mac_ports.py @@ -37,7 +37,7 @@ import logging import re # Import salt libs -import salt.utils # Can be removed when alias_function is removed +import salt.utils # TODO: Remove this when alias_function is removed import salt.utils.data import salt.utils.path import salt.utils.pkg diff --git a/salt/modules/network.py b/salt/modules/network.py index 2346a72026..ec0927eccf 100644 --- a/salt/modules/network.py +++ b/salt/modules/network.py @@ -13,7 +13,7 @@ import os import socket # Import salt libs -import salt.utils # Can be removed when alias_function mac_str_to_bytes are moved +import salt.utils # TODO: Remove this when alias_function mac_str_to_bytes are moved import salt.utils.decorators.path import salt.utils.files import salt.utils.network diff --git a/salt/modules/pacman.py b/salt/modules/pacman.py index 0ad735d937..5088cc9020 100644 --- a/salt/modules/pacman.py +++ b/salt/modules/pacman.py @@ -18,7 +18,7 @@ import logging import os.path # Import salt libs -import salt.utils # Can be removed once alias_function, fnmatch_multiple are moved +import salt.utils # TODO: Remove this once alias_function, fnmatch_multiple are moved import salt.utils.args import salt.utils.data import salt.utils.pkg diff --git a/salt/modules/pillar.py b/salt/modules/pillar.py index fa1b091e3b..990e0045b8 100644 --- a/salt/modules/pillar.py +++ b/salt/modules/pillar.py @@ -17,7 +17,7 @@ from salt.ext import six # Import salt libs import salt.pillar -import salt.utils # Can be removed once alias_function is moved +import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.crypt import salt.utils.data import salt.utils.dictupdate diff --git a/salt/modules/pkgin.py b/salt/modules/pkgin.py index a85baacd82..f4a302b0c5 100644 --- a/salt/modules/pkgin.py +++ b/salt/modules/pkgin.py @@ -17,7 +17,7 @@ import os import re # Import salt libs -import salt.utils # Can be removed when alias_function is moved +import salt.utils # TODO: Remove this when alias_function is moved import salt.utils.data import salt.utils.path import salt.utils.pkg diff --git a/salt/modules/pkgng.py b/salt/modules/pkgng.py index 026cf68e98..b7afdb199f 100644 --- a/salt/modules/pkgng.py +++ b/salt/modules/pkgng.py @@ -44,7 +44,7 @@ import logging import os # Import salt libs -import salt.utils # Can be removed once alias_function is moved +import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data import salt.utils.files import salt.utils.itertools diff --git a/salt/modules/pkgutil.py b/salt/modules/pkgutil.py index 2367c40d23..ce0f7de66f 100644 --- a/salt/modules/pkgutil.py +++ b/salt/modules/pkgutil.py @@ -14,7 +14,7 @@ from __future__ import absolute_import import copy # Import salt libs -import salt.utils # Can be removed once alias_function is moved +import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data import salt.utils.pkg import salt.utils.versions diff --git a/salt/modules/rpmbuild.py b/salt/modules/rpmbuild.py index a6687db524..6482553875 100644 --- a/salt/modules/rpmbuild.py +++ b/salt/modules/rpmbuild.py @@ -24,7 +24,7 @@ import functools # Import salt libs from salt.exceptions import SaltInvocationError -import salt.utils # Can be removed when chugid_and_umask is moved +import salt.utils # TODO: Remove this when chugid_and_umask is moved import salt.utils.files import salt.utils.path import salt.utils.vt diff --git a/salt/modules/saltutil.py b/salt/modules/saltutil.py index 3d8d55dee6..337c5de7c0 100644 --- a/salt/modules/saltutil.py +++ b/salt/modules/saltutil.py @@ -46,7 +46,7 @@ import salt.payload import salt.runner import salt.state import salt.transport -import salt.utils # Can be removed once alias_function is moved +import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.args import salt.utils.event import salt.utils.extmods diff --git a/salt/modules/solarisips.py b/salt/modules/solarisips.py index eb44512703..be992325d0 100644 --- a/salt/modules/solarisips.py +++ b/salt/modules/solarisips.py @@ -43,7 +43,7 @@ import logging # Import salt libs -import salt.utils # Can be removed once alias_function is moved +import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data import salt.utils.path import salt.utils.pkg diff --git a/salt/modules/solarispkg.py b/salt/modules/solarispkg.py index 43da38c015..bab2c4e30a 100644 --- a/salt/modules/solarispkg.py +++ b/salt/modules/solarispkg.py @@ -16,7 +16,7 @@ import os import logging # Import salt libs -import salt.utils # Can be removed once alias_function is moved +import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data import salt.utils.files from salt.exceptions import CommandExecutionError, MinionError diff --git a/salt/modules/state.py b/salt/modules/state.py index 050343dba2..2f20cdaa45 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -28,7 +28,7 @@ import time import salt.config import salt.payload import salt.state -import salt.utils # Can be removed once namespaced_function is moved +import salt.utils # TODO: Remove this once namespaced_function is moved import salt.utils.args import salt.utils.data import salt.utils.event diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index f6383343b8..16cd9860ee 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -41,7 +41,7 @@ from salt.ext.six.moves import configparser # pylint: enable=import-error,redefined-builtin # Import Salt libs -import salt.utils # Can be removed once alias_function, fnmatch_multiple are moved +import salt.utils # TODO: Remove this once alias_function, fnmatch_multiple are moved import salt.utils.args import salt.utils.data import salt.utils.decorators.path diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py index 4dfbeabaae..12251fb33a 100644 --- a/salt/modules/zypper.py +++ b/salt/modules/zypper.py @@ -32,7 +32,7 @@ from xml.dom import minidom as dom from xml.parsers.expat import ExpatError # Import salt libs -import salt.utils # Can be removed once alias_function is moved +import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data import salt.utils.event import salt.utils.files diff --git a/salt/states/user.py b/salt/states/user.py index 1328960675..03a2ac1590 100644 --- a/salt/states/user.py +++ b/salt/states/user.py @@ -29,7 +29,7 @@ import os import logging # Import Salt libs -import salt.utils # Can be removed once date_format is moved +import salt.utils # TODO: Remove this once date_format is moved import salt.utils.platform import salt.utils.user from salt.utils.locales import sdecode, sdecode_if_string diff --git a/salt/states/virtualenv_mod.py b/salt/states/virtualenv_mod.py index 364699051f..bd394bfd0b 100644 --- a/salt/states/virtualenv_mod.py +++ b/salt/states/virtualenv_mod.py @@ -12,7 +12,7 @@ import os # Import Salt libs import salt.version -import salt.utils # Can be removed once alias_function is moved +import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.platform import salt.utils.versions from salt.exceptions import CommandExecutionError, CommandNotFoundError diff --git a/salt/utils/files.py b/salt/utils/files.py index aae01211b2..9a4a14eebd 100644 --- a/salt/utils/files.py +++ b/salt/utils/files.py @@ -16,7 +16,7 @@ import time import urllib # Import Salt libs -import salt.utils # Can be removed when backup_minion is moved +import salt.utils # TODO: Remove this when backup_minion is moved import salt.utils.path import salt.utils.platform import salt.utils.stringutils diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py index 5b466ecc6f..ae028463a0 100644 --- a/salt/utils/gitfs.py +++ b/salt/utils/gitfs.py @@ -20,7 +20,7 @@ import time from datetime import datetime # Import salt libs -import salt.utils # Can be removed once check_whitelist_blacklist is moved +import salt.utils # TODO: Remove this once check_whitelist_blacklist is moved import salt.utils.configparser import salt.utils.data import salt.utils.files diff --git a/salt/utils/minions.py b/salt/utils/minions.py index 1f5466bbd1..41c6b11cef 100644 --- a/salt/utils/minions.py +++ b/salt/utils/minions.py @@ -13,7 +13,7 @@ import logging # Import salt libs import salt.payload -import salt.utils # Can be removed once expr_match is moved +import salt.utils # TODO: Remove this once expr_match is moved import salt.utils.data import salt.utils.files import salt.utils.network diff --git a/tests/support/mixins.py b/tests/support/mixins.py index 64a4bd4fbc..036c6fa3ba 100644 --- a/tests/support/mixins.py +++ b/tests/support/mixins.py @@ -31,7 +31,7 @@ from tests.support.paths import CODE_DIR # Import salt libs import salt.config -import salt.utils # Can be removed once namespaced_function is moved +import salt.utils # TODO: Remove this once namespaced_function is moved import salt.utils.event import salt.utils.files import salt.utils.path From 3eefd334c5dbc51a6916b257889b10c50a51e006 Mon Sep 17 00:00:00 2001 From: Pratik Bandarkar Date: Fri, 13 Oct 2017 14:29:39 +0000 Subject: [PATCH 595/633] Fixed "create_attach_volumes" salt-cloud action for GCP Fixes #44088 - "create_attach_volumes" salt-cloud works with mandatory and default arguments as mentioned in docstring of the function. --- salt/cloud/clouds/gce.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/salt/cloud/clouds/gce.py b/salt/cloud/clouds/gce.py index 950fa533e6..f554a25136 100644 --- a/salt/cloud/clouds/gce.py +++ b/salt/cloud/clouds/gce.py @@ -2400,9 +2400,10 @@ def create_attach_volumes(name, kwargs, call=None): '-a or --action.' ) - volumes = kwargs['volumes'] + volumes = literal_eval(kwargs['volumes']) node = kwargs['node'] - node_data = _expand_node(node) + conn = get_conn() + node_data = _expand_node(conn.ex_get_node(node)) letter = ord('a') - 1 for idx, volume in enumerate(volumes): @@ -2412,9 +2413,9 @@ def create_attach_volumes(name, kwargs, call=None): 'disk_name': volume_name, 'location': node_data['extra']['zone']['name'], 'size': volume['size'], - 'type': volume['type'], - 'image': volume['image'], - 'snapshot': volume['snapshot'] + 'type': volume.get('type', 'pd-standard'), + 'image': volume.get('image', None), + 'snapshot': volume.get('snapshot', None) } create_disk(volume_dict, 'function') From 4ab5d5d390e335ee05752e5373f85d3386c65d67 Mon Sep 17 00:00:00 2001 From: Thomas Quinot Date: Fri, 13 Oct 2017 16:34:49 +0200 Subject: [PATCH 596/633] Confusion between host name and address Keys in target dict are host names (as expanded by clustershell), not addresses. --- salt/roster/clustershell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/roster/clustershell.py b/salt/roster/clustershell.py index 1603c6dfba..1b68c06da9 100644 --- a/salt/roster/clustershell.py +++ b/salt/roster/clustershell.py @@ -43,7 +43,7 @@ def targets(tgt, tgt_type='glob', **kwargs): for host, addr in host_addrs.items(): addr = str(addr) - ret[addr] = __opts__.get('roster_defaults', {}).copy() + ret[host] = __opts__.get('roster_defaults', {}).copy() for port in ports: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -51,7 +51,7 @@ def targets(tgt, tgt_type='glob', **kwargs): sock.connect((addr, port)) sock.shutdown(socket.SHUT_RDWR) sock.close() - ret[host].update({'host': host, 'port': port}) + ret[host].update({'host': addr, 'port': port}) except socket.error: pass return ret From 1af21bbe5eb904bc9aaa22b5d8e746a18f25c9fb Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Fri, 13 Oct 2017 09:35:51 -0600 Subject: [PATCH 597/633] Made sure that unicoded data is sent to sha256() --- salt/utils/aws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/utils/aws.py b/salt/utils/aws.py index 8b9c5a9aed..84c093bd29 100644 --- a/salt/utils/aws.py +++ b/salt/utils/aws.py @@ -260,7 +260,7 @@ def sig4(method, endpoint, params, prov_dict, # Create payload hash (hash of the request body content). For GET # requests, the payload is an empty string (''). if not payload_hash: - payload_hash = hashlib.sha256(data).hexdigest() + payload_hash = hashlib.sha256(data.encode('utf-8')).hexdigest() # Combine elements to create create canonical request canonical_request = '\n'.join(( From 1e7211838dea16939024cf50be949d3a7ba3f8ae Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Fri, 13 Oct 2017 09:51:32 -0600 Subject: [PATCH 598/633] Use system encoding --- salt/utils/aws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/utils/aws.py b/salt/utils/aws.py index 84c093bd29..bf50b971d4 100644 --- a/salt/utils/aws.py +++ b/salt/utils/aws.py @@ -260,7 +260,7 @@ def sig4(method, endpoint, params, prov_dict, # Create payload hash (hash of the request body content). For GET # requests, the payload is an empty string (''). if not payload_hash: - payload_hash = hashlib.sha256(data.encode('utf-8')).hexdigest() + payload_hash = hashlib.sha256(data.encode(sys.getdefaultencoding())).hexdigest() # Combine elements to create create canonical request canonical_request = '\n'.join(( From 0e8b325667ca835a7a015a0b45ef02c1142a4a89 Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Fri, 13 Oct 2017 09:54:26 -0600 Subject: [PATCH 599/633] Apparently __salt_system_encoding__ is a thing --- salt/utils/aws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/utils/aws.py b/salt/utils/aws.py index bf50b971d4..ee1cc647cd 100644 --- a/salt/utils/aws.py +++ b/salt/utils/aws.py @@ -260,7 +260,7 @@ def sig4(method, endpoint, params, prov_dict, # Create payload hash (hash of the request body content). For GET # requests, the payload is an empty string (''). if not payload_hash: - payload_hash = hashlib.sha256(data.encode(sys.getdefaultencoding())).hexdigest() + payload_hash = hashlib.sha256(data.encode(__salt_system_encoding__)).hexdigest() # Combine elements to create create canonical request canonical_request = '\n'.join(( From bbd9db4d00d685c0c3f3c89272fbb2ee888f106b Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Fri, 13 Oct 2017 10:04:50 -0600 Subject: [PATCH 600/633] One more encoding --- salt/utils/aws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/utils/aws.py b/salt/utils/aws.py index ee1cc647cd..c87c4765da 100644 --- a/salt/utils/aws.py +++ b/salt/utils/aws.py @@ -278,7 +278,7 @@ def sig4(method, endpoint, params, prov_dict, algorithm, amzdate, credential_scope, - hashlib.sha256(canonical_request).hexdigest() + hashlib.sha256(canonical_request.encode(__salt_system_encoding__)).hexdigest() )) # Create the signing key using the function defined above. From c1a3b048a490f458a34a156e82b8f1fe2069c8ae Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Tue, 10 Oct 2017 08:44:57 -0700 Subject: [PATCH 601/633] Adding the ability to pass pillar values in when using state.sls_id --- salt/modules/state.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/salt/modules/state.py b/salt/modules/state.py index e5616af7d7..484bdd0e82 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -1377,6 +1377,21 @@ def sls_id(id_, mods, test=None, queue=False, **kwargs): :conf_minion:`pillarenv` minion config option nor this CLI argument is used, all Pillar environments will be merged together. + pillar + Custom Pillar values, passed as a dictionary of key-value pairs + + .. code-block:: bash + + salt '*' state.sls_id my_state my_module pillar='{"foo": "bar"}' + + .. note:: + Values passed this way will override Pillar values set via + ``pillar_roots`` or an external Pillar source. + + .. versionchanged:: Oxygen + GPG-encrypted CLI Pillar data is now supported via the GPG + renderer. See :ref:`here ` for details. + CLI Example: .. code-block:: bash @@ -1397,12 +1412,26 @@ def sls_id(id_, mods, test=None, queue=False, **kwargs): if opts['environment'] is None: opts['environment'] = 'base' + pillar_override = kwargs.get('pillar') + pillar_enc = kwargs.get('pillar_enc') + if pillar_enc is None \ + and pillar_override is not None \ + and not isinstance(pillar_override, dict): + raise SaltInvocationError( + 'Pillar data must be formatted as a dictionary, unless pillar_enc ' + 'is specified.' + ) + try: st_ = salt.state.HighState(opts, + pillar_override, + pillar_enc=pillar_enc, proxy=__proxy__, initial_pillar=_get_initial_pillar(opts)) except NameError: st_ = salt.state.HighState(opts, + pillar_override, + pillar_enc=pillar_enc, initial_pillar=_get_initial_pillar(opts)) if not _check_pillar(kwargs, st_.opts['pillar']): From cc962ba400ccd775e48efd3d780bfc2c0df7b697 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Tue, 10 Oct 2017 09:02:10 -0700 Subject: [PATCH 602/633] Updating docstring with right versionadded information --- salt/modules/state.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/salt/modules/state.py b/salt/modules/state.py index 484bdd0e82..85f6ad4189 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -1388,9 +1388,7 @@ def sls_id(id_, mods, test=None, queue=False, **kwargs): Values passed this way will override Pillar values set via ``pillar_roots`` or an external Pillar source. - .. versionchanged:: Oxygen - GPG-encrypted CLI Pillar data is now supported via the GPG - renderer. See :ref:`here ` for details. + .. versionadded:: Oxygen CLI Example: From b8a2cd754419fdae1e9b4a33fec2073f3dfbf293 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 13 Oct 2017 09:15:33 -0700 Subject: [PATCH 603/633] Clarifying documention for pillar kwarg for sls and sls_id. --- salt/modules/state.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/salt/modules/state.py b/salt/modules/state.py index 85f6ad4189..ba514e0e1e 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -921,8 +921,9 @@ def sls(mods, test=None, exclude=None, queue=False, **kwargs): salt '*' state.apply test pillar='{"foo": "bar"}' .. note:: - Values passed this way will override Pillar values set via - ``pillar_roots`` or an external Pillar source. + Values passed this way will override existing Pillar values set via + ``pillar_roots`` or an external Pillar source. Pillar values that + are not included in the kwarg will not be overwritten. .. versionchanged:: 2016.3.0 GPG-encrypted CLI Pillar data is now supported via the GPG @@ -1385,8 +1386,9 @@ def sls_id(id_, mods, test=None, queue=False, **kwargs): salt '*' state.sls_id my_state my_module pillar='{"foo": "bar"}' .. note:: - Values passed this way will override Pillar values set via - ``pillar_roots`` or an external Pillar source. + Values passed this way will override existing Pillar values set via + ``pillar_roots`` or an external Pillar source. Pillar values that + are not included in the kwarg will not be overwritten. .. versionadded:: Oxygen From cc43ca27af134876e140d680147becf1a4534be5 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 13 Oct 2017 15:47:19 -0600 Subject: [PATCH 604/633] Return multiprocessing queue in LogSetupMock class --- tests/unit/utils/test_parsers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/utils/test_parsers.py b/tests/unit/utils/test_parsers.py index ba4cc402d8..5f76b36800 100644 --- a/tests/unit/utils/test_parsers.py +++ b/tests/unit/utils/test_parsers.py @@ -78,7 +78,8 @@ class LogSetupMock(object): ''' Mock ''' - return None + import multiprocessing + return multiprocessing.Queue() def setup_multiprocessing_logging_listener(self, opts, *args): # pylint: disable=invalid-name,unused-argument ''' From caf086c05ad4fc39de5d18f75d8c0e3b79f6c19f Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 13 Oct 2017 16:02:01 -0600 Subject: [PATCH 605/633] Skip Master, Minion, and Syndic parser tests --- tests/unit/utils/test_parsers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/utils/test_parsers.py b/tests/unit/utils/test_parsers.py index ba4cc402d8..d613a122c4 100644 --- a/tests/unit/utils/test_parsers.py +++ b/tests/unit/utils/test_parsers.py @@ -21,6 +21,7 @@ import salt.utils.parsers import salt.log.setup as log import salt.config import salt.syspaths +import salt.utils class ErrorMock(object): # pylint: disable=too-few-public-methods @@ -488,6 +489,7 @@ class LogSettingsParserTests(TestCase): @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(salt.utils.is_windows(), 'Windows uses a logging listener') class MasterOptionParserTestCase(LogSettingsParserTests): ''' Tests parsing Salt Master options @@ -514,6 +516,7 @@ class MasterOptionParserTestCase(LogSettingsParserTests): @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(salt.utils.is_windows(), 'Windows uses a logging listener') class MinionOptionParserTestCase(LogSettingsParserTests): ''' Tests parsing Salt Minion options @@ -567,6 +570,7 @@ class ProxyMinionOptionParserTestCase(LogSettingsParserTests): @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(salt.utils.is_windows(), 'Windows uses a logging listener') class SyndicOptionParserTestCase(LogSettingsParserTests): ''' Tests parsing Salt Syndic options From b3761a040117f413e04da5b9c50a6dc7ee61fd6c Mon Sep 17 00:00:00 2001 From: Roald Nefs Date: Sat, 14 Oct 2017 13:24:09 +0200 Subject: [PATCH 606/633] Fix doc indentation in mattermost_returner --- salt/returners/mattermost_returner.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/salt/returners/mattermost_returner.py b/salt/returners/mattermost_returner.py index 3d022297de..46c78073cf 100644 --- a/salt/returners/mattermost_returner.py +++ b/salt/returners/mattermost_returner.py @@ -4,29 +4,43 @@ Return salt data via mattermost .. versionadded:: 2017.7.0 -The following fields can be set in the minion conf file:: +The following fields can be set in the minion conf file: + .. code-block:: yaml + mattermost.hook (required) mattermost.username (optional) mattermost.channel (optional) + Alternative configuration values can be used by prefacing the configuration. Any values not found in the alternative configuration will be pulled from the default location: + .. code-block:: yaml + mattermost.channel mattermost.hook mattermost.username + mattermost settings may also be configured as: + .. code-block:: yaml + mattermost: - channel: RoomName - hook: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - username: user + channel: RoomName + hook: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + username: user + To use the mattermost returner, append '--return mattermost' to the salt command. + .. code-block:: bash + salt '*' test.ping --return mattermost + To override individual configuration items, append --return_kwargs '{'key:': 'value'}' to the salt command. + .. code-block:: bash + salt '*' test.ping --return mattermost --return_kwargs '{'channel': '#random'}' ''' from __future__ import absolute_import @@ -53,6 +67,7 @@ __virtualname__ = 'mattermost' def __virtual__(): ''' Return virtual name of the module. + :return: The virtual name of the module. ''' return __virtualname__ @@ -118,6 +133,7 @@ def returner(ret): def event_return(events): ''' Send the events to a mattermost room. + :param events: List of events :return: Boolean if messages were sent successfully. ''' @@ -153,6 +169,7 @@ def post_message(channel, hook): ''' Send a message to a mattermost room. + :param channel: The room name. :param message: The message to send to the mattermost room. :param username: Specify who the message is from. From 877abb89d03d5caaf53fb0ee5ab2bf1cce2e8f17 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 13 Oct 2017 20:52:56 -0500 Subject: [PATCH 607/633] Complete the salt.utils refactor This moves the remaining 30 functions from salt.utils to new locations. --- doc/topics/cloud/cloud.rst | 24 +- salt/acl/__init__.py | 6 +- salt/auth/__init__.py | 25 +- salt/beacons/__init__.py | 2 +- salt/beacons/journald.py | 4 +- salt/beacons/sh.py | 1 - salt/cache/localfs.py | 1 - salt/cli/batch.py | 14 +- salt/cli/caller.py | 11 +- salt/cli/cp.py | 8 +- salt/cli/run.py | 6 +- salt/cli/salt.py | 47 +- salt/client/mixins.py | 3 +- salt/client/ssh/__init__.py | 6 +- salt/client/ssh/shell.py | 1 - salt/cloud/__init__.py | 26 +- salt/cloud/clouds/azurearm.py | 6 +- salt/cloud/clouds/cloudstack.py | 2 +- salt/cloud/clouds/dimensiondata.py | 5 +- salt/cloud/clouds/gce.py | 2 +- salt/cloud/clouds/libvirt.py | 1 - salt/cloud/clouds/lxc.py | 3 - salt/cloud/clouds/nova.py | 2 +- salt/cloud/clouds/openstack.py | 28 +- salt/cloud/clouds/packet.py | 2 +- salt/cloud/clouds/parallels.py | 1 - salt/cloud/clouds/proxmox.py | 1 - salt/cloud/clouds/pyrax.py | 8 +- salt/cloud/clouds/saltify.py | 2 +- salt/cloud/clouds/vmware.py | 1 - salt/cloud/libcloudfuncs.py | 4 +- salt/daemons/flo/jobber.py | 1 - salt/daemons/masterapi.py | 10 +- salt/engines/docker_events.py | 2 +- salt/engines/napalm_syslog.py | 23 +- salt/engines/slack.py | 1 - salt/exceptions.py | 5 +- salt/fileclient.py | 7 +- salt/fileserver/azurefs.py | 1 - salt/fileserver/hgfs.py | 4 +- salt/fileserver/minionfs.py | 4 +- salt/fileserver/roots.py | 3 +- salt/fileserver/s3fs.py | 1 - salt/fileserver/svnfs.py | 4 +- salt/grains/core.py | 1 - salt/key.py | 4 +- salt/master.py | 5 +- salt/minion.py | 10 +- salt/modules/apcups.py | 1 - salt/modules/aptpkg.py | 4 +- salt/modules/artifactory.py | 2 +- salt/modules/augeas_cfg.py | 1 - salt/modules/aws_sqs.py | 1 - salt/modules/bigip.py | 2 - salt/modules/boto_cloudtrail.py | 1 - salt/modules/boto_cognitoidentity.py | 1 - salt/modules/boto_elasticsearch_domain.py | 1 - salt/modules/boto_iot.py | 1 - salt/modules/boto_rds.py | 1 - salt/modules/boto_s3_bucket.py | 1 - salt/modules/cloud.py | 5 +- salt/modules/cp.py | 1 - salt/modules/cron.py | 6 +- salt/modules/debconfmod.py | 1 - salt/modules/dockermod.py | 11 +- salt/modules/ebuild.py | 4 +- salt/modules/file.py | 24 +- salt/modules/freebsdpkg.py | 8 +- salt/modules/git.py | 7 +- salt/modules/hashutil.py | 1 - salt/modules/incron.py | 6 +- salt/modules/kernelpkg_linux_yum.py | 3 +- salt/modules/kubernetes.py | 6 +- salt/modules/launchctl.py | 3 +- salt/modules/logadm.py | 1 - salt/modules/lxc.py | 8 +- salt/modules/mac_brew.py | 4 +- salt/modules/mac_group.py | 6 +- salt/modules/mac_ports.py | 4 +- salt/modules/mac_service.py | 5 +- salt/modules/mac_user.py | 1 - salt/modules/memcached.py | 6 +- salt/modules/mine.py | 8 +- salt/modules/network.py | 8 +- salt/modules/nilrt_ip.py | 1 - salt/modules/npm.py | 1 - salt/modules/nspawn.py | 10 +- salt/modules/openstack_config.py | 12 +- salt/modules/pacman.py | 8 +- salt/modules/pagerduty.py | 5 +- salt/modules/pcs.py | 1 - salt/modules/pdbedit.py | 1 - salt/modules/pillar.py | 4 +- salt/modules/pkgin.py | 4 +- salt/modules/pkgng.py | 12 +- salt/modules/pkgutil.py | 4 +- salt/modules/qemu_img.py | 1 - salt/modules/qemu_nbd.py | 1 - salt/modules/rpmbuild.py | 4 +- salt/modules/rsync.py | 2 +- salt/modules/saltutil.py | 6 +- salt/modules/scsi.py | 1 - salt/modules/smartos_vmadm.py | 5 +- salt/modules/solarisips.py | 4 +- salt/modules/solarispkg.py | 4 +- salt/modules/ssh.py | 1 - salt/modules/state.py | 4 +- salt/modules/supervisord.py | 1 - salt/modules/test.py | 8 +- salt/modules/upstart.py | 6 +- salt/modules/vagrant.py | 5 +- salt/modules/vcenter.py | 4 +- salt/modules/virt.py | 1 - salt/modules/vsphere.py | 3 +- salt/modules/win_file.py | 2 +- salt/modules/win_network.py | 9 +- salt/modules/win_pkg.py | 8 +- salt/modules/win_pki.py | 3 +- salt/modules/win_repo.py | 6 +- salt/modules/win_status.py | 2 +- salt/modules/win_system.py | 6 +- salt/modules/win_useradd.py | 4 +- salt/modules/yumpkg.py | 12 +- salt/modules/zabbix.py | 2 +- salt/modules/zookeeper.py | 1 - salt/modules/zypper.py | 6 +- salt/netapi/__init__.py | 4 +- salt/netapi/rest_cherrypy/app.py | 1 - salt/netapi/rest_tornado/saltnado.py | 13 +- .../rest_tornado/saltnado_websockets.py | 4 +- salt/output/__init__.py | 4 +- salt/pillar/digicert.py | 1 - salt/pillar/django_orm.py | 1 - salt/pillar/gpg.py | 2 +- salt/pillar/hg_pillar.py | 1 - salt/pillar/venafi.py | 1 - salt/renderers/json.py | 4 +- salt/renderers/nacl.py | 1 - salt/returners/couchbase_return.py | 5 +- salt/returners/etcd_return.py | 1 - salt/returners/librato_return.py | 1 - salt/returners/postgres_local_cache.py | 1 - salt/returners/rawfile_json.py | 1 - salt/returners/sms_return.py | 3 - salt/returners/zabbix_return.py | 2 +- salt/roster/cloud.py | 1 - salt/roster/sshconfig.py | 4 +- salt/runners/mine.py | 1 - salt/runners/pagerduty.py | 6 +- salt/runners/state.py | 8 +- salt/state.py | 20 +- salt/states/cmd.py | 4 +- salt/states/docker_container.py | 1 - salt/states/event.py | 6 +- salt/states/file.py | 26 +- salt/states/libcloud_dns.py | 1 - salt/states/libcloud_loadbalancer.py | 1 - salt/states/libcloud_storage.py | 1 - salt/states/module.py | 7 +- salt/states/pkg.py | 2 +- salt/states/rabbitmq_cluster.py | 4 +- salt/states/user.py | 6 +- salt/states/virtualenv_mod.py | 4 +- salt/states/win_system.py | 4 +- salt/states/winrepo.py | 1 - salt/states/x509.py | 1 - salt/states/zone.py | 3 +- salt/thorium/check.py | 4 +- salt/thorium/file.py | 4 +- salt/thorium/reg.py | 8 +- salt/tokens/localfs.py | 2 +- salt/transport/tcp.py | 1 - salt/transport/zeromq.py | 5 +- salt/utils/__init__.py | 1333 ++++++----------- salt/utils/args.py | 143 ++ salt/utils/boto.py | 1 - salt/utils/boto3.py | 1 - salt/utils/cloud.py | 3 +- salt/utils/data.py | 99 +- salt/utils/dateutils.py | 94 ++ salt/utils/files.py | 32 +- salt/utils/functools.py | 58 + salt/utils/gitfs.py | 5 +- salt/utils/itertools.py | 23 + salt/utils/json.py | 44 + salt/utils/mac_utils.py | 1 - salt/utils/master.py | 40 +- salt/utils/minions.py | 4 +- salt/utils/parsers.py | 5 +- salt/utils/path.py | 31 + salt/utils/profile.py | 100 ++ salt/utils/pycrypto.py | 2 - salt/utils/reactor.py | 6 +- salt/utils/shlex.py | 21 - salt/utils/stringutils.py | 200 ++- salt/utils/systemd.py | 1 - salt/utils/templates.py | 42 +- salt/utils/thin.py | 2 +- salt/utils/url.py | 6 +- salt/utils/win_functions.py | 9 + .../netapi/rest_cherrypy/test_app.py | 1 - .../netapi/rest_tornado/test_app.py | 4 +- tests/integration/runners/test_state.py | 1 - tests/integration/states/test_pkg.py | 3 +- tests/support/mixins.py | 4 +- tests/support/unit.py | 11 + tests/unit/beacons/test_journald_beacon.py | 4 +- tests/unit/cache/test_cache.py | 1 - tests/unit/config/test_config.py | 6 +- tests/unit/grains/test_core.py | 1 - tests/unit/modules/test_boto_lambda.py | 1 - tests/unit/modules/test_disk.py | 2 +- tests/unit/modules/test_file.py | 36 +- tests/unit/modules/test_hosts.py | 2 +- tests/unit/modules/test_ini_manage.py | 3 +- tests/unit/modules/test_launchctl.py | 1 - tests/unit/modules/test_pagerduty.py | 2 +- tests/unit/modules/test_virt.py | 1 - tests/unit/modules/test_win_network.py | 2 +- tests/unit/output/test_highstate_out.py | 1 - tests/unit/runners/test_cache.py | 2 +- tests/unit/states/test_file.py | 1 - tests/unit/test_auth.py | 2 +- tests/unit/test_crypt.py | 1 - tests/unit/test_ssh_config_roster.py | 4 +- tests/unit/transport/test_ipc.py | 1 - tests/unit/transport/test_zeromq.py | 2 +- tests/unit/utils/test_args.py | 40 +- tests/unit/utils/test_data.py | 195 ++- tests/unit/utils/test_dateutils.py | 70 + tests/unit/utils/test_files.py | 31 + tests/unit/utils/test_format_call.py | 56 - tests/unit/utils/test_jid.py | 47 + tests/unit/utils/test_json.py | 58 + tests/unit/utils/test_path.py | 15 +- .../utils/test_runtime_whitespace_regex.py | 2 +- tests/unit/utils/test_safe_walk.py | 47 - tests/unit/utils/test_stringutils.py | 9 +- tests/unit/utils/test_templates.py | 25 + tests/unit/utils/test_utils.py | 419 ------ tests/unit/utils/test_yamlencoding.py | 38 + tests/unit/utils/test_zeromq.py | 18 +- 242 files changed, 2332 insertions(+), 2009 deletions(-) create mode 100644 salt/utils/dateutils.py create mode 100644 salt/utils/functools.py create mode 100644 salt/utils/json.py create mode 100644 salt/utils/profile.py delete mode 100644 salt/utils/shlex.py create mode 100644 tests/unit/utils/test_dateutils.py delete mode 100644 tests/unit/utils/test_format_call.py create mode 100644 tests/unit/utils/test_jid.py create mode 100644 tests/unit/utils/test_json.py delete mode 100644 tests/unit/utils/test_safe_walk.py create mode 100644 tests/unit/utils/test_templates.py delete mode 100644 tests/unit/utils/test_utils.py create mode 100644 tests/unit/utils/test_yamlencoding.py diff --git a/doc/topics/cloud/cloud.rst b/doc/topics/cloud/cloud.rst index 568866a7e4..78c9f0d64e 100644 --- a/doc/topics/cloud/cloud.rst +++ b/doc/topics/cloud/cloud.rst @@ -146,24 +146,24 @@ library. The following two lines set up the imports: .. code-block:: python from salt.cloud.libcloudfuncs import * # pylint: disable=W0614,W0401 - import salt.utils + import salt.utils.functools And then a series of declarations will make the necessary functions available within the cloud module. .. code-block:: python - get_size = salt.utils.namespaced_function(get_size, globals()) - get_image = salt.utils.namespaced_function(get_image, globals()) - avail_locations = salt.utils.namespaced_function(avail_locations, globals()) - avail_images = salt.utils.namespaced_function(avail_images, globals()) - avail_sizes = salt.utils.namespaced_function(avail_sizes, globals()) - script = salt.utils.namespaced_function(script, globals()) - destroy = salt.utils.namespaced_function(destroy, globals()) - list_nodes = salt.utils.namespaced_function(list_nodes, globals()) - list_nodes_full = salt.utils.namespaced_function(list_nodes_full, globals()) - list_nodes_select = salt.utils.namespaced_function(list_nodes_select, globals()) - show_instance = salt.utils.namespaced_function(show_instance, globals()) + get_size = salt.utils.functools.namespaced_function(get_size, globals()) + get_image = salt.utils.functools.namespaced_function(get_image, globals()) + avail_locations = salt.utils.functools.namespaced_function(avail_locations, globals()) + avail_images = salt.utils.functools.namespaced_function(avail_images, globals()) + avail_sizes = salt.utils.functools.namespaced_function(avail_sizes, globals()) + script = salt.utils.functools.namespaced_function(script, globals()) + destroy = salt.utils.functools.namespaced_function(destroy, globals()) + list_nodes = salt.utils.functools.namespaced_function(list_nodes, globals()) + list_nodes_full = salt.utils.functools.namespaced_function(list_nodes_full, globals()) + list_nodes_select = salt.utils.functools.namespaced_function(list_nodes_select, globals()) + show_instance = salt.utils.functools.namespaced_function(show_instance, globals()) If necessary, these functions may be replaced by removing the appropriate declaration line, and then adding the function as normal. diff --git a/salt/acl/__init__.py b/salt/acl/__init__.py index 00efe2e40c..b5bc8a73e0 100644 --- a/salt/acl/__init__.py +++ b/salt/acl/__init__.py @@ -12,7 +12,7 @@ found by reading the salt documentation: from __future__ import absolute_import # Import salt libs -import salt.utils +import salt.utils.stringutils # Import 3rd-party libs from salt.ext import six @@ -31,13 +31,13 @@ class PublisherACL(object): Takes a username as a string and returns a boolean. True indicates that the provided user has been blacklisted ''' - return not salt.utils.check_whitelist_blacklist(user, blacklist=self.blacklist.get('users', [])) + return not salt.utils.stringutils.check_whitelist_blacklist(user, blacklist=self.blacklist.get('users', [])) def cmd_is_blacklisted(self, cmd): # If this is a regular command, it is a single function if isinstance(cmd, six.string_types): cmd = [cmd] for fun in cmd: - if not salt.utils.check_whitelist_blacklist(fun, blacklist=self.blacklist.get('modules', [])): + if not salt.utils.stringutils.check_whitelist_blacklist(fun, blacklist=self.blacklist.get('modules', [])): return True return False diff --git a/salt/auth/__init__.py b/salt/auth/__init__.py index 9c9bb9855e..6e0da617c2 100644 --- a/salt/auth/__init__.py +++ b/salt/auth/__init__.py @@ -28,12 +28,12 @@ from salt.ext.six.moves import input import salt.config import salt.loader import salt.transport.client -import salt.utils import salt.utils.args +import salt.utils.dictupdate import salt.utils.files import salt.utils.minions -import salt.utils.versions import salt.utils.user +import salt.utils.versions import salt.utils.zeromq import salt.payload @@ -89,9 +89,10 @@ class LoadAuth(object): fstr = '{0}.auth'.format(load['eauth']) if fstr not in self.auth: return False - fcall = salt.utils.format_call(self.auth[fstr], - load, - expected_extra_kws=AUTH_INTERNAL_KEYWORDS) + fcall = salt.utils.args.format_call( + self.auth[fstr], + load, + expected_extra_kws=AUTH_INTERNAL_KEYWORDS) try: if 'kwargs' in fcall: return self.auth[fstr](*fcall['args'], **fcall['kwargs']) @@ -135,9 +136,10 @@ class LoadAuth(object): fstr = '{0}.acl'.format(mod) if fstr not in self.auth: return None - fcall = salt.utils.format_call(self.auth[fstr], - load, - expected_extra_kws=AUTH_INTERNAL_KEYWORDS) + fcall = salt.utils.args.format_call( + self.auth[fstr], + load, + expected_extra_kws=AUTH_INTERNAL_KEYWORDS) try: return self.auth[fstr](*fcall['args'], **fcall['kwargs']) except Exception as e: @@ -170,9 +172,10 @@ class LoadAuth(object): fstr = '{0}.groups'.format(load['eauth']) if fstr not in self.auth: return False - fcall = salt.utils.format_call(self.auth[fstr], - load, - expected_extra_kws=AUTH_INTERNAL_KEYWORDS) + fcall = salt.utils.args.format_call( + self.auth[fstr], + load, + expected_extra_kws=AUTH_INTERNAL_KEYWORDS) try: return self.auth[fstr](*fcall['args'], **fcall['kwargs']) except IndexError: diff --git a/salt/beacons/__init__.py b/salt/beacons/__init__.py index 54bea7aa96..0830e39b5d 100644 --- a/salt/beacons/__init__.py +++ b/salt/beacons/__init__.py @@ -10,7 +10,7 @@ import re # Import Salt libs import salt.loader -import salt.utils +import salt.utils.event import salt.utils.minion from salt.ext.six.moves import map from salt.exceptions import CommandExecutionError diff --git a/salt/beacons/journald.py b/salt/beacons/journald.py index 9b566d587d..27807a5947 100644 --- a/salt/beacons/journald.py +++ b/salt/beacons/journald.py @@ -7,7 +7,7 @@ A simple beacon to watch journald for specific entries from __future__ import absolute_import # Import salt libs -import salt.utils +import salt.utils.data import salt.utils.locales import salt.ext.six from salt.ext.six.moves import map @@ -99,7 +99,7 @@ def beacon(config): n_flag += 1 if n_flag == len(_config['services'][name]): # Match! - sub = salt.utils.simple_types_filter(cur) + sub = salt.utils.data.simple_types_filter(cur) sub.update({'tag': name}) ret.append(sub) return ret diff --git a/salt/beacons/sh.py b/salt/beacons/sh.py index 7375a1352a..2fdb521719 100644 --- a/salt/beacons/sh.py +++ b/salt/beacons/sh.py @@ -8,7 +8,6 @@ from __future__ import absolute_import import time # Import salt libs -import salt.utils import salt.utils.path import salt.utils.vt diff --git a/salt/cache/localfs.py b/salt/cache/localfs.py index 0a73d62403..16c299e955 100644 --- a/salt/cache/localfs.py +++ b/salt/cache/localfs.py @@ -18,7 +18,6 @@ import shutil import tempfile from salt.exceptions import SaltCacheError -import salt.utils import salt.utils.atomicfile import salt.utils.files diff --git a/salt/cli/batch.py b/salt/cli/batch.py index 6e0c5b7ac6..a39acd5705 100644 --- a/salt/cli/batch.py +++ b/salt/cli/batch.py @@ -11,7 +11,7 @@ import copy from datetime import datetime, timedelta # Import salt libs -import salt.utils # TODO: Remove this once print_cli is moved +import salt.utils.stringutils import salt.client import salt.output import salt.exceptions @@ -73,7 +73,7 @@ class Batch(object): m = next(six.iterkeys(ret)) except StopIteration: if not self.quiet: - salt.utils.print_cli('No minions matched the target.') + salt.utils.stringutils.print_cli('No minions matched the target.') break if m is not None: fret.add(m) @@ -95,7 +95,7 @@ class Batch(object): return int(self.opts['batch']) except ValueError: if not self.quiet: - salt.utils.print_cli('Invalid batch data sent: {0}\nData must be in the ' + salt.utils.stringutils.print_cli('Invalid batch data sent: {0}\nData must be in the ' 'form of %10, 10% or 3'.format(self.opts['batch'])) def __update_wait(self, wait): @@ -147,7 +147,7 @@ class Batch(object): # We already know some minions didn't respond to the ping, so inform # the user we won't be attempting to run a job on them for down_minion in self.down_minions: - salt.utils.print_cli('Minion {0} did not respond. No job will be sent.'.format(down_minion)) + salt.utils.stringutils.print_cli('Minion {0} did not respond. No job will be sent.'.format(down_minion)) # Iterate while we still have things to execute while len(ret) < len(self.minions): @@ -172,7 +172,7 @@ class Batch(object): if next_: if not self.quiet: - salt.utils.print_cli('\nExecuting run on {0}\n'.format(sorted(next_))) + salt.utils.stringutils.print_cli('\nExecuting run on {0}\n'.format(sorted(next_))) # create a new iterator for this batch of minions new_iter = self.local.cmd_iter_no_block( *args, @@ -219,14 +219,14 @@ class Batch(object): if part['data']['id'] in minion_tracker[queue]['minions']: minion_tracker[queue]['minions'].remove(part['data']['id']) else: - salt.utils.print_cli('minion {0} was already deleted from tracker, probably a duplicate key'.format(part['id'])) + salt.utils.stringutils.print_cli('minion {0} was already deleted from tracker, probably a duplicate key'.format(part['id'])) else: parts.update(part) for id in part: if id in minion_tracker[queue]['minions']: minion_tracker[queue]['minions'].remove(id) else: - salt.utils.print_cli('minion {0} was already deleted from tracker, probably a duplicate key'.format(id)) + salt.utils.stringutils.print_cli('minion {0} was already deleted from tracker, probably a duplicate key'.format(id)) except StopIteration: # if a iterator is done: # - set it to inactive diff --git a/salt/cli/caller.py b/salt/cli/caller.py index dca0b70edf..f681e0cd0e 100644 --- a/salt/cli/caller.py +++ b/salt/cli/caller.py @@ -20,12 +20,13 @@ import salt.minion import salt.output import salt.payload import salt.transport -import salt.utils # TODO: Remove this once print_cli, activate_profile, and output_profile are moved import salt.utils.args import salt.utils.files import salt.utils.jid import salt.utils.kinds as kinds import salt.utils.minion +import salt.utils.profile +import salt.utils.stringutils import salt.defaults.exitcodes from salt.cli import daemons from salt.log import LOG_LEVELS @@ -113,7 +114,7 @@ class BaseCaller(object): docs[name] = func.__doc__ for name in sorted(docs): if name.startswith(self.opts.get('fun', '')): - salt.utils.print_cli('{0}:\n{1}\n'.format(name, docs[name])) + salt.utils.stringutils.print_cli('{0}:\n{1}\n'.format(name, docs[name])) def print_grains(self): ''' @@ -128,11 +129,11 @@ class BaseCaller(object): ''' profiling_enabled = self.opts.get('profiling_enabled', False) try: - pr = salt.utils.activate_profile(profiling_enabled) + pr = salt.utils.profile.activate_profile(profiling_enabled) try: ret = self.call() finally: - salt.utils.output_profile( + salt.utils.profile.output_profile( pr, stats_path=self.opts.get('profiling_path', '/tmp/stats'), stop=True) @@ -209,7 +210,7 @@ class BaseCaller(object): ret['return'] = func(*args, **kwargs) except TypeError as exc: sys.stderr.write('\nPassed invalid arguments: {0}.\n\nUsage:\n'.format(exc)) - salt.utils.print_cli(func.__doc__) + salt.utils.stringutils.print_cli(func.__doc__) active_level = LOG_LEVELS.get( self.opts['log_level'].lower(), logging.ERROR) if active_level <= logging.DEBUG: diff --git a/salt/cli/cp.py b/salt/cli/cp.py index b45edc0c4d..f7ed13b8b1 100644 --- a/salt/cli/cp.py +++ b/salt/cli/cp.py @@ -19,7 +19,6 @@ import sys # Import salt libs import salt.client import salt.output -import salt.utils import salt.utils.files import salt.utils.gzip_util import salt.utils.itertools @@ -127,9 +126,10 @@ class SaltCP(object): if os.path.isfile(fn_): files.update(self._file_dict(fn_)) elif os.path.isdir(fn_): - salt.utils.print_cli(fn_ + ' is a directory, only files are supported ' - 'in non-chunked mode. Use "--chunked" command ' - 'line argument.') + salt.utils.stringutils.print_cli( + fn_ + ' is a directory, only files are supported ' + 'in non-chunked mode. Use "--chunked" command ' + 'line argument.') sys.exit(1) return files diff --git a/salt/cli/run.py b/salt/cli/run.py index f54e984252..76c5c469d4 100644 --- a/salt/cli/run.py +++ b/salt/cli/run.py @@ -2,8 +2,8 @@ from __future__ import print_function from __future__ import absolute_import -import salt.utils # TODO: Remove this once activate_profile and output_profile are moved import salt.utils.parsers +import salt.utils.profile from salt.utils.verify import check_user, verify_log from salt.exceptions import SaltClientError import salt.defaults.exitcodes # pylint: disable=W0611 @@ -35,7 +35,7 @@ class SaltRun(salt.utils.parsers.SaltRunOptionParser): # someone tries to use the runners via the python API try: if check_user(self.config['user']): - pr = salt.utils.activate_profile(profiling_enabled) + pr = salt.utils.profile.activate_profile(profiling_enabled) try: ret = runner.run() # In older versions ret['data']['retcode'] was used @@ -49,7 +49,7 @@ class SaltRun(salt.utils.parsers.SaltRunOptionParser): elif isinstance(ret, dict) and 'retcode' in ret.get('data', {}): self.exit(ret['data']['retcode']) finally: - salt.utils.output_profile( + salt.utils.profile.output_profile( pr, stats_path=self.options.profiling_path, stop=True) diff --git a/salt/cli/salt.py b/salt/cli/salt.py index 814140ff12..df10b0ab1c 100644 --- a/salt/cli/salt.py +++ b/salt/cli/salt.py @@ -7,8 +7,9 @@ sys.modules['pkg_resources'] = None import os # Import Salt libs -import salt.utils # TODO: Remove this once print_cli is moved +import salt.utils.job import salt.utils.parsers +import salt.utils.stringutils from salt.utils.args import yamlify_arg from salt.utils.verify import verify_log from salt.exceptions import ( @@ -93,7 +94,7 @@ class SaltCMD(salt.utils.parsers.SaltCMDOptionParser): # potentially switch to batch execution if self.options.batch_safe_limit > 1: if len(self._preview_target()) >= self.options.batch_safe_limit: - salt.utils.print_cli('\nNOTICE: Too many minions targeted, switching to batch execution.') + salt.utils.stringutils.print_cli('\nNOTICE: Too many minions targeted, switching to batch execution.') self.options.batch = self.options.batch_safe_size self._run_batch() return @@ -140,7 +141,7 @@ class SaltCMD(salt.utils.parsers.SaltCMDOptionParser): if self.config['async']: jid = self.local_client.cmd_async(**kwargs) - salt.utils.print_cli('Executed command with job ID: {0}'.format(jid)) + salt.utils.stringutils.print_cli('Executed command with job ID: {0}'.format(jid)) return # local will be None when there was an error @@ -279,12 +280,12 @@ class SaltCMD(salt.utils.parsers.SaltCMDOptionParser): def _print_errors_summary(self, errors): if errors: - salt.utils.print_cli('\n') - salt.utils.print_cli('---------------------------') - salt.utils.print_cli('Errors') - salt.utils.print_cli('---------------------------') + salt.utils.stringutils.print_cli('\n') + salt.utils.stringutils.print_cli('---------------------------') + salt.utils.stringutils.print_cli('Errors') + salt.utils.stringutils.print_cli('---------------------------') for error in errors: - salt.utils.print_cli(self._format_error(error)) + salt.utils.stringutils.print_cli(self._format_error(error)) def _print_returns_summary(self, ret): ''' @@ -314,22 +315,22 @@ class SaltCMD(salt.utils.parsers.SaltCMDOptionParser): return_counter += 1 if self._get_retcode(ret[each_minion]): failed_minions.append(each_minion) - salt.utils.print_cli('\n') - salt.utils.print_cli('-------------------------------------------') - salt.utils.print_cli('Summary') - salt.utils.print_cli('-------------------------------------------') - salt.utils.print_cli('# of minions targeted: {0}'.format(return_counter + not_return_counter)) - salt.utils.print_cli('# of minions returned: {0}'.format(return_counter)) - salt.utils.print_cli('# of minions that did not return: {0}'.format(not_return_counter)) - salt.utils.print_cli('# of minions with errors: {0}'.format(len(failed_minions))) + salt.utils.stringutils.print_cli('\n') + salt.utils.stringutils.print_cli('-------------------------------------------') + salt.utils.stringutils.print_cli('Summary') + salt.utils.stringutils.print_cli('-------------------------------------------') + salt.utils.stringutils.print_cli('# of minions targeted: {0}'.format(return_counter + not_return_counter)) + salt.utils.stringutils.print_cli('# of minions returned: {0}'.format(return_counter)) + salt.utils.stringutils.print_cli('# of minions that did not return: {0}'.format(not_return_counter)) + salt.utils.stringutils.print_cli('# of minions with errors: {0}'.format(len(failed_minions))) if self.options.verbose: if not_connected_minions: - salt.utils.print_cli('Minions not connected: {0}'.format(" ".join(not_connected_minions))) + salt.utils.stringutils.print_cli('Minions not connected: {0}'.format(" ".join(not_connected_minions))) if not_response_minions: - salt.utils.print_cli('Minions not responding: {0}'.format(" ".join(not_response_minions))) + salt.utils.stringutils.print_cli('Minions not responding: {0}'.format(" ".join(not_response_minions))) if failed_minions: - salt.utils.print_cli('Minions with failures: {0}'.format(" ".join(failed_minions))) - salt.utils.print_cli('-------------------------------------------') + salt.utils.stringutils.print_cli('Minions with failures: {0}'.format(" ".join(failed_minions))) + salt.utils.stringutils.print_cli('-------------------------------------------') def _progress_end(self, out): import salt.output @@ -421,6 +422,6 @@ class SaltCMD(salt.utils.parsers.SaltCMDOptionParser): salt.output.display_output({fun: docs[fun]}, 'nested', self.config) else: for fun in sorted(docs): - salt.utils.print_cli('{0}:'.format(fun)) - salt.utils.print_cli(docs[fun]) - salt.utils.print_cli('') + salt.utils.stringutils.print_cli('{0}:'.format(fun)) + salt.utils.stringutils.print_cli(docs[fun]) + salt.utils.stringutils.print_cli('') diff --git a/salt/client/mixins.py b/salt/client/mixins.py index 9b851c6ad9..76a202fcdd 100644 --- a/salt/client/mixins.py +++ b/salt/client/mixins.py @@ -16,7 +16,6 @@ import copy as pycopy # Import Salt libs import salt.exceptions import salt.minion -import salt.utils # TODO: Remove this once format_call is moved import salt.utils.args import salt.utils.doc import salt.utils.error @@ -371,7 +370,7 @@ class SyncClientMixin(object): args = low[u'arg'] kwargs = low[u'kwarg'] else: - f_call = salt.utils.format_call( + f_call = salt.utils.args.format_call( self.functions[fun], low, expected_extra_kws=CLIENT_INTERNAL_KEYWORDS diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py index 96f20ba023..28a64da7da 100644 --- a/salt/client/ssh/__init__.py +++ b/salt/client/ssh/__init__.py @@ -36,12 +36,12 @@ import salt.minion import salt.roster import salt.serializers.yaml import salt.state -import salt.utils import salt.utils.args import salt.utils.atomicfile import salt.utils.event import salt.utils.files import salt.utils.hashutils +import salt.utils.json import salt.utils.network import salt.utils.path import salt.utils.stringutils @@ -497,7 +497,7 @@ class SSH(object): **target) stdout, stderr, retcode = single.cmd_block() try: - data = salt.utils.find_json(stdout) + data = salt.utils.json.find_json(stdout) return {host: data.get(u'local', data)} except Exception: if stderr: @@ -525,7 +525,7 @@ class SSH(object): stdout, stderr, retcode = single.run() # This job is done, yield try: - data = salt.utils.find_json(stdout) + data = salt.utils.json.find_json(stdout) if len(data) < 2 and u'local' in data: ret[u'ret'] = data[u'local'] else: diff --git a/salt/client/ssh/shell.py b/salt/client/ssh/shell.py index a3b59abe95..eabe92ed22 100644 --- a/salt/client/ssh/shell.py +++ b/salt/client/ssh/shell.py @@ -15,7 +15,6 @@ import subprocess # Import salt libs import salt.defaults.exitcodes -import salt.utils import salt.utils.nb_popen import salt.utils.vt diff --git a/salt/cloud/__init__.py b/salt/cloud/__init__.py index fc48740255..595df07871 100644 --- a/salt/cloud/__init__.py +++ b/salt/cloud/__init__.py @@ -29,11 +29,11 @@ from salt.exceptions import ( import salt.config import salt.client import salt.loader -import salt.utils import salt.utils.args import salt.utils.cloud import salt.utils.context import salt.utils.crypt +import salt.utils.data import salt.utils.dictupdate import salt.utils.files import salt.syspaths @@ -244,7 +244,7 @@ class CloudClient(object): Pass the cloud function and low data structure to run ''' l_fun = getattr(self, fun) - f_call = salt.utils.format_call(l_fun, low) + f_call = salt.utils.args.format_call(l_fun, low) return l_fun(*f_call.get('args', ()), **f_call.get('kwargs', {})) def list_sizes(self, provider=None): @@ -252,7 +252,7 @@ class CloudClient(object): List all available sizes in configured cloud systems ''' mapper = salt.cloud.Map(self._opts_defaults()) - return salt.utils.simple_types_filter( + return salt.utils.data.simple_types_filter( mapper.size_list(provider) ) @@ -261,7 +261,7 @@ class CloudClient(object): List all available images in configured cloud systems ''' mapper = salt.cloud.Map(self._opts_defaults()) - return salt.utils.simple_types_filter( + return salt.utils.data.simple_types_filter( mapper.image_list(provider) ) @@ -270,7 +270,7 @@ class CloudClient(object): List all available locations in configured cloud systems ''' mapper = salt.cloud.Map(self._opts_defaults()) - return salt.utils.simple_types_filter( + return salt.utils.data.simple_types_filter( mapper.location_list(provider) ) @@ -344,7 +344,7 @@ class CloudClient(object): mapper = salt.cloud.Map(self._opts_defaults(**kwargs)) if isinstance(names, six.string_types): names = names.split(',') - return salt.utils.simple_types_filter( + return salt.utils.data.simple_types_filter( mapper.run_profile(profile, names, vm_overrides=vm_overrides) ) @@ -358,7 +358,7 @@ class CloudClient(object): kwarg.update(kwargs) mapper = salt.cloud.Map(self._opts_defaults(**kwarg)) dmap = mapper.map_data() - return salt.utils.simple_types_filter( + return salt.utils.data.simple_types_filter( mapper.run_map(dmap) ) @@ -369,7 +369,7 @@ class CloudClient(object): mapper = salt.cloud.Map(self._opts_defaults(destroy=True)) if isinstance(names, six.string_types): names = names.split(',') - return salt.utils.simple_types_filter( + return salt.utils.data.simple_types_filter( mapper.destroy(names) ) @@ -408,7 +408,7 @@ class CloudClient(object): vm_['profile'] = None vm_['provider'] = provider - ret[name] = salt.utils.simple_types_filter( + ret[name] = salt.utils.data.simple_types_filter( mapper.create(vm_)) return ret @@ -443,7 +443,7 @@ class CloudClient(object): extra_['provider'] = provider extra_['profile'] = None extra_['action'] = action - ret[name] = salt.utils.simple_types_filter( + ret[name] = salt.utils.data.simple_types_filter( mapper.extras(extra_) ) return ret @@ -2324,7 +2324,7 @@ def create_multiprocessing(parallel_data, queue=None): output.pop('deploy_kwargs', None) return { - parallel_data['name']: salt.utils.simple_types_filter(output) + parallel_data['name']: salt.utils.data.simple_types_filter(output) } @@ -2360,7 +2360,7 @@ def destroy_multiprocessing(parallel_data, queue=None): return {parallel_data['name']: {'Error': str(exc)}} return { - parallel_data['name']: salt.utils.simple_types_filter(output) + parallel_data['name']: salt.utils.data.simple_types_filter(output) } @@ -2383,7 +2383,7 @@ def run_parallel_map_providers_query(data, queue=None): return ( data['alias'], data['driver'], - salt.utils.simple_types_filter( + salt.utils.data.simple_types_filter( cloud.clouds[data['fun']]() ) ) diff --git a/salt/cloud/clouds/azurearm.py b/salt/cloud/clouds/azurearm.py index a22b05b20d..835495b420 100644 --- a/salt/cloud/clouds/azurearm.py +++ b/salt/cloud/clouds/azurearm.py @@ -63,8 +63,8 @@ import yaml import collections import salt.cache import salt.config as config -import salt.utils import salt.utils.cloud +import salt.utils.data import salt.utils.files from salt.utils.versions import LooseVersion from salt.ext import six @@ -575,7 +575,7 @@ def show_instance(name, resource_group=None, call=None): # pylint: disable=unus data['resource_group'] = resource_group __utils__['cloud.cache_node']( - salt.utils.simple_types_filter(data), + salt.utils.data.simple_types_filter(data), __active_provider_name__, __opts__ ) @@ -1460,7 +1460,7 @@ def make_safe(data): ''' Turn object data into something serializable ''' - return salt.utils.simple_types_filter(object_to_dict(data)) + return salt.utils.data.simple_types_filter(object_to_dict(data)) def create_security_group(call=None, kwargs=None): # pylint: disable=unused-argument diff --git a/salt/cloud/clouds/cloudstack.py b/salt/cloud/clouds/cloudstack.py index 8732f05f47..06d667a54b 100644 --- a/salt/cloud/clouds/cloudstack.py +++ b/salt/cloud/clouds/cloudstack.py @@ -33,7 +33,7 @@ import salt.config as config import salt.utils.cloud import salt.utils.event from salt.cloud.libcloudfuncs import * # pylint: disable=redefined-builtin,wildcard-import,unused-wildcard-import -from salt.utils import namespaced_function +from salt.utils.functools import namespaced_function from salt.exceptions import SaltCloudSystemExit from salt.utils.versions import LooseVersion as _LooseVersion diff --git a/salt/cloud/clouds/dimensiondata.py b/salt/cloud/clouds/dimensiondata.py index d022c5719f..04ba82b4e0 100644 --- a/salt/cloud/clouds/dimensiondata.py +++ b/salt/cloud/clouds/dimensiondata.py @@ -55,12 +55,9 @@ except ImportError: # Import generic libcloud functions # from salt.cloud.libcloudfuncs import * -# Import salt libs -import salt.utils - # Import salt.cloud libs from salt.cloud.libcloudfuncs import * # pylint: disable=redefined-builtin,wildcard-import,unused-wildcard-import -from salt.utils import namespaced_function +from salt.utils.functools import namespaced_function import salt.utils.cloud import salt.config as config from salt.exceptions import ( diff --git a/salt/cloud/clouds/gce.py b/salt/cloud/clouds/gce.py index 6e26cf8b95..4418e2674b 100644 --- a/salt/cloud/clouds/gce.py +++ b/salt/cloud/clouds/gce.py @@ -82,7 +82,7 @@ except ImportError: # pylint: enable=import-error # Import salt libs -from salt.utils import namespaced_function +from salt.utils.functools import namespaced_function from salt.ext import six import salt.utils.cloud import salt.utils.files diff --git a/salt/cloud/clouds/libvirt.py b/salt/cloud/clouds/libvirt.py index 8767c96425..6519ba066f 100644 --- a/salt/cloud/clouds/libvirt.py +++ b/salt/cloud/clouds/libvirt.py @@ -74,7 +74,6 @@ except ImportError: # Import salt libs import salt.config as config -import salt.utils import salt.utils.cloud from salt.exceptions import ( SaltCloudConfigError, diff --git a/salt/cloud/clouds/lxc.py b/salt/cloud/clouds/lxc.py index 3043874e3f..bb26a8fab4 100644 --- a/salt/cloud/clouds/lxc.py +++ b/salt/cloud/clouds/lxc.py @@ -17,9 +17,6 @@ import copy import time from pprint import pformat -# Import salt libs -import salt.utils - # Import salt cloud libs import salt.utils.cloud import salt.config as config diff --git a/salt/cloud/clouds/nova.py b/salt/cloud/clouds/nova.py index d59600f04a..d73fa610af 100644 --- a/salt/cloud/clouds/nova.py +++ b/salt/cloud/clouds/nova.py @@ -223,7 +223,7 @@ except ImportError as exc: # Import Salt Cloud Libs from salt.cloud.libcloudfuncs import * # pylint: disable=W0614,W0401 import salt.config as config -from salt.utils import namespaced_function +from salt.utils.functools import namespaced_function from salt.exceptions import ( SaltCloudConfigError, SaltCloudNotFound, diff --git a/salt/cloud/clouds/openstack.py b/salt/cloud/clouds/openstack.py index a073ba29e9..d93d6a768c 100644 --- a/salt/cloud/clouds/openstack.py +++ b/salt/cloud/clouds/openstack.py @@ -185,7 +185,6 @@ except Exception: from salt.cloud.libcloudfuncs import * # pylint: disable=W0614,W0401 # Import salt libs -import salt.utils # TODO: Remove this once namespaced_function has been moved import salt.utils.cloud import salt.utils.files import salt.utils.pycrypto @@ -197,6 +196,7 @@ from salt.exceptions import ( SaltCloudExecutionFailure, SaltCloudExecutionTimeout ) +from salt.utils.functools import namespaced_function # Import netaddr IP matching try: @@ -214,19 +214,19 @@ __virtualname__ = 'openstack' # Some of the libcloud functions need to be in the same namespace as the # functions defined in the module, so we create new function objects inside # this module namespace -get_size = salt.utils.namespaced_function(get_size, globals()) -get_image = salt.utils.namespaced_function(get_image, globals()) -avail_locations = salt.utils.namespaced_function(avail_locations, globals()) -avail_images = salt.utils.namespaced_function(avail_images, globals()) -avail_sizes = salt.utils.namespaced_function(avail_sizes, globals()) -script = salt.utils.namespaced_function(script, globals()) -destroy = salt.utils.namespaced_function(destroy, globals()) -reboot = salt.utils.namespaced_function(reboot, globals()) -list_nodes = salt.utils.namespaced_function(list_nodes, globals()) -list_nodes_full = salt.utils.namespaced_function(list_nodes_full, globals()) -list_nodes_select = salt.utils.namespaced_function(list_nodes_select, globals()) -show_instance = salt.utils.namespaced_function(show_instance, globals()) -get_node = salt.utils.namespaced_function(get_node, globals()) +get_size = namespaced_function(get_size, globals()) +get_image = namespaced_function(get_image, globals()) +avail_locations = namespaced_function(avail_locations, globals()) +avail_images = namespaced_function(avail_images, globals()) +avail_sizes = namespaced_function(avail_sizes, globals()) +script = namespaced_function(script, globals()) +destroy = namespaced_function(destroy, globals()) +reboot = namespaced_function(reboot, globals()) +list_nodes = namespaced_function(list_nodes, globals()) +list_nodes_full = namespaced_function(list_nodes_full, globals()) +list_nodes_select = namespaced_function(list_nodes_select, globals()) +show_instance = namespaced_function(show_instance, globals()) +get_node = namespaced_function(get_node, globals()) # Only load in this module is the OPENSTACK configurations are in place diff --git a/salt/cloud/clouds/packet.py b/salt/cloud/clouds/packet.py index d80995b87e..634d90d092 100644 --- a/salt/cloud/clouds/packet.py +++ b/salt/cloud/clouds/packet.py @@ -75,7 +75,7 @@ from salt.exceptions import ( import salt.utils.cloud from salt.cloud.libcloudfuncs import get_size, get_image, script, show_instance -from salt.utils import namespaced_function +from salt.utils.functools import namespaced_function get_size = namespaced_function(get_size, globals()) get_image = namespaced_function(get_image, globals()) diff --git a/salt/cloud/clouds/parallels.py b/salt/cloud/clouds/parallels.py index a8db20eaae..da9686e19d 100644 --- a/salt/cloud/clouds/parallels.py +++ b/salt/cloud/clouds/parallels.py @@ -27,7 +27,6 @@ import pprint import logging # Import Salt libs -import salt.utils from salt._compat import ElementTree as ET # pylint: disable=import-error,no-name-in-module from salt.ext.six.moves.urllib.error import URLError diff --git a/salt/cloud/clouds/proxmox.py b/salt/cloud/clouds/proxmox.py index b5952c8958..8690a538e1 100644 --- a/salt/cloud/clouds/proxmox.py +++ b/salt/cloud/clouds/proxmox.py @@ -39,7 +39,6 @@ from salt.ext import six import salt.utils # Import salt cloud libs -import salt.utils.cloud import salt.config as config from salt.exceptions import ( SaltCloudSystemExit, diff --git a/salt/cloud/clouds/pyrax.py b/salt/cloud/clouds/pyrax.py index 58786ab65f..a867f72569 100644 --- a/salt/cloud/clouds/pyrax.py +++ b/salt/cloud/clouds/pyrax.py @@ -13,7 +13,7 @@ module instead. from __future__ import absolute_import # Import salt libs -import salt.utils +import salt.utils.data import salt.config as config # Import pyrax libraries @@ -84,13 +84,13 @@ def queues_exists(call, kwargs): def queues_show(call, kwargs): conn = get_conn('RackspaceQueues') - return salt.utils.simple_types_filter(conn.show(kwargs['name']).__dict__) + return salt.utils.data.simple_types_filter(conn.show(kwargs['name']).__dict__) def queues_create(call, kwargs): conn = get_conn('RackspaceQueues') if conn.create(kwargs['name']): - return salt.utils.simple_types_filter(conn.show(kwargs['name']).__dict__) + return salt.utils.data.simple_types_filter(conn.show(kwargs['name']).__dict__) else: return {} @@ -100,4 +100,4 @@ def queues_delete(call, kwargs): if conn.delete(kwargs['name']): return {} else: - return salt.utils.simple_types_filter(conn.show(kwargs['name'].__dict__)) + return salt.utils.data.simple_types_filter(conn.show(kwargs['name'].__dict__)) diff --git a/salt/cloud/clouds/saltify.py b/salt/cloud/clouds/saltify.py index 3c0980dfea..1f3d0867bd 100644 --- a/salt/cloud/clouds/saltify.py +++ b/salt/cloud/clouds/saltify.py @@ -17,7 +17,7 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils +import salt.utils.cloud import salt.config as config import salt.netapi import salt.ext.six as six diff --git a/salt/cloud/clouds/vmware.py b/salt/cloud/clouds/vmware.py index 9e0f6485b0..226119940b 100644 --- a/salt/cloud/clouds/vmware.py +++ b/salt/cloud/clouds/vmware.py @@ -124,7 +124,6 @@ import os.path import subprocess # Import salt libs -import salt.utils import salt.utils.cloud import salt.utils.network import salt.utils.stringutils diff --git a/salt/cloud/libcloudfuncs.py b/salt/cloud/libcloudfuncs.py index 93a91e7b74..37e2f777b6 100644 --- a/salt/cloud/libcloudfuncs.py +++ b/salt/cloud/libcloudfuncs.py @@ -40,8 +40,8 @@ import salt.utils.event import salt.client # Import salt cloud libs -import salt.utils import salt.utils.cloud +import salt.utils.data import salt.config as config from salt.exceptions import SaltCloudNotFound, SaltCloudSystemExit @@ -123,7 +123,7 @@ def get_node(conn, name): nodes = conn.list_nodes() for node in nodes: if node.name == name: - __utils__['cloud.cache_node'](salt.utils.simple_types_filter(node.__dict__), __active_provider_name__, __opts__) + __utils__['cloud.cache_node'](salt.utils.data.simple_types_filter(node.__dict__), __active_provider_name__, __opts__) return node diff --git a/salt/daemons/flo/jobber.py b/salt/daemons/flo/jobber.py index 1f50b3227e..a16e9fd2db 100644 --- a/salt/daemons/flo/jobber.py +++ b/salt/daemons/flo/jobber.py @@ -19,7 +19,6 @@ import json # Import salt libs from salt.ext import six import salt.daemons.masterapi -import salt.utils import salt.utils.args import salt.utils.data import salt.utils.files diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index 4d5a8a1c06..4bd3f08a14 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -15,7 +15,6 @@ import stat # Import salt libs import salt.crypt -import salt.utils # TODO: Remove this once check_whitelist_blacklist, expr_match, get_values_of_matching_keys are moved import salt.cache import salt.client import salt.payload @@ -39,6 +38,7 @@ import salt.utils.gzip_util import salt.utils.jid import salt.utils.minions import salt.utils.platform +import salt.utils.stringutils import salt.utils.user import salt.utils.verify from salt.defaults import DEFAULT_TARGET_DELIM @@ -241,7 +241,7 @@ def access_keys(opts): log.profile('Beginning pwd.getpwall() call in masterapi access_keys function') for user in pwd.getpwall(): user = user.pw_name - if user not in keys and salt.utils.check_whitelist_blacklist(user, whitelist=acl_users): + if user not in keys and salt.utils.stringutils.check_whitelist_blacklist(user, whitelist=acl_users): keys[user] = mk_key(opts, user) log.profile('End pwd.getpwall() call in masterapi access_keys function') @@ -342,7 +342,7 @@ class AutoKey(object): if line.startswith('#'): continue else: - if salt.utils.expr_match(keyid, line): + if salt.utils.stringutils.expr_match(keyid, line): return True return False @@ -1235,7 +1235,9 @@ class LocalFuncs(object): auth_ret = True if auth_ret is not True: - auth_list = salt.utils.get_values_of_matching_keys( + # Avoid circular import + import salt.utils.master + auth_list = salt.utils.master.get_values_of_matching_keys( self.opts['publisher_acl'], auth_ret) if not auth_list: diff --git a/salt/engines/docker_events.py b/salt/engines/docker_events.py index f2ec60f913..d28d345267 100644 --- a/salt/engines/docker_events.py +++ b/salt/engines/docker_events.py @@ -11,7 +11,7 @@ import json import logging import traceback -import salt.utils +import salt.utils.event # pylint: disable=import-error try: diff --git a/salt/engines/napalm_syslog.py b/salt/engines/napalm_syslog.py index 8c1f94dc69..74edea3c6e 100644 --- a/salt/engines/napalm_syslog.py +++ b/salt/engines/napalm_syslog.py @@ -189,9 +189,9 @@ except ImportError: HAS_NAPALM_LOGS = False # Import salt libs -import salt.utils import salt.utils.event as event import salt.utils.network +import salt.utils.stringutils # ---------------------------------------------------------------------------------------------------------------------- # module properties @@ -337,25 +337,28 @@ def start(transport='zmq', try: event_os = dict_object['os'] if os_blacklist or os_whitelist: - valid_os = salt.utils.check_whitelist_blacklist(event_os, - whitelist=os_whitelist, - blacklist=os_blacklist) + valid_os = salt.utils.stringutils.check_whitelist_blacklist( + event_os, + whitelist=os_whitelist, + blacklist=os_blacklist) if not valid_os: log.info('Ignoring NOS {} as per whitelist/blacklist'.format(event_os)) continue event_error = dict_object['error'] if error_blacklist or error_whitelist: - valid_error = salt.utils.check_whitelist_blacklist(event_error, - whitelist=error_whitelist, - blacklist=error_blacklist) + valid_error = salt.utils.stringutils.check_whitelist_blacklist( + event_error, + whitelist=error_whitelist, + blacklist=error_blacklist) if not valid_error: log.info('Ignoring error {} as per whitelist/blacklist'.format(event_error)) continue event_host = dict_object.get('host') or dict_object.get('ip') if host_blacklist or host_whitelist: - valid_host = salt.utils.check_whitelist_blacklist(event_host, - whitelist=host_whitelist, - blacklist=host_blacklist) + valid_host = salt.utils.stringutils.check_whitelist_blacklist( + event_host, + whitelist=host_whitelist, + blacklist=host_blacklist) if not valid_host: log.info('Ignoring messages from {} as per whitelist/blacklist'.format(event_host)) continue diff --git a/salt/engines/slack.py b/salt/engines/slack.py index 9f0e1638bc..ca68496d82 100644 --- a/salt/engines/slack.py +++ b/salt/engines/slack.py @@ -115,7 +115,6 @@ import salt.client import salt.loader import salt.minion import salt.runner -import salt.utils import salt.utils.args import salt.utils.event import salt.utils.http diff --git a/salt/exceptions.py b/salt/exceptions.py index 7215112ea3..c6ff7c488b 100644 --- a/salt/exceptions.py +++ b/salt/exceptions.py @@ -235,9 +235,10 @@ class SaltRenderError(SaltException): if trace: exc_str += u'\n{0}\n'.format(trace) if self.line_num and self.buffer: - import salt.utils + # Avoid circular import import salt.utils.stringutils - self.context = salt.utils.get_context( + import salt.utils.templates + self.context = salt.utils.templates.get_context( self.buffer, self.line_num, marker=marker diff --git a/salt/fileclient.py b/salt/fileclient.py index 0836df7230..508685cd32 100644 --- a/salt/fileclient.py +++ b/salt/fileclient.py @@ -24,7 +24,6 @@ import salt.loader import salt.payload import salt.transport import salt.fileserver -import salt.utils import salt.utils.files import salt.utils.gzip_util import salt.utils.hashutils @@ -240,7 +239,7 @@ class Client(object): for fn_ in self.file_list(saltenv): fn_ = sdecode(fn_) if fn_.strip() and fn_.startswith(path): - if salt.utils.check_include_exclude( + if salt.utils.stringutils.check_include_exclude( fn_, include_pat, exclude_pat): fn_ = self.cache_file( salt.utils.url.create(fn_), saltenv, cachedir=cachedir) @@ -490,7 +489,7 @@ class Client(object): strpath = u'index.html' if salt.utils.platform.is_windows(): - strpath = salt.utils.sanitize_win_path_string(strpath) + strpath = salt.utils.path.sanitize_win_path(strpath) dest = os.path.join(dest, strpath) @@ -805,7 +804,7 @@ class Client(object): ''' url_data = urlparse(url) if salt.utils.platform.is_windows(): - netloc = salt.utils.sanitize_win_path_string(url_data.netloc) + netloc = salt.utils.path.sanitize_win_path(url_data.netloc) else: netloc = url_data.netloc diff --git a/salt/fileserver/azurefs.py b/salt/fileserver/azurefs.py index 560455779b..816763020e 100644 --- a/salt/fileserver/azurefs.py +++ b/salt/fileserver/azurefs.py @@ -55,7 +55,6 @@ import shutil # Import salt libs import salt.fileserver -import salt.utils import salt.utils.files import salt.utils.gzip_util import salt.utils.hashutils diff --git a/salt/fileserver/hgfs.py b/salt/fileserver/hgfs.py index 495021dfd5..bc7d2c32e7 100644 --- a/salt/fileserver/hgfs.py +++ b/salt/fileserver/hgfs.py @@ -59,11 +59,11 @@ except ImportError: # pylint: enable=import-error # Import salt libs -import salt.utils import salt.utils.data import salt.utils.files import salt.utils.gzip_util import salt.utils.hashutils +import salt.utils.stringutils import salt.utils.url import salt.utils.versions import salt.fileserver @@ -591,7 +591,7 @@ def _env_is_exposed(env): else: blacklist = __opts__['hgfs_saltenv_blacklist'] - return salt.utils.check_whitelist_blacklist( + return salt.utils.stringutils.check_whitelist_blacklist( env, whitelist=whitelist, blacklist=blacklist, diff --git a/salt/fileserver/minionfs.py b/salt/fileserver/minionfs.py index fd5dd2ed23..9756a7d780 100644 --- a/salt/fileserver/minionfs.py +++ b/salt/fileserver/minionfs.py @@ -31,10 +31,10 @@ import logging # Import salt libs import salt.fileserver -import salt.utils import salt.utils.files import salt.utils.gzip_util import salt.utils.hashutils +import salt.utils.stringutils import salt.utils.url import salt.utils.versions @@ -61,7 +61,7 @@ def _is_exposed(minion): ''' Check if the minion is exposed, based on the whitelist and blacklist ''' - return salt.utils.check_whitelist_blacklist( + return salt.utils.stringutils.check_whitelist_blacklist( minion, whitelist=__opts__['minionfs_whitelist'], blacklist=__opts__['minionfs_blacklist'] diff --git a/salt/fileserver/roots.py b/salt/fileserver/roots.py index a92537fa14..6d8ec7ed08 100644 --- a/salt/fileserver/roots.py +++ b/salt/fileserver/roots.py @@ -29,6 +29,7 @@ import salt.utils.files import salt.utils.gzip_util import salt.utils.hashutils import salt.utils.path +import salt.utils.platform import salt.utils.versions from salt.ext import six @@ -351,7 +352,7 @@ def _file_lists(load, form): 'roots: %s symlink destination is %s', abs_path, link_dest ) - if salt.utils.is_windows() \ + if salt.utils.platform.is_windows() \ and link_dest.startswith('\\\\'): # Symlink points to a network path. Since you can't # join UNC and non-UNC paths, just assume the original diff --git a/salt/fileserver/s3fs.py b/salt/fileserver/s3fs.py index 5883408d94..25e1a07814 100644 --- a/salt/fileserver/s3fs.py +++ b/salt/fileserver/s3fs.py @@ -68,7 +68,6 @@ import logging # Import salt libs import salt.fileserver as fs import salt.modules -import salt.utils import salt.utils.files import salt.utils.gzip_util import salt.utils.hashutils diff --git a/salt/fileserver/svnfs.py b/salt/fileserver/svnfs.py index c2ff3d373a..cd761e2bb1 100644 --- a/salt/fileserver/svnfs.py +++ b/salt/fileserver/svnfs.py @@ -54,11 +54,11 @@ except ImportError: # pylint: enable=import-error # Import salt libs -import salt.utils import salt.utils.data import salt.utils.files import salt.utils.gzip_util import salt.utils.hashutils +import salt.utils.stringutils import salt.utils.url import salt.utils.versions import salt.fileserver @@ -506,7 +506,7 @@ def _env_is_exposed(env): else: blacklist = __opts__['svnfs_saltenv_blacklist'] - return salt.utils.check_whitelist_blacklist( + return salt.utils.stringutils.check_whitelist_blacklist( env, whitelist=whitelist, blacklist=blacklist, diff --git a/salt/grains/core.py b/salt/grains/core.py index 5cdcdccdf5..f54e786fc4 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -43,7 +43,6 @@ except ImportError: # Import salt libs import salt.exceptions import salt.log -import salt.utils import salt.utils.dns import salt.utils.files import salt.utils.network diff --git a/salt/key.py b/salt/key.py index a4f1eefa0f..c620cdb0ef 100644 --- a/salt/key.py +++ b/salt/key.py @@ -22,13 +22,13 @@ import salt.crypt import salt.daemons.masterapi import salt.exceptions import salt.minion -import salt.utils # TODO: Remove this once get_master_key is moved import salt.utils.args import salt.utils.crypt import salt.utils.data import salt.utils.event import salt.utils.files import salt.utils.kinds +import salt.utils.master import salt.utils.sdb import salt.utils.user @@ -149,7 +149,7 @@ class KeyCLI(object): low[u'eauth'] = self.opts[u'eauth'] else: low[u'user'] = salt.utils.user.get_specific_user() - low[u'key'] = salt.utils.get_master_key(low[u'user'], self.opts, skip_perm_errors) + low[u'key'] = salt.utils.master.get_master_key(low[u'user'], self.opts, skip_perm_errors) self.auth = low diff --git a/salt/master.py b/salt/master.py index 05c0cc704b..c79dfccca0 100644 --- a/salt/master.py +++ b/salt/master.py @@ -47,7 +47,6 @@ import tornado.gen # pylint: disable=F0401 # Import salt libs import salt.crypt -import salt.utils # TODO: Remove this once get_values_of_matching_keys is moved import salt.client import salt.payload import salt.pillar @@ -1440,7 +1439,7 @@ class AESFuncs(object): path_name = os.path.split(syndic_cache_path)[0] if not os.path.exists(path_name): os.makedirs(path_name) - with salt.utils.fopen(syndic_cache_path, u'w') as wfh: + with salt.utils.files.fopen(syndic_cache_path, u'w') as wfh: wfh.write(u'') # Format individual return loads @@ -1903,7 +1902,7 @@ class ClearFuncs(object): auth_ret = True if auth_ret is not True: - auth_list = salt.utils.get_values_of_matching_keys( + auth_list = salt.utils.master.get_values_of_matching_keys( self.opts[u'publisher_acl'], auth_ret) if not auth_list: diff --git a/salt/minion.py b/salt/minion.py index d70775e38a..9ea6c170db 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -74,6 +74,12 @@ try: HAS_ZMQ_MONITOR = True except ImportError: HAS_ZMQ_MONITOR = False + +try: + import salt.utils.win_functions + HAS_WIN_FUNCTIONS = True +except ImportError: + HAS_WIN_FUNCTIONS = False # pylint: enable=import-error # Import salt libs @@ -86,7 +92,6 @@ import salt.engines import salt.payload import salt.pillar import salt.syspaths -import salt.utils import salt.utils.args import salt.utils.context import salt.utils.data @@ -2371,7 +2376,8 @@ class Minion(MinionBase): enable_sigusr1_handler() # Make sure to gracefully handle CTRL_LOGOFF_EVENT - salt.utils.enable_ctrl_logoff_handler() + if HAS_WIN_FUNCTIONS: + salt.utils.win_functions.enable_ctrl_logoff_handler() # On first startup execute a state run if configured to do so self._state_run() diff --git a/salt/modules/apcups.py b/salt/modules/apcups.py index 647ae78280..8095412898 100644 --- a/salt/modules/apcups.py +++ b/salt/modules/apcups.py @@ -8,7 +8,6 @@ from __future__ import absolute_import import logging # Import Salt libs -import salt.utils import salt.utils.path import salt.utils.decorators as decorators diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py index 591b7ab2dd..d38bde1ac1 100644 --- a/salt/modules/aptpkg.py +++ b/salt/modules/aptpkg.py @@ -38,10 +38,10 @@ from salt.ext.six.moves.urllib.request import Request as _Request, urlopen as _u import salt.config import salt.syspaths from salt.modules.cmdmod import _parse_env -import salt.utils # TODO: Remove this when alias_function is moved import salt.utils.args import salt.utils.data import salt.utils.files +import salt.utils.functools import salt.utils.itertools import salt.utils.path import salt.utils.pkg @@ -319,7 +319,7 @@ def latest_version(*names, **kwargs): return ret # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def version(*names, **kwargs): diff --git a/salt/modules/artifactory.py b/salt/modules/artifactory.py index 74572cbad5..811cb2b4ae 100644 --- a/salt/modules/artifactory.py +++ b/salt/modules/artifactory.py @@ -10,7 +10,7 @@ import base64 import logging # Import Salt libs -import salt.utils +import salt.utils.files import salt.ext.six.moves.http_client # pylint: disable=import-error,redefined-builtin,no-name-in-module from salt.ext.six.moves import urllib # pylint: disable=no-name-in-module from salt.ext.six.moves.urllib.error import HTTPError, URLError # pylint: disable=no-name-in-module diff --git a/salt/modules/augeas_cfg.py b/salt/modules/augeas_cfg.py index e0f78329ec..a8554a0e66 100644 --- a/salt/modules/augeas_cfg.py +++ b/salt/modules/augeas_cfg.py @@ -41,7 +41,6 @@ except ImportError: pass # Import salt libs -import salt.utils import salt.utils.args from salt.exceptions import SaltInvocationError diff --git a/salt/modules/aws_sqs.py b/salt/modules/aws_sqs.py index a906899718..467912d5e5 100644 --- a/salt/modules/aws_sqs.py +++ b/salt/modules/aws_sqs.py @@ -9,7 +9,6 @@ import logging import json # Import salt libs -import salt.utils import salt.utils.path from salt.ext import six diff --git a/salt/modules/bigip.py b/salt/modules/bigip.py index bea9d55f31..bc3427ac06 100644 --- a/salt/modules/bigip.py +++ b/salt/modules/bigip.py @@ -22,8 +22,6 @@ except ImportError: from salt.ext import six # Import salt libs -import salt.utils -import salt.output import salt.exceptions # Setup the logger diff --git a/salt/modules/boto_cloudtrail.py b/salt/modules/boto_cloudtrail.py index 99a1191ec8..01390cffa1 100644 --- a/salt/modules/boto_cloudtrail.py +++ b/salt/modules/boto_cloudtrail.py @@ -57,7 +57,6 @@ import logging from salt.ext import six import salt.utils.boto3 import salt.utils.compat -import salt.utils from salt.utils.versions import LooseVersion as _LooseVersion log = logging.getLogger(__name__) diff --git a/salt/modules/boto_cognitoidentity.py b/salt/modules/boto_cognitoidentity.py index a1a7f97805..2f769af4bf 100644 --- a/salt/modules/boto_cognitoidentity.py +++ b/salt/modules/boto_cognitoidentity.py @@ -83,7 +83,6 @@ import logging # Import Salt libs import salt.utils.boto3 import salt.utils.compat -import salt.utils from salt.utils.versions import LooseVersion as _LooseVersion log = logging.getLogger(__name__) diff --git a/salt/modules/boto_elasticsearch_domain.py b/salt/modules/boto_elasticsearch_domain.py index d4a156db5e..68e6c628fb 100644 --- a/salt/modules/boto_elasticsearch_domain.py +++ b/salt/modules/boto_elasticsearch_domain.py @@ -83,7 +83,6 @@ import json from salt.ext import six import salt.utils.boto3 import salt.utils.compat -import salt.utils from salt.exceptions import SaltInvocationError from salt.utils.versions import LooseVersion as _LooseVersion diff --git a/salt/modules/boto_iot.py b/salt/modules/boto_iot.py index c48c4ae42e..05ee9256f1 100644 --- a/salt/modules/boto_iot.py +++ b/salt/modules/boto_iot.py @@ -58,7 +58,6 @@ import datetime # Import Salt libs import salt.utils.boto3 import salt.utils.compat -import salt.utils from salt.utils.versions import LooseVersion as _LooseVersion log = logging.getLogger(__name__) diff --git a/salt/modules/boto_rds.py b/salt/modules/boto_rds.py index cf778bd86e..bfb3991992 100644 --- a/salt/modules/boto_rds.py +++ b/salt/modules/boto_rds.py @@ -56,7 +56,6 @@ import time import salt.utils.boto3 import salt.utils.compat import salt.utils.odict as odict -import salt.utils from salt.exceptions import SaltInvocationError from salt.utils.versions import LooseVersion as _LooseVersion diff --git a/salt/modules/boto_s3_bucket.py b/salt/modules/boto_s3_bucket.py index abfc01c90d..b18f92482a 100644 --- a/salt/modules/boto_s3_bucket.py +++ b/salt/modules/boto_s3_bucket.py @@ -60,7 +60,6 @@ import json from salt.ext import six from salt.ext.six.moves import range # pylint: disable=import-error import salt.utils.compat -import salt.utils from salt.exceptions import SaltInvocationError from salt.utils.versions import LooseVersion as _LooseVersion diff --git a/salt/modules/cloud.py b/salt/modules/cloud.py index 796da4c622..ecbcfd95fa 100644 --- a/salt/modules/cloud.py +++ b/salt/modules/cloud.py @@ -8,7 +8,7 @@ from __future__ import absolute_import import os import logging import copy -import salt.utils +import salt.utils.data # Import salt libs try: @@ -17,7 +17,6 @@ try: except ImportError: HAS_SALTCLOUD = False -import salt.utils from salt.exceptions import SaltCloudConfigError # Import 3rd-party libs @@ -175,7 +174,7 @@ def get_instance(name, provider=None): ''' data = action(fun='show_instance', names=[name], provider=provider) - info = salt.utils.simple_types_filter(data) + info = salt.utils.data.simple_types_filter(data) try: # get the first: [alias][driver][vm_name] info = next(six.itervalues(next(six.itervalues(next(six.itervalues(info)))))) diff --git a/salt/modules/cp.py b/salt/modules/cp.py index 9c2ea4e3c1..cd894bea58 100644 --- a/salt/modules/cp.py +++ b/salt/modules/cp.py @@ -14,7 +14,6 @@ import fnmatch # Import salt libs import salt.minion import salt.fileclient -import salt.utils import salt.utils.files import salt.utils.gzip_util import salt.utils.locales diff --git a/salt/modules/cron.py b/salt/modules/cron.py index c387da3d63..6c8d486cf8 100644 --- a/salt/modules/cron.py +++ b/salt/modules/cron.py @@ -14,8 +14,8 @@ import os import random # Import salt libs -import salt.utils import salt.utils.files +import salt.utils.functools import salt.utils.path import salt.utils.stringutils from salt.ext.six.moves import range @@ -366,7 +366,7 @@ def list_tab(user): return ret # For consistency's sake -ls = salt.utils.alias_function(list_tab, 'ls') +ls = salt.utils.functools.alias_function(list_tab, 'ls') def set_special(user, special, cmd): @@ -612,7 +612,7 @@ def rm_job(user, return comdat['stderr'] return ret -rm = salt.utils.alias_function(rm_job, 'rm') +rm = salt.utils.functools.alias_function(rm_job, 'rm') def set_env(user, name, value=None): diff --git a/salt/modules/debconfmod.py b/salt/modules/debconfmod.py index 18e19d1cce..1ce20d9176 100644 --- a/salt/modules/debconfmod.py +++ b/salt/modules/debconfmod.py @@ -10,7 +10,6 @@ import os import re # Import salt libs -import salt.utils import salt.utils.path import salt.utils.files import salt.utils.versions diff --git a/salt/modules/dockermod.py b/salt/modules/dockermod.py index 58cc1f0d25..6249988e27 100644 --- a/salt/modules/dockermod.py +++ b/salt/modules/dockermod.py @@ -201,12 +201,13 @@ import subprocess from salt.exceptions import CommandExecutionError, SaltInvocationError from salt.ext import six from salt.ext.six.moves import map # pylint: disable=import-error,redefined-builtin -import salt.utils import salt.utils.args import salt.utils.decorators import salt.utils.docker import salt.utils.files +import salt.utils.functools import salt.utils.hashutils +import salt.utils.json import salt.utils.path import salt.utils.stringutils import salt.utils.thin @@ -2745,7 +2746,7 @@ def copy_from(name, source, dest, overwrite=False, makedirs=False): # Docker cp gets a file from the container, alias this to copy_from -cp = salt.utils.alias_function(copy_from, 'cp') +cp = salt.utils.functools.alias_function(copy_from, 'cp') @_ensure_exists @@ -4393,7 +4394,7 @@ def pause(name): .format(name))} return _change_state(name, 'pause', 'paused') -freeze = salt.utils.alias_function(pause, 'freeze') +freeze = salt.utils.functools.alias_function(pause, 'freeze') @_ensure_exists @@ -4600,7 +4601,7 @@ def unpause(name): .format(name))} return _change_state(name, 'unpause', 'running') -unfreeze = salt.utils.alias_function(unpause, 'unfreeze') +unfreeze = salt.utils.functools.alias_function(unpause, 'unfreeze') def wait(name, ignore_already_stopped=False, fail_on_exit_status=False): @@ -5370,7 +5371,7 @@ def call(name, function, *args, **kwargs): # process "real" result in stdout try: - data = salt.utils.find_json(ret['stdout']) + data = salt.utils.json.find_json(ret['stdout']) local = data.get('local', data) if isinstance(local, dict): if 'retcode' in local: diff --git a/salt/modules/ebuild.py b/salt/modules/ebuild.py index facde056bd..df6c80bcdd 100644 --- a/salt/modules/ebuild.py +++ b/salt/modules/ebuild.py @@ -21,9 +21,9 @@ import logging import re # Import salt libs -import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.args import salt.utils.data +import salt.utils.functools import salt.utils.path import salt.utils.pkg import salt.utils.systemd @@ -261,7 +261,7 @@ def latest_version(*names, **kwargs): return ret # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def _get_upgradable(backtrack=3): diff --git a/salt/modules/file.py b/salt/modules/file.py index 1e91d73552..eb8327a187 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -46,12 +46,12 @@ except ImportError: pass # Import salt libs -import salt.utils import salt.utils.args import salt.utils.atomicfile import salt.utils.filebuffer import salt.utils.files import salt.utils.find +import salt.utils.functools import salt.utils.hashutils import salt.utils.itertools import salt.utils.locales @@ -2320,14 +2320,14 @@ def replace(path, if not_found_content is None: not_found_content = repl if prepend_if_not_found: - new_file.insert(0, not_found_content + salt.utils.to_bytes(os.linesep)) + new_file.insert(0, not_found_content + salt.utils.stringutils.to_bytes(os.linesep)) else: # append_if_not_found # Make sure we have a newline at the end of the file if 0 != len(new_file): - if not new_file[-1].endswith(salt.utils.to_bytes(os.linesep)): - new_file[-1] += salt.utils.to_bytes(os.linesep) - new_file.append(not_found_content + salt.utils.to_bytes(os.linesep)) + if not new_file[-1].endswith(salt.utils.stringutils.to_bytes(os.linesep)): + new_file[-1] += salt.utils.stringutils.to_bytes(os.linesep) + new_file.append(not_found_content + salt.utils.stringutils.to_bytes(os.linesep)) has_changes = True if not dry_run: try: @@ -2513,7 +2513,7 @@ def blockreplace(path, bufsize=1, mode='rb') for line in fi_file: - line = salt.utils.to_str(line) + line = salt.utils.stringutils.to_str(line) result = line if marker_start in line: @@ -2622,7 +2622,7 @@ def blockreplace(path, try: fh_ = salt.utils.atomicfile.atomic_open(path, 'wb') for line in new_file: - fh_.write(salt.utils.to_bytes(line)) + fh_.write(salt.utils.stringutils.to_bytes(line)) finally: fh_.close() @@ -3763,7 +3763,7 @@ def source_list(source, source_hash, saltenv): single_hash = single[single_src] if single[single_src] else source_hash urlparsed_single_src = _urlparse(single_src) # Fix this for Windows - if salt.utils.is_windows(): + if salt.utils.platform.is_windows(): # urlparse doesn't handle a local Windows path without the # protocol indicator (file://). The scheme will be the # drive letter instead of the protocol. So, we'll add the @@ -3800,7 +3800,7 @@ def source_list(source, source_hash, saltenv): ret = (single, source_hash) break urlparsed_src = _urlparse(single) - if salt.utils.is_windows(): + if salt.utils.platform.is_windows(): # urlparse doesn't handle a local Windows path without the # protocol indicator (file://). The scheme will be the # drive letter instead of the protocol. So, we'll add the @@ -5868,7 +5868,7 @@ def list_backups(path, limit=None): [files[x] for x in sorted(files, reverse=True)[:limit]] ))) -list_backup = salt.utils.alias_function(list_backups, 'list_backup') +list_backup = salt.utils.functools.alias_function(list_backups, 'list_backup') def list_backups_dir(path, limit=None): @@ -5969,7 +5969,7 @@ def restore_backup(path, backup_id): '{1}'.format(backup_id, path) return ret - salt.utils.backup_minion(path, _get_bkroot()) + salt.utils.files.backup_minion(path, _get_bkroot()) try: shutil.copyfile(backup['Location'], path) except IOError as exc: @@ -6040,7 +6040,7 @@ def delete_backup(path, backup_id): return ret -remove_backup = salt.utils.alias_function(delete_backup, 'remove_backup') +remove_backup = salt.utils.functools.alias_function(delete_backup, 'remove_backup') def grep(path, diff --git a/salt/modules/freebsdpkg.py b/salt/modules/freebsdpkg.py index de37a4218e..295709c730 100644 --- a/salt/modules/freebsdpkg.py +++ b/salt/modules/freebsdpkg.py @@ -80,8 +80,8 @@ import logging import re # Import salt libs -import salt.utils # TODO: Remove this when alias_function is moved import salt.utils.data +import salt.utils.functools import salt.utils.pkg from salt.exceptions import CommandExecutionError, MinionError from salt.ext import six @@ -200,7 +200,7 @@ def latest_version(*names, **kwargs): return '' if len(names) == 1 else dict((x, '') for x in names) # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def version(*names, **kwargs): @@ -486,9 +486,9 @@ def remove(name=None, pkgs=None, **kwargs): return ret # Support pkg.delete to remove packages to more closely match pkg_delete -delete = salt.utils.alias_function(remove, 'delete') +delete = salt.utils.functools.alias_function(remove, 'delete') # No equivalent to purge packages, use remove instead -purge = salt.utils.alias_function(remove, 'purge') +purge = salt.utils.functools.alias_function(remove, 'purge') def _rehash(): diff --git a/salt/modules/git.py b/salt/modules/git.py index 7cd2acb704..bd24fb983f 100644 --- a/salt/modules/git.py +++ b/salt/modules/git.py @@ -12,12 +12,13 @@ import re import stat # Import salt libs -import salt.utils import salt.utils.args import salt.utils.files +import salt.utils.functools import salt.utils.itertools import salt.utils.path import salt.utils.platform +import salt.utils.templates import salt.utils.url from salt.exceptions import SaltInvocationError, CommandExecutionError from salt.utils.versions import LooseVersion as _LooseVersion @@ -209,7 +210,7 @@ def _git_run(command, cwd=None, user=None, password=None, identity=None, for id_file in identity: if 'salt://' in id_file: with salt.utils.files.set_umask(0o077): - tmp_identity_file = salt.utils.mkstemp() + tmp_identity_file = salt.utils.files.mkstemp() _id_file = id_file id_file = __salt__['cp.get_file'](id_file, tmp_identity_file, @@ -1242,7 +1243,7 @@ def config_get_regexp(key, ret.setdefault(param, []).append(value) return ret -config_get_regex = salt.utils.alias_function(config_get_regexp, 'config_get_regex') +config_get_regex = salt.utils.functools.alias_function(config_get_regexp, 'config_get_regex') def config_set(key, diff --git a/salt/modules/hashutil.py b/salt/modules/hashutil.py index f7fb98cc1d..bfb83e7a6d 100644 --- a/salt/modules/hashutil.py +++ b/salt/modules/hashutil.py @@ -12,7 +12,6 @@ import hmac # Import Salt libs import salt.exceptions from salt.ext import six -import salt.utils import salt.utils.files import salt.utils.hashutils import salt.utils.stringutils diff --git a/salt/modules/incron.py b/salt/modules/incron.py index 5782943b93..ae752fec6d 100644 --- a/salt/modules/incron.py +++ b/salt/modules/incron.py @@ -9,8 +9,8 @@ import logging import os # Import salt libs -import salt.utils import salt.utils.files +import salt.utils.functools from salt.ext.six.moves import range # Set up logging @@ -211,7 +211,7 @@ def list_tab(user): return ret # For consistency's sake -ls = salt.utils.alias_function(list_tab, 'ls') +ls = salt.utils.functools.alias_function(list_tab, 'ls') def set_job(user, path, mask, cmd): @@ -315,4 +315,4 @@ def rm_job(user, return ret -rm = salt.utils.alias_function(rm_job, 'rm') +rm = salt.utils.functools.alias_function(rm_job, 'rm') diff --git a/salt/modules/kernelpkg_linux_yum.py b/salt/modules/kernelpkg_linux_yum.py index 661fdc92e7..f82ffc274f 100644 --- a/salt/modules/kernelpkg_linux_yum.py +++ b/salt/modules/kernelpkg_linux_yum.py @@ -12,6 +12,7 @@ try: from salt.utils.versions import LooseVersion as _LooseVersion from salt.exceptions import CommandExecutionError import salt.utils.data + import salt.utils.functools import salt.utils.systemd import salt.modules.yumpkg __IMPORT_ERROR = None @@ -24,7 +25,7 @@ log = logging.getLogger(__name__) __virtualname__ = 'kernelpkg' # Import functions from yumpkg -_yum = salt.utils.namespaced_function(salt.modules.yumpkg._yum, globals()) # pylint: disable=invalid-name, protected-access +_yum = salt.utils.functools.namespaced_function(salt.modules.yumpkg._yum, globals()) # pylint: disable=invalid-name, protected-access def __virtual__(): diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index 36d7cc4df1..fd252c61cd 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -177,11 +177,11 @@ def _cleanup(**kwargs): cert = kubernetes.client.configuration.cert_file key = kubernetes.client.configuration.key_file if cert and os.path.exists(cert) and os.path.basename(cert).startswith('salt-kube-'): - salt.utils.safe_rm(cert) + salt.utils.files.safe_rm(cert) if key and os.path.exists(key) and os.path.basename(key).startswith('salt-kube-'): - salt.utils.safe_rm(key) + salt.utils.files.safe_rm(key) if ca and os.path.exists(ca) and os.path.basename(ca).startswith('salt-kube-'): - salt.utils.safe_rm(ca) + salt.utils.files.safe_rm(ca) def ping(**kwargs): diff --git a/salt/modules/launchctl.py b/salt/modules/launchctl.py index fa951b2bee..9d46e1d732 100644 --- a/salt/modules/launchctl.py +++ b/salt/modules/launchctl.py @@ -20,11 +20,10 @@ import fnmatch import re # Import salt libs -import salt.utils +import salt.utils.files import salt.utils.platform import salt.utils.stringutils import salt.utils.decorators as decorators -import salt.utils.files from salt.utils.versions import LooseVersion as _LooseVersion from salt.ext import six diff --git a/salt/modules/logadm.py b/salt/modules/logadm.py index 9d1d498818..3f40eee9be 100644 --- a/salt/modules/logadm.py +++ b/salt/modules/logadm.py @@ -13,7 +13,6 @@ except ImportError: from pipes import quote as _quote_args # Import salt libs -import salt.utils import salt.utils.args import salt.utils.decorators as decorators import salt.utils.files diff --git a/salt/modules/lxc.py b/salt/modules/lxc.py index f9c69b5404..f165146f94 100644 --- a/salt/modules/lxc.py +++ b/salt/modules/lxc.py @@ -25,11 +25,11 @@ import re import random # Import salt libs -import salt.utils import salt.utils.args import salt.utils.cloud import salt.utils.dictupdate import salt.utils.files +import salt.utils.functools import salt.utils.hashutils import salt.utils.network import salt.utils.odict @@ -2598,7 +2598,7 @@ def destroy(name, stop=False, path=None): return _change_state('lxc-destroy', name, None, path=path) # Compatibility between LXC and nspawn -remove = salt.utils.alias_function(destroy, 'remove') +remove = salt.utils.functools.alias_function(destroy, 'remove') def exists(name, path=None): @@ -2941,7 +2941,7 @@ def set_password(name, users, password, encrypted=True, path=None): ) return True -set_pass = salt.utils.alias_function(set_password, 'set_pass') +set_pass = salt.utils.functools.alias_function(set_password, 'set_pass') def update_lxc_conf(name, lxc_conf, lxc_conf_unset, path=None): @@ -4206,7 +4206,7 @@ def copy_to(name, source, dest, overwrite=False, makedirs=False, path=None): overwrite=overwrite, makedirs=makedirs) -cp = salt.utils.alias_function(copy_to, 'cp') +cp = salt.utils.functools.alias_function(copy_to, 'cp') def read_conf(conf_file, out_format='simple'): diff --git a/salt/modules/mac_brew.py b/salt/modules/mac_brew.py index 08fe63105a..a3abc17888 100644 --- a/salt/modules/mac_brew.py +++ b/salt/modules/mac_brew.py @@ -17,8 +17,8 @@ import json import logging # Import salt libs -import salt.utils # TODO: Remove this when alias_function is moved import salt.utils.data +import salt.utils.functools import salt.utils.path import salt.utils.pkg import salt.utils.versions @@ -190,7 +190,7 @@ def latest_version(*names, **kwargs): return versions_dict # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def remove(name=None, pkgs=None, **kwargs): diff --git a/salt/modules/mac_group.py b/salt/modules/mac_group.py index 971aa74ed1..8d260b18ba 100644 --- a/salt/modules/mac_group.py +++ b/salt/modules/mac_group.py @@ -11,7 +11,7 @@ except ImportError: pass # Import Salt Libs -import salt.utils +import salt.utils.functools import salt.utils.itertools import salt.utils.stringutils from salt.exceptions import CommandExecutionError, SaltInvocationError @@ -26,8 +26,8 @@ def __virtual__(): if (__grains__.get('kernel') != 'Darwin' or __grains__['osrelease_info'] < (10, 7)): return (False, 'The mac_group execution module cannot be loaded: only available on Darwin-based systems >= 10.7') - _dscl = salt.utils.namespaced_function(_dscl, globals()) - _flush_dscl_cache = salt.utils.namespaced_function( + _dscl = salt.utils.functools.namespaced_function(_dscl, globals()) + _flush_dscl_cache = salt.utils.functools.namespaced_function( _flush_dscl_cache, globals() ) return __virtualname__ diff --git a/salt/modules/mac_ports.py b/salt/modules/mac_ports.py index 80e5952395..8ff8deed5b 100644 --- a/salt/modules/mac_ports.py +++ b/salt/modules/mac_ports.py @@ -37,8 +37,8 @@ import logging import re # Import salt libs -import salt.utils # TODO: Remove this when alias_function is removed import salt.utils.data +import salt.utils.functools import salt.utils.path import salt.utils.pkg import salt.utils.platform @@ -180,7 +180,7 @@ def latest_version(*names, **kwargs): return ret # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def remove(name=None, pkgs=None, **kwargs): diff --git a/salt/modules/mac_service.py b/salt/modules/mac_service.py index 5493910279..9291974465 100644 --- a/salt/modules/mac_service.py +++ b/salt/modules/mac_service.py @@ -11,12 +11,11 @@ import re import plistlib # Import salt libs -import salt.utils +import salt.utils.decorators as decorators +import salt.utils.files import salt.utils.path import salt.utils.platform import salt.utils.stringutils -import salt.utils.decorators as decorators -import salt.utils.files from salt.exceptions import CommandExecutionError from salt.utils.versions import LooseVersion as _LooseVersion diff --git a/salt/modules/mac_user.py b/salt/modules/mac_user.py index 930ee36b16..2b7372c17d 100644 --- a/salt/modules/mac_user.py +++ b/salt/modules/mac_user.py @@ -23,7 +23,6 @@ from salt.ext.six.moves import range # pylint: disable=import-error,redefined-b from salt.ext.six import string_types # Import salt libs -import salt.utils import salt.utils.args import salt.utils.decorators.path import salt.utils.stringutils diff --git a/salt/modules/memcached.py b/salt/modules/memcached.py index 2849ce84d2..f623d95ed8 100644 --- a/salt/modules/memcached.py +++ b/salt/modules/memcached.py @@ -12,7 +12,7 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils +import salt.utils.functools from salt.exceptions import CommandExecutionError, SaltInvocationError from salt.ext.six import integer_types @@ -238,7 +238,7 @@ def increment(key, delta=1, host=DEFAULT_HOST, port=DEFAULT_PORT): except ValueError: raise SaltInvocationError('Delta value must be an integer') -incr = salt.utils.alias_function(increment, 'incr') +incr = salt.utils.functools.alias_function(increment, 'incr') def decrement(key, delta=1, host=DEFAULT_HOST, port=DEFAULT_PORT): @@ -269,4 +269,4 @@ def decrement(key, delta=1, host=DEFAULT_HOST, port=DEFAULT_PORT): except ValueError: raise SaltInvocationError('Delta value must be an integer') -decr = salt.utils.alias_function(decrement, 'decr') +decr = salt.utils.functools.alias_function(decrement, 'decr') diff --git a/salt/modules/mine.py b/salt/modules/mine.py index b9ba6271cf..f7be219f41 100644 --- a/salt/modules/mine.py +++ b/salt/modules/mine.py @@ -13,7 +13,6 @@ import traceback # Import salt libs import salt.crypt import salt.payload -import salt.utils import salt.utils.args import salt.utils.event import salt.utils.network @@ -212,9 +211,10 @@ def send(func, *args, **kwargs): except IndexError: # Safe error, arg may be in kwargs pass - f_call = salt.utils.format_call(__salt__[mine_func], - func_data, - expected_extra_kws=MINE_INTERNAL_KEYWORDS) + f_call = salt.utils.args.format_call( + __salt__[mine_func], + func_data, + expected_extra_kws=MINE_INTERNAL_KEYWORDS) for arg in args: if arg not in f_call['args']: f_call['args'].append(arg) diff --git a/salt/modules/network.py b/salt/modules/network.py index ec0927eccf..223accf1e6 100644 --- a/salt/modules/network.py +++ b/salt/modules/network.py @@ -13,8 +13,8 @@ import os import socket # Import salt libs -import salt.utils # TODO: Remove this when alias_function mac_str_to_bytes are moved import salt.utils.decorators.path +import salt.utils.functools import salt.utils.files import salt.utils.network import salt.utils.path @@ -1038,7 +1038,7 @@ def hw_addr(iface): return salt.utils.network.hw_addr(iface) # Alias hwaddr to preserve backward compat -hwaddr = salt.utils.alias_function(hw_addr, 'hwaddr') +hwaddr = salt.utils.functools.alias_function(hw_addr, 'hwaddr') def interface(iface): @@ -1191,7 +1191,7 @@ def ip_addrs(interface=None, include_loopback=False, cidr=None, type=None): return addrs -ipaddrs = salt.utils.alias_function(ip_addrs, 'ipaddrs') +ipaddrs = salt.utils.functools.alias_function(ip_addrs, 'ipaddrs') def ip_addrs6(interface=None, include_loopback=False, cidr=None): @@ -1215,7 +1215,7 @@ def ip_addrs6(interface=None, include_loopback=False, cidr=None): else: return addrs -ipaddrs6 = salt.utils.alias_function(ip_addrs6, 'ipaddrs6') +ipaddrs6 = salt.utils.functools.alias_function(ip_addrs6, 'ipaddrs6') def get_hostname(): diff --git a/salt/modules/nilrt_ip.py b/salt/modules/nilrt_ip.py index a2450b067d..c96c151b68 100644 --- a/salt/modules/nilrt_ip.py +++ b/salt/modules/nilrt_ip.py @@ -10,7 +10,6 @@ import logging import time # Import salt libs -import salt.utils import salt.utils.validate.net import salt.exceptions diff --git a/salt/modules/npm.py b/salt/modules/npm.py index 5e430f2636..21e34427b8 100644 --- a/salt/modules/npm.py +++ b/salt/modules/npm.py @@ -13,7 +13,6 @@ import json import logging # Import salt libs -import salt.utils import salt.utils.path import salt.utils.user import salt.modules.cmdmod diff --git a/salt/modules/nspawn.py b/salt/modules/nspawn.py index eb3d94bc1b..b5c669eae9 100644 --- a/salt/modules/nspawn.py +++ b/salt/modules/nspawn.py @@ -34,8 +34,8 @@ import tempfile # Import Salt libs import salt.defaults.exitcodes -import salt.utils import salt.utils.args +import salt.utils.functools import salt.utils.path import salt.utils.systemd from salt.exceptions import CommandExecutionError, SaltInvocationError @@ -890,7 +890,7 @@ def list_running(): # 'machinectl list' shows only running containers, so allow this to work as an # alias to nspawn.list_running -list_ = salt.utils.alias_function(list_running, 'list_') +list_ = salt.utils.functools.alias_function(list_running, 'list_') def list_stopped(): @@ -1263,7 +1263,7 @@ def remove(name, stop=False): # Compatibility between LXC and nspawn -destroy = salt.utils.alias_function(remove, 'destroy') +destroy = salt.utils.functools.alias_function(remove, 'destroy') @_ensure_exists @@ -1319,7 +1319,7 @@ def copy_to(name, source, dest, overwrite=False, makedirs=False): overwrite=overwrite, makedirs=makedirs) -cp = salt.utils.alias_function(copy_to, 'cp') +cp = salt.utils.functools.alias_function(copy_to, 'cp') # Everything below requres systemd >= 219 @@ -1484,4 +1484,4 @@ def pull_dkr(url, name, index): ''' return _pull_image('dkr', url, name, index=index) -pull_docker = salt.utils.alias_function(pull_dkr, 'pull_docker') +pull_docker = salt.utils.functools.alias_function(pull_dkr, 'pull_docker') diff --git a/salt/modules/openstack_config.py b/salt/modules/openstack_config.py index 1033f4a68d..adc7712cb9 100644 --- a/salt/modules/openstack_config.py +++ b/salt/modules/openstack_config.py @@ -8,14 +8,10 @@ Modify, retrieve, or delete values from OpenStack configuration files. :platform: linux ''' +# Import Python libs from __future__ import absolute_import -# Import Salt libs -import salt.utils -import salt.exceptions - -import salt.utils.decorators.path - import shlex + try: import pipes HAS_DEPS = True @@ -29,6 +25,10 @@ elif HAS_DEPS and hasattr(pipes, 'quote'): else: _quote = None +# Import Salt libs +import salt.utils.decorators.path +import salt.exceptions + # Don't shadow built-in's. __func_alias__ = { 'set_': 'set' diff --git a/salt/modules/pacman.py b/salt/modules/pacman.py index 5088cc9020..e6b944f5cd 100644 --- a/salt/modules/pacman.py +++ b/salt/modules/pacman.py @@ -18,11 +18,11 @@ import logging import os.path # Import salt libs -import salt.utils # TODO: Remove this once alias_function, fnmatch_multiple are moved import salt.utils.args import salt.utils.data -import salt.utils.pkg +import salt.utils.functools import salt.utils.itertools +import salt.utils.pkg import salt.utils.systemd from salt.exceptions import CommandExecutionError, MinionError from salt.utils.versions import LooseVersion as _LooseVersion @@ -107,7 +107,7 @@ def latest_version(*names, **kwargs): return ret # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def upgrade_available(name): @@ -576,7 +576,7 @@ def install(name=None, _available = list_repo_pkgs(*[x[0] for x in wildcards], refresh=refresh) for pkgname, verstr in wildcards: candidates = _available.get(pkgname, []) - match = salt.utils.fnmatch_multiple(candidates, verstr) + match = salt.utils.itertools.fnmatch_multiple(candidates, verstr) if match is not None: targets.append('='.join((pkgname, match))) else: diff --git a/salt/modules/pagerduty.py b/salt/modules/pagerduty.py index 7d014bfeca..667f64f3ac 100644 --- a/salt/modules/pagerduty.py +++ b/salt/modules/pagerduty.py @@ -23,6 +23,7 @@ import yaml import json # Import salt libs +import salt.utils.functools import salt.utils.pagerduty from salt.ext.six import string_types @@ -121,7 +122,7 @@ def list_windows(profile=None, api_key=None): # The long version, added for consistency -list_maintenance_windows = salt.utils.alias_function(list_windows, 'list_maintenance_windows') +list_maintenance_windows = salt.utils.functools.alias_function(list_windows, 'list_maintenance_windows') def list_policies(profile=None, api_key=None): @@ -143,7 +144,7 @@ def list_policies(profile=None, api_key=None): # The long version, added for consistency -list_escalation_policies = salt.utils.alias_function(list_policies, 'list_escalation_policies') +list_escalation_policies = salt.utils.functools.alias_function(list_policies, 'list_escalation_policies') def create_event(service_key=None, description=None, details=None, diff --git a/salt/modules/pcs.py b/salt/modules/pcs.py index d99bc29ea5..e61caa2d48 100644 --- a/salt/modules/pcs.py +++ b/salt/modules/pcs.py @@ -13,7 +13,6 @@ Pacemaker/Cororsync conifguration system (PCS) from __future__ import absolute_import # Import salt libs -import salt.utils import salt.utils.path from salt.ext import six diff --git a/salt/modules/pdbedit.py b/salt/modules/pdbedit.py index a226398719..9472befcb1 100644 --- a/salt/modules/pdbedit.py +++ b/salt/modules/pdbedit.py @@ -20,7 +20,6 @@ except ImportError: from pipes import quote as _quote_args # Import Salt libs -import salt.utils import salt.utils.path log = logging.getLogger(__name__) diff --git a/salt/modules/pillar.py b/salt/modules/pillar.py index 990e0045b8..035e082a40 100644 --- a/salt/modules/pillar.py +++ b/salt/modules/pillar.py @@ -17,10 +17,10 @@ from salt.ext import six # Import salt libs import salt.pillar -import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.crypt import salt.utils.data import salt.utils.dictupdate +import salt.utils.functools import salt.utils.odict from salt.defaults import DEFAULT_TARGET_DELIM from salt.exceptions import CommandExecutionError @@ -267,7 +267,7 @@ def items(*args, **kwargs): return pillar.compile_pillar() # Allow pillar.data to also be used to return pillar data -data = salt.utils.alias_function(items, 'data') +data = salt.utils.functools.alias_function(items, 'data') def _obfuscate_inner(var): diff --git a/salt/modules/pkgin.py b/salt/modules/pkgin.py index f4a302b0c5..5406e606a1 100644 --- a/salt/modules/pkgin.py +++ b/salt/modules/pkgin.py @@ -17,8 +17,8 @@ import os import re # Import salt libs -import salt.utils # TODO: Remove this when alias_function is moved import salt.utils.data +import salt.utils.functools import salt.utils.path import salt.utils.pkg import salt.utils.decorators as decorators @@ -195,7 +195,7 @@ def latest_version(*names, **kwargs): # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def version(*names, **kwargs): diff --git a/salt/modules/pkgng.py b/salt/modules/pkgng.py index b7afdb199f..1125d182cd 100644 --- a/salt/modules/pkgng.py +++ b/salt/modules/pkgng.py @@ -44,9 +44,9 @@ import logging import os # Import salt libs -import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data import salt.utils.files +import salt.utils.functools import salt.utils.itertools import salt.utils.pkg import salt.utils.versions @@ -217,7 +217,7 @@ def version(*names, **kwargs): ]) # Support pkg.info get version info, since this is the CLI usage -info = salt.utils.alias_function(version, 'info') +info = salt.utils.functools.alias_function(version, 'info') def refresh_db(jail=None, chroot=None, root=None, force=False): @@ -266,7 +266,7 @@ def refresh_db(jail=None, chroot=None, root=None, force=False): # Support pkg.update to refresh the db, since this is the CLI usage -update = salt.utils.alias_function(refresh_db, 'update') +update = salt.utils.functools.alias_function(refresh_db, 'update') def latest_version(*names, **kwargs): @@ -345,7 +345,7 @@ def latest_version(*names, **kwargs): # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def list_pkgs(versions_as_list=False, @@ -1062,9 +1062,9 @@ def remove(name=None, return ret # Support pkg.delete to remove packages, since this is the CLI usage -delete = salt.utils.alias_function(remove, 'delete') +delete = salt.utils.functools.alias_function(remove, 'delete') # No equivalent to purge packages, use remove instead -purge = salt.utils.alias_function(remove, 'purge') +purge = salt.utils.functools.alias_function(remove, 'purge') def upgrade(*names, **kwargs): diff --git a/salt/modules/pkgutil.py b/salt/modules/pkgutil.py index ce0f7de66f..b4985fe762 100644 --- a/salt/modules/pkgutil.py +++ b/salt/modules/pkgutil.py @@ -14,8 +14,8 @@ from __future__ import absolute_import import copy # Import salt libs -import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data +import salt.utils.functools import salt.utils.pkg import salt.utils.versions from salt.exceptions import CommandExecutionError, MinionError @@ -241,7 +241,7 @@ def latest_version(*names, **kwargs): return ret # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def install(name=None, refresh=False, version=None, pkgs=None, **kwargs): diff --git a/salt/modules/qemu_img.py b/salt/modules/qemu_img.py index 8c947d78c9..a19bedd5e5 100644 --- a/salt/modules/qemu_img.py +++ b/salt/modules/qemu_img.py @@ -13,7 +13,6 @@ from __future__ import absolute_import import os # Import salt libs -import salt.utils import salt.utils.path diff --git a/salt/modules/qemu_nbd.py b/salt/modules/qemu_nbd.py index f5ffa3b36b..e2b79988df 100644 --- a/salt/modules/qemu_nbd.py +++ b/salt/modules/qemu_nbd.py @@ -15,7 +15,6 @@ import time import logging # Import salt libs -import salt.utils import salt.utils.path import salt.crypt diff --git a/salt/modules/rpmbuild.py b/salt/modules/rpmbuild.py index 6482553875..684e2b3bd1 100644 --- a/salt/modules/rpmbuild.py +++ b/salt/modules/rpmbuild.py @@ -24,9 +24,9 @@ import functools # Import salt libs from salt.exceptions import SaltInvocationError -import salt.utils # TODO: Remove this when chugid_and_umask is moved import salt.utils.files import salt.utils.path +import salt.utils.user import salt.utils.vt # Import 3rd-party libs @@ -487,7 +487,7 @@ def make_repo(repodir, times_looped = 0 error_msg = 'Failed to sign file {0}'.format(abs_file) cmd = 'rpm {0} --addsign {1}'.format(define_gpg_name, abs_file) - preexec_fn = functools.partial(salt.utils.chugid_and_umask, runas, None) + preexec_fn = functools.partial(salt.utils.user.chugid_and_umask, runas, None) try: stdout, stderr = None, None proc = salt.utils.vt.Terminal( diff --git a/salt/modules/rsync.py b/salt/modules/rsync.py index e536b063e4..2ed626b6e2 100644 --- a/salt/modules/rsync.py +++ b/salt/modules/rsync.py @@ -184,7 +184,7 @@ def rsync(src, else: raise CommandExecutionError('{0} does not exist'.format(src)) else: - tmp_src = salt.utils.mkstemp() + tmp_src = salt.utils.files.mkstemp() file_src = __salt__['cp.get_file'](_src, tmp_src, saltenv) diff --git a/salt/modules/saltutil.py b/salt/modules/saltutil.py index 337c5de7c0..12f8be9fa4 100644 --- a/salt/modules/saltutil.py +++ b/salt/modules/saltutil.py @@ -46,11 +46,11 @@ import salt.payload import salt.runner import salt.state import salt.transport -import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.args import salt.utils.event import salt.utils.extmods import salt.utils.files +import salt.utils.functools import salt.utils.minion import salt.utils.process import salt.utils.url @@ -621,7 +621,7 @@ def sync_output(saltenv=None, refresh=True, extmod_whitelist=None, extmod_blackl refresh_modules() return ret -sync_outputters = salt.utils.alias_function(sync_output, 'sync_outputters') +sync_outputters = salt.utils.functools.alias_function(sync_output, 'sync_outputters') def sync_clouds(saltenv=None, refresh=True, extmod_whitelist=None, extmod_blacklist=None): @@ -907,7 +907,7 @@ def refresh_pillar(): ret = False # Effectively a no-op, since we can't really return without an event system return ret -pillar_refresh = salt.utils.alias_function(refresh_pillar, 'pillar_refresh') +pillar_refresh = salt.utils.functools.alias_function(refresh_pillar, 'pillar_refresh') def refresh_modules(async=True): diff --git a/salt/modules/scsi.py b/salt/modules/scsi.py index eb0deccbbb..26914d2eca 100644 --- a/salt/modules/scsi.py +++ b/salt/modules/scsi.py @@ -6,7 +6,6 @@ from __future__ import absolute_import import os.path import logging -import salt.utils import salt.utils.path log = logging.getLogger(__name__) diff --git a/salt/modules/smartos_vmadm.py b/salt/modules/smartos_vmadm.py index 7b173c9fbb..ec5bb00584 100644 --- a/salt/modules/smartos_vmadm.py +++ b/salt/modules/smartos_vmadm.py @@ -14,12 +14,11 @@ except ImportError: from pipes import quote as _quote_args # Import Salt libs -import salt.utils import salt.utils.args -import salt.utils.path -import salt.utils.platform import salt.utils.decorators as decorators import salt.utils.files +import salt.utils.path +import salt.utils.platform from salt.utils.odict import OrderedDict # Import 3rd-party libs diff --git a/salt/modules/solarisips.py b/salt/modules/solarisips.py index be992325d0..7bcf96e6f2 100644 --- a/salt/modules/solarisips.py +++ b/salt/modules/solarisips.py @@ -43,8 +43,8 @@ import logging # Import salt libs -import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data +import salt.utils.functools import salt.utils.path import salt.utils.pkg from salt.exceptions import CommandExecutionError @@ -335,7 +335,7 @@ def latest_version(name, **kwargs): return '' # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def get_fmri(name, **kwargs): diff --git a/salt/modules/solarispkg.py b/salt/modules/solarispkg.py index bab2c4e30a..1f738c41b3 100644 --- a/salt/modules/solarispkg.py +++ b/salt/modules/solarispkg.py @@ -16,8 +16,8 @@ import os import logging # Import salt libs -import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data +import salt.utils.functools import salt.utils.files from salt.exceptions import CommandExecutionError, MinionError @@ -162,7 +162,7 @@ def latest_version(*names, **kwargs): return ret # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def upgrade_available(name): diff --git a/salt/modules/ssh.py b/salt/modules/ssh.py index cba2035534..a17fac5217 100644 --- a/salt/modules/ssh.py +++ b/salt/modules/ssh.py @@ -20,7 +20,6 @@ import subprocess # Import salt libs from salt.ext import six -import salt.utils import salt.utils.decorators.path import salt.utils.files import salt.utils.path diff --git a/salt/modules/state.py b/salt/modules/state.py index 2f20cdaa45..ebdff4b704 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -28,11 +28,11 @@ import time import salt.config import salt.payload import salt.state -import salt.utils # TODO: Remove this once namespaced_function is moved import salt.utils.args import salt.utils.data import salt.utils.event import salt.utils.files +import salt.utils.functools import salt.utils.hashutils import salt.utils.jid import salt.utils.platform @@ -76,7 +76,7 @@ def __virtual__(): ''' # Update global namespace with functions that are cloned in this module global _orchestrate - _orchestrate = salt.utils.namespaced_function(_orchestrate, globals()) + _orchestrate = salt.utils.functools.namespaced_function(_orchestrate, globals()) return __virtualname__ diff --git a/salt/modules/supervisord.py b/salt/modules/supervisord.py index 648a65e2b9..efee524ecb 100644 --- a/salt/modules/supervisord.py +++ b/salt/modules/supervisord.py @@ -14,7 +14,6 @@ from salt.ext.six import string_types from salt.ext.six.moves import configparser # pylint: disable=import-error # Import salt libs -import salt.utils import salt.utils.stringutils from salt.exceptions import CommandExecutionError, CommandNotFoundError diff --git a/salt/modules/test.py b/salt/modules/test.py index 86d434298d..fae0fdbe9a 100644 --- a/salt/modules/test.py +++ b/salt/modules/test.py @@ -13,11 +13,11 @@ import traceback import random # Import Salt libs -import salt -import salt.utils import salt.utils.args +import salt.utils.functools import salt.utils.hashutils import salt.utils.platform +import salt.utils.versions import salt.version import salt.loader from salt.ext import six @@ -198,7 +198,7 @@ def versions_report(): return '\n'.join(salt.version.versions_report()) -versions = salt.utils.alias_function(versions_report, 'versions') +versions = salt.utils.functools.alias_function(versions_report, 'versions') def conf_test(): @@ -495,7 +495,7 @@ def opts_pkg(): def rand_str(size=9999999999, hash_type=None): - salt.utils.warn_until( + salt.utils.versions.warn_until( 'Neon', 'test.rand_str has been renamed to test.random_hash' ) diff --git a/salt/modules/upstart.py b/salt/modules/upstart.py index dfd3a50338..980aa2e845 100644 --- a/salt/modules/upstart.py +++ b/salt/modules/upstart.py @@ -54,9 +54,9 @@ import itertools import fnmatch # Import salt libs -import salt.utils -import salt.utils.files import salt.modules.cmdmod +import salt.utils.files +import salt.utils.path import salt.utils.systemd __func_alias__ = { @@ -482,7 +482,7 @@ def _get_service_exec(): http://www.debian.org/doc/debian-policy/ch-opersys.html#s9.3.3 ''' executable = 'update-rc.d' - salt.utils.check_or_die(executable) + salt.utils.path.check_or_die(executable) return executable diff --git a/salt/modules/vagrant.py b/salt/modules/vagrant.py index f94fd01c82..91f6afcf26 100644 --- a/salt/modules/vagrant.py +++ b/salt/modules/vagrant.py @@ -34,7 +34,8 @@ import logging import os # Import salt libs -import salt.utils +import salt.utils.files +import salt.utils.path from salt.exceptions import CommandExecutionError, SaltInvocationError import salt.ext.six as six @@ -616,7 +617,7 @@ def get_ssh_config(name, network_mask='', get_private_key=False): if get_private_key: # retrieve the Vagrant private key from the host try: - with salt.utils.fopen(ssh_config['IdentityFile']) as pks: + with salt.utils.files.fopen(ssh_config['IdentityFile']) as pks: ans['private_key'] = pks.read() except (OSError, IOError) as e: raise CommandExecutionError("Error processing Vagrant private key file: {}".format(e)) diff --git a/salt/modules/vcenter.py b/salt/modules/vcenter.py index bac3c674b4..e73ac800be 100644 --- a/salt/modules/vcenter.py +++ b/salt/modules/vcenter.py @@ -6,7 +6,7 @@ from __future__ import absolute_import # Import python libs import logging -import salt.utils +import salt.utils.platform log = logging.getLogger(__name__) @@ -20,7 +20,7 @@ def __virtual__(): ''' Only work on proxy ''' - if salt.utils.is_proxy(): + if salt.utils.platform.is_proxy(): return __virtualname__ return False diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 3d291a6ebd..2d36b5b2d2 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -36,7 +36,6 @@ except ImportError: HAS_LIBVIRT = False # Import salt libs -import salt.utils import salt.utils.files import salt.utils.network import salt.utils.path diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index d3ba2a2529..a18ab459f3 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -170,14 +170,13 @@ from functools import wraps # Import Salt Libs from salt.ext import six -import salt.utils import salt.utils.args import salt.utils.dictupdate as dictupdate import salt.utils.http import salt.utils.path +import salt.utils.pbm import salt.utils.vmware import salt.utils.vsan -import salt.utils.pbm from salt.exceptions import CommandExecutionError, VMwareSaltError, \ ArgumentValueError, InvalidConfigError, VMwareObjectRetrievalError, \ VMwareApiError, InvalidEntityError, VMwareObjectExistsError diff --git a/salt/modules/win_file.py b/salt/modules/win_file.py index ba047e5da0..f1b55fafad 100644 --- a/salt/modules/win_file.py +++ b/salt/modules/win_file.py @@ -62,7 +62,7 @@ from salt.modules.file import (check_hash, # pylint: disable=W0611 list_backups_dir) from salt.modules.file import normpath as normpath_ -from salt.utils import namespaced_function as _namespaced_function +from salt.utils.functools import namespaced_function as _namespaced_function HAS_WINDOWS_MODULES = False try: diff --git a/salt/modules/win_network.py b/salt/modules/win_network.py index d01ea6d3e8..ec2102ea46 100644 --- a/salt/modules/win_network.py +++ b/salt/modules/win_network.py @@ -11,7 +11,6 @@ import datetime import socket # Import Salt libs -import salt.utils import salt.utils.network import salt.utils.platform import salt.utils.validate.net @@ -19,7 +18,7 @@ from salt.modules.network import (wol, get_hostname, interface, interface_ip, subnets6, ip_in_subnet, convert_cidr, calc_net, get_fqdn, ifacestartswith, iphexval) -from salt.utils import namespaced_function as _namespaced_function +from salt.utils.functools import namespaced_function as _namespaced_function try: import salt.utils.winapi @@ -316,7 +315,7 @@ def hw_addr(iface): return salt.utils.network.hw_addr(iface) # Alias hwaddr to preserve backward compat -hwaddr = salt.utils.alias_function(hw_addr, 'hwaddr') +hwaddr = salt.utils.functools.alias_function(hw_addr, 'hwaddr') def subnets(): @@ -360,7 +359,7 @@ def ip_addrs(interface=None, include_loopback=False): return salt.utils.network.ip_addrs(interface=interface, include_loopback=include_loopback) -ipaddrs = salt.utils.alias_function(ip_addrs, 'ipaddrs') +ipaddrs = salt.utils.functools.alias_function(ip_addrs, 'ipaddrs') def ip_addrs6(interface=None, include_loopback=False): @@ -378,7 +377,7 @@ def ip_addrs6(interface=None, include_loopback=False): return salt.utils.network.ip_addrs6(interface=interface, include_loopback=include_loopback) -ipaddrs6 = salt.utils.alias_function(ip_addrs6, 'ipaddrs6') +ipaddrs6 = salt.utils.functools.alias_function(ip_addrs6, 'ipaddrs6') def connect(host, port=None, **kwargs): diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index 3cae80a152..2c76ba3250 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -1281,10 +1281,10 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): arguments = ['/i', cached_pkg] if pkginfo[version_num].get('allusers', True): arguments.append('ALLUSERS="1"') - arguments.extend(salt.utils.shlex_split(install_flags, posix=False)) + arguments.extend(salt.utils.args.shlex_split(install_flags, posix=False)) else: cmd = cached_pkg - arguments = salt.utils.shlex_split(install_flags, posix=False) + arguments = salt.utils.args.shlex_split(install_flags, posix=False) # Install the software # Check Use Scheduler Option @@ -1620,10 +1620,10 @@ def remove(name=None, pkgs=None, version=None, **kwargs): if use_msiexec: cmd = msiexec arguments = ['/x'] - arguments.extend(salt.utils.shlex_split(uninstall_flags, posix=False)) + arguments.extend(salt.utils.args.shlex_split(uninstall_flags, posix=False)) else: cmd = expanded_cached_pkg - arguments = salt.utils.shlex_split(uninstall_flags, posix=False) + arguments = salt.utils.args.shlex_split(uninstall_flags, posix=False) # Uninstall the software # Check Use Scheduler Option diff --git a/salt/modules/win_pki.py b/salt/modules/win_pki.py index 329da531f0..2c5e28e711 100644 --- a/salt/modules/win_pki.py +++ b/salt/modules/win_pki.py @@ -25,6 +25,7 @@ import os # Import salt libs import salt.utils.platform import salt.utils.powershell +import salt.utils.versions from salt.exceptions import SaltInvocationError _DEFAULT_CONTEXT = 'LocalMachine' @@ -46,7 +47,7 @@ def __virtual__(): if not salt.utils.platform.is_windows(): return False, 'Only available on Windows Systems' - if salt.utils.version_cmp(__grains__['osversion'], '6.2.9200') == -1: + if salt.utils.versions.version_cmp(__grains__['osversion'], '6.2.9200') == -1: return False, 'Only available on Windows 8+ / Windows Server 2012 +' if not __salt__['cmd.shell_info']('powershell')['installed']: diff --git a/salt/modules/win_repo.py b/salt/modules/win_repo.py index 0d23e1c7fc..a087504e09 100644 --- a/salt/modules/win_repo.py +++ b/salt/modules/win_repo.py @@ -15,7 +15,7 @@ import os # Import salt libs import salt.output -import salt.utils +import salt.utils.functools import salt.utils.path import salt.utils.platform import salt.loader @@ -50,9 +50,9 @@ def __virtual__(): ''' if salt.utils.platform.is_windows(): global _genrepo, _update_git_repos - _genrepo = salt.utils.namespaced_function(_genrepo, globals()) + _genrepo = salt.utils.functools.namespaced_function(_genrepo, globals()) _update_git_repos = \ - salt.utils.namespaced_function(_update_git_repos, globals()) + salt.utils.functools.namespaced_function(_update_git_repos, globals()) return __virtualname__ return (False, 'This module only works on Windows.') diff --git a/salt/modules/win_status.py b/salt/modules/win_status.py index 9accd79ce8..e0f18e6bcb 100644 --- a/salt/modules/win_status.py +++ b/salt/modules/win_status.py @@ -22,7 +22,7 @@ import salt.utils.event import salt.utils.platform import salt.utils.stringutils from salt.utils.network import host_to_ips as _host_to_ips -from salt.utils import namespaced_function as _namespaced_function +from salt.utils.functools import namespaced_function as _namespaced_function # These imports needed for namespaced functions # pylint: disable=W0611 diff --git a/salt/modules/win_system.py b/salt/modules/win_system.py index 15652d8493..0f37fa1a44 100644 --- a/salt/modules/win_system.py +++ b/salt/modules/win_system.py @@ -21,7 +21,7 @@ import time from datetime import datetime # Import salt libs -import salt.utils +import salt.utils.functools import salt.utils.locales import salt.utils.platform from salt.exceptions import CommandExecutionError @@ -468,7 +468,7 @@ def set_computer_desc(desc=None): return {'Computer Description': get_computer_desc()} -set_computer_description = salt.utils.alias_function(set_computer_desc, 'set_computer_description') # pylint: disable=invalid-name +set_computer_description = salt.utils.functools.alias_function(set_computer_desc, 'set_computer_description') # pylint: disable=invalid-name def get_system_info(): @@ -542,7 +542,7 @@ def get_computer_desc(): return False if desc is None else desc -get_computer_description = salt.utils.alias_function(get_computer_desc, 'get_computer_description') # pylint: disable=invalid-name +get_computer_description = salt.utils.functools.alias_function(get_computer_desc, 'get_computer_description') # pylint: disable=invalid-name def get_hostname(): diff --git a/salt/modules/win_useradd.py b/salt/modules/win_useradd.py index 9c11e31226..e758ee3c26 100644 --- a/salt/modules/win_useradd.py +++ b/salt/modules/win_useradd.py @@ -35,8 +35,8 @@ except: # pylint: disable=W0702 from pipes import quote as _cmd_quote # Import Salt libs -import salt.utils import salt.utils.args +import salt.utils.dateutils import salt.utils.platform from salt.ext import six from salt.ext.six import string_types @@ -297,7 +297,7 @@ def update(name, user_info['acct_expires'] = win32netcon.TIMEQ_FOREVER else: try: - dt_obj = salt.utils.date_cast(expiration_date) + dt_obj = salt.utils.dateutils.date_cast(expiration_date) except (ValueError, RuntimeError): return 'Invalid Date/Time Format: {0}'.format(expiration_date) user_info['acct_expires'] = time.mktime(dt_obj.timetuple()) diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index 16cd9860ee..2abe1ee5f3 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -41,11 +41,11 @@ from salt.ext.six.moves import configparser # pylint: enable=import-error,redefined-builtin # Import Salt libs -import salt.utils # TODO: Remove this once alias_function, fnmatch_multiple are moved import salt.utils.args import salt.utils.data import salt.utils.decorators.path import salt.utils.files +import salt.utils.functools import salt.utils.itertools import salt.utils.lazy import salt.utils.pkg @@ -544,7 +544,7 @@ def latest_version(*names, **kwargs): return ret # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def upgrade_available(name): @@ -966,7 +966,7 @@ def list_upgrades(refresh=True, **kwargs): return dict([(x.name, x.version) for x in _yum_pkginfo(out['stdout'])]) # Preserve expected CLI usage (yum list updates) -list_updates = salt.utils.alias_function(list_upgrades, 'list_updates') +list_updates = salt.utils.functools.alias_function(list_upgrades, 'list_updates') def list_downloaded(): @@ -1483,7 +1483,7 @@ def install(name=None, if '*' in version_num: # Resolve wildcard matches candidates = _available.get(pkgname, []) - match = salt.utils.fnmatch_multiple(candidates, version_num) + match = salt.utils.itertools.fnmatch_multiple(candidates, version_num) if match is not None: version_num = match else: @@ -2249,7 +2249,7 @@ def list_holds(pattern=__HOLD_PATTERN, full=True): ret.append(match) return ret -get_locked_packages = salt.utils.alias_function(list_holds, 'get_locked_packages') +get_locked_packages = salt.utils.functools.alias_function(list_holds, 'get_locked_packages') def verify(*names, **kwargs): @@ -2556,7 +2556,7 @@ def group_install(name, return install(pkgs=pkgs, **kwargs) -groupinstall = salt.utils.alias_function(group_install, 'groupinstall') +groupinstall = salt.utils.functools.alias_function(group_install, 'groupinstall') def list_repos(basedir=None): diff --git a/salt/modules/zabbix.py b/salt/modules/zabbix.py index 7fc76f4a09..a73c5ae126 100644 --- a/salt/modules/zabbix.py +++ b/salt/modules/zabbix.py @@ -30,7 +30,7 @@ import socket import json # Import salt libs -import salt.utils +import salt.utils.http import salt.utils.path from salt.utils.versions import LooseVersion as _LooseVersion from salt.ext.six.moves.urllib.error import HTTPError, URLError # pylint: disable=import-error,no-name-in-module diff --git a/salt/modules/zookeeper.py b/salt/modules/zookeeper.py index c14c539e38..1483de3388 100644 --- a/salt/modules/zookeeper.py +++ b/salt/modules/zookeeper.py @@ -74,7 +74,6 @@ except ImportError: HAS_KAZOO = False # Import Salt libraries -import salt.utils import salt.utils.stringutils __virtualname__ = 'zookeeper' diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py index 12251fb33a..71b280c040 100644 --- a/salt/modules/zypper.py +++ b/salt/modules/zypper.py @@ -32,10 +32,10 @@ from xml.dom import minidom as dom from xml.parsers.expat import ExpatError # Import salt libs -import salt.utils # TODO: Remove this once alias_function is moved import salt.utils.data import salt.utils.event import salt.utils.files +import salt.utils.functools import salt.utils.path import salt.utils.pkg import salt.utils.systemd @@ -431,7 +431,7 @@ def list_upgrades(refresh=True, **kwargs): return ret # Provide a list_updates function for those used to using zypper list-updates -list_updates = salt.utils.alias_function(list_upgrades, 'list_updates') +list_updates = salt.utils.functools.alias_function(list_upgrades, 'list_updates') def info_installed(*names, **kwargs): @@ -589,7 +589,7 @@ def latest_version(*names, **kwargs): # available_version is being deprecated -available_version = salt.utils.alias_function(latest_version, 'available_version') +available_version = salt.utils.functools.alias_function(latest_version, 'available_version') def upgrade_available(name, **kwargs): diff --git a/salt/netapi/__init__.py b/salt/netapi/__init__.py index 99a33697f2..6932d8d275 100644 --- a/salt/netapi/__init__.py +++ b/salt/netapi/__init__.py @@ -14,7 +14,7 @@ import salt.config import salt.runner import salt.syspaths import salt.wheel -import salt.utils +import salt.utils.args import salt.client.ssh.client import salt.exceptions @@ -71,7 +71,7 @@ class NetapiClient(object): 'No authentication credentials given') l_fun = getattr(self, low['client']) - f_call = salt.utils.format_call(l_fun, low) + f_call = salt.utils.args.format_call(l_fun, low) return l_fun(*f_call.get('args', ()), **f_call.get('kwargs', {})) def local_async(self, *args, **kwargs): diff --git a/salt/netapi/rest_cherrypy/app.py b/salt/netapi/rest_cherrypy/app.py index 16a97da324..88f4ef91f3 100644 --- a/salt/netapi/rest_cherrypy/app.py +++ b/salt/netapi/rest_cherrypy/app.py @@ -603,7 +603,6 @@ import yaml # Import Salt libs import salt import salt.auth -import salt.utils import salt.utils.event import salt.utils.stringutils from salt.ext import six diff --git a/salt/netapi/rest_tornado/saltnado.py b/salt/netapi/rest_tornado/saltnado.py index d40edf44e0..7b93b32de3 100644 --- a/salt/netapi/rest_tornado/saltnado.py +++ b/salt/netapi/rest_tornado/saltnado.py @@ -214,15 +214,16 @@ ioloop.install() # salt imports import salt.netapi -import salt.utils +import salt.utils.args import salt.utils.event +import salt.utils.json from salt.utils.event import tagify import salt.client import salt.runner import salt.auth from salt.exceptions import EauthAuthenticationError -json = salt.utils.import_json() +json = salt.utils.json.import_json() logger = logging.getLogger() # The clients rest_cherrypi supports. We want to mimic the interface, but not @@ -1056,9 +1057,13 @@ class SaltAPIHandler(BaseSaltAPIHandler, SaltClientsMixIn): # pylint: disable=W pub_data = self.saltclients['runner'](chunk) raise tornado.gen.Return(pub_data) - # salt.utils.format_call doesn't work for functions having the annotation tornado.gen.coroutine + # salt.utils.args.format_call doesn't work for functions having the + # annotation tornado.gen.coroutine def _format_call_run_job_async(self, chunk): - f_call = salt.utils.format_call(salt.client.LocalClient.run_job, chunk, is_class_method=True) + f_call = salt.utils.args.format_call( + salt.client.LocalClient.run_job, + chunk, + is_class_method=True) f_call.get('kwargs', {})['io_loop'] = tornado.ioloop.IOLoop.current() return f_call diff --git a/salt/netapi/rest_tornado/saltnado_websockets.py b/salt/netapi/rest_tornado/saltnado_websockets.py index 33c37f1872..dfc8e1719a 100644 --- a/salt/netapi/rest_tornado/saltnado_websockets.py +++ b/salt/netapi/rest_tornado/saltnado_websockets.py @@ -297,10 +297,10 @@ from .saltnado import _check_cors_origin import tornado.gen -import salt.utils +import salt.utils.json import salt.netapi -json = salt.utils.import_json() +json = salt.utils.json.import_json() import logging logger = logging.getLogger() diff --git a/salt/output/__init__.py b/salt/output/__init__.py index 6cb90a47e1..ff9d85dbe0 100644 --- a/salt/output/__init__.py +++ b/salt/output/__init__.py @@ -16,9 +16,9 @@ import traceback # Import Salt libs import salt.loader -import salt.utils import salt.utils.files import salt.utils.platform +import salt.utils.stringutils # Import 3rd-party libs from salt.ext import six @@ -127,7 +127,7 @@ def display_output(data, out=None, opts=None, **kwargs): ofh.close() return if display_data: - salt.utils.print_cli(display_data) + salt.utils.stringutils.print_cli(display_data) except IOError as exc: # Only raise if it's NOT a broken pipe if exc.errno != errno.EPIPE: diff --git a/salt/pillar/digicert.py b/salt/pillar/digicert.py index 5a45864b3b..3d2a0b8a51 100644 --- a/salt/pillar/digicert.py +++ b/salt/pillar/digicert.py @@ -15,7 +15,6 @@ section of your ``master`` configuration file: ''' from __future__ import absolute_import import logging -import salt.utils import salt.cache import salt.syspaths as syspaths diff --git a/salt/pillar/django_orm.py b/salt/pillar/django_orm.py index b87260110d..9d8b0379e9 100644 --- a/salt/pillar/django_orm.py +++ b/salt/pillar/django_orm.py @@ -97,7 +97,6 @@ import sys import salt.exceptions from salt.ext import six -import salt.utils import salt.utils.stringutils HAS_VIRTUALENV = False diff --git a/salt/pillar/gpg.py b/salt/pillar/gpg.py index 477954fc68..35985d0312 100644 --- a/salt/pillar/gpg.py +++ b/salt/pillar/gpg.py @@ -18,7 +18,7 @@ Set ``gpg_keydir`` in your config to adjust the homedir the renderer uses. ''' from __future__ import absolute_import -import salt.utils +import salt.loader def ext_pillar(minion_id, pillar, *args, **kwargs): diff --git a/salt/pillar/hg_pillar.py b/salt/pillar/hg_pillar.py index 32c5bd9a04..a7c5db03d3 100644 --- a/salt/pillar/hg_pillar.py +++ b/salt/pillar/hg_pillar.py @@ -27,7 +27,6 @@ import os # Import Salt Libs import salt.pillar -import salt.utils import salt.utils.stringutils # Import Third Party Libs diff --git a/salt/pillar/venafi.py b/salt/pillar/venafi.py index 9c93729ac7..e6b0f0989b 100644 --- a/salt/pillar/venafi.py +++ b/salt/pillar/venafi.py @@ -15,7 +15,6 @@ section of your ``master`` configuration file: ''' from __future__ import absolute_import import logging -import salt.utils import salt.cache import salt.syspaths as syspaths diff --git a/salt/renderers/json.py b/salt/renderers/json.py index a3f8a402b4..c3db661335 100644 --- a/salt/renderers/json.py +++ b/salt/renderers/json.py @@ -6,8 +6,8 @@ JSON Renderer for Salt from __future__ import absolute_import # Import python libs -import salt.utils -json = salt.utils.import_json() +import salt.utils.json +json = salt.utils.json.import_json() # Import salt libs from salt.ext.six import string_types diff --git a/salt/renderers/nacl.py b/salt/renderers/nacl.py index 91ba558e9d..266fa624ce 100644 --- a/salt/renderers/nacl.py +++ b/salt/renderers/nacl.py @@ -58,7 +58,6 @@ import re import logging # Import salt libs -import salt.utils import salt.utils.stringio import salt.syspaths diff --git a/salt/returners/couchbase_return.py b/salt/returners/couchbase_return.py index b9e770bb13..a454d02190 100644 --- a/salt/returners/couchbase_return.py +++ b/salt/returners/couchbase_return.py @@ -60,8 +60,9 @@ except ImportError: HAS_DEPS = False # Import salt libs -import salt.utils import salt.utils.jid +import salt.utils.json +import salt.utils.minions log = logging.getLogger(__name__) @@ -80,7 +81,7 @@ def __virtual__(): return False, 'Could not import couchbase returner; couchbase is not installed.' # try to load some faster json libraries. In order of fastest to slowest - json = salt.utils.import_json() + json = salt.utils.json.import_json() couchbase.set_json_converters(json.dumps, json.loads) return __virtualname__ diff --git a/salt/returners/etcd_return.py b/salt/returners/etcd_return.py index 5f3d8a2bae..e6e2fddf51 100644 --- a/salt/returners/etcd_return.py +++ b/salt/returners/etcd_return.py @@ -77,7 +77,6 @@ try: except ImportError: HAS_LIBS = False -import salt.utils import salt.utils.jid log = logging.getLogger(__name__) diff --git a/salt/returners/librato_return.py b/salt/returners/librato_return.py index 4f36f908ef..e14b7c4697 100644 --- a/salt/returners/librato_return.py +++ b/salt/returners/librato_return.py @@ -35,7 +35,6 @@ from __future__ import absolute_import, print_function import logging # Import Salt libs -import salt.utils import salt.utils.jid import salt.returners diff --git a/salt/returners/postgres_local_cache.py b/salt/returners/postgres_local_cache.py index 7b1a134458..497d8fc56d 100644 --- a/salt/returners/postgres_local_cache.py +++ b/salt/returners/postgres_local_cache.py @@ -115,7 +115,6 @@ import re import sys # Import salt libs -import salt.utils import salt.utils.jid from salt.ext import six diff --git a/salt/returners/rawfile_json.py b/salt/returners/rawfile_json.py index 0341a7bce4..9755935496 100644 --- a/salt/returners/rawfile_json.py +++ b/salt/returners/rawfile_json.py @@ -23,7 +23,6 @@ import logging import json import salt.returners -import salt.utils import salt.utils.files log = logging.getLogger(__name__) diff --git a/salt/returners/sms_return.py b/salt/returners/sms_return.py index 97c55b0c61..81694115e8 100644 --- a/salt/returners/sms_return.py +++ b/salt/returners/sms_return.py @@ -29,11 +29,8 @@ To use the sms returner, append '--return sms' to the salt command. ''' from __future__ import absolute_import - - import logging -import salt.utils import salt.returners log = logging.getLogger(__name__) diff --git a/salt/returners/zabbix_return.py b/salt/returners/zabbix_return.py index 9969e3365c..22f9168c8e 100644 --- a/salt/returners/zabbix_return.py +++ b/salt/returners/zabbix_return.py @@ -56,7 +56,7 @@ def zbx(): def zabbix_send(key, host, output): - with salt.utils.fopen(zbx()['zabbix_config'], 'r') as file_handle: + with salt.utils.files.fopen(zbx()['zabbix_config'], 'r') as file_handle: for line in file_handle: if "ServerActive" in line: flag = "true" diff --git a/salt/roster/cloud.py b/salt/roster/cloud.py index 1bd0009156..2b21ecdbda 100644 --- a/salt/roster/cloud.py +++ b/salt/roster/cloud.py @@ -24,7 +24,6 @@ import os # Import Salt libs import salt.loader -import salt.utils import salt.utils.cloud import salt.utils.validate.net import salt.config diff --git a/salt/roster/sshconfig.py b/salt/roster/sshconfig.py index 29c01ccaf3..83a9bc4485 100644 --- a/salt/roster/sshconfig.py +++ b/salt/roster/sshconfig.py @@ -15,7 +15,7 @@ import fnmatch import re # Import Salt libs -import salt.utils +import salt.utils.files from salt.ext.six import string_types import logging @@ -98,7 +98,7 @@ def targets(tgt, tgt_type='glob', **kwargs): defaults to /etc/salt/roster ''' ssh_config_file = _get_ssh_config_file(__opts__) - with salt.utils.fopen(ssh_config_file, 'r') as fp: + with salt.utils.files.fopen(ssh_config_file, 'r') as fp: all_minions = parse_ssh_config([line.rstrip() for line in fp]) rmatcher = RosterMatcher(all_minions, tgt, tgt_type) matched = rmatcher.targets() diff --git a/salt/runners/mine.py b/salt/runners/mine.py index 081c27536b..140a853b23 100644 --- a/salt/runners/mine.py +++ b/salt/runners/mine.py @@ -8,7 +8,6 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils import salt.utils.minions log = logging.getLevelName(__name__) diff --git a/salt/runners/pagerduty.py b/salt/runners/pagerduty.py index fcdfdd8126..b0791f0ee5 100644 --- a/salt/runners/pagerduty.py +++ b/salt/runners/pagerduty.py @@ -22,7 +22,7 @@ import yaml import json # Import salt libs -import salt.utils +import salt.utils.functools import salt.utils.pagerduty from salt.ext.six import string_types @@ -121,7 +121,7 @@ def list_windows(profile=None, api_key=None): # The long version, added for consistency -list_maintenance_windows = salt.utils.alias_function(list_windows, 'list_maintenance_windows') +list_maintenance_windows = salt.utils.functools.alias_function(list_windows, 'list_maintenance_windows') def list_policies(profile=None, api_key=None): @@ -143,7 +143,7 @@ def list_policies(profile=None, api_key=None): # The long version, added for consistency -list_escalation_policies = salt.utils.alias_function(list_policies, 'list_escalation_policies') +list_escalation_policies = salt.utils.functools.alias_function(list_policies, 'list_escalation_policies') def create_event(service_key=None, description=None, details=None, diff --git a/salt/runners/state.py b/salt/runners/state.py index 25f12a814e..e114dfafb1 100644 --- a/salt/runners/state.py +++ b/salt/runners/state.py @@ -8,8 +8,8 @@ import logging # Import salt libs import salt.loader -import salt.utils import salt.utils.event +import salt.utils.functools from salt.exceptions import SaltInvocationError LOGGER = logging.getLogger(__name__) @@ -89,8 +89,8 @@ def orchestrate(mods, return ret # Aliases for orchestrate runner -orch = salt.utils.alias_function(orchestrate, 'orch') -sls = salt.utils.alias_function(orchestrate, 'sls') +orch = salt.utils.functools.alias_function(orchestrate, 'orch') +sls = salt.utils.functools.alias_function(orchestrate, 'sls') def orchestrate_single(fun, name, test=None, queue=False, pillar=None, **kwargs): @@ -197,7 +197,7 @@ def orchestrate_show_sls(mods, ret = {minion.opts['id']: running} return ret -orch_show_sls = salt.utils.alias_function(orchestrate_show_sls, 'orch_show_sls') +orch_show_sls = salt.utils.functools.alias_function(orchestrate_show_sls, 'orch_show_sls') def event(tagmatch='*', diff --git a/salt/state.py b/salt/state.py index 5527a0f87c..ae6b839091 100644 --- a/salt/state.py +++ b/salt/state.py @@ -27,7 +27,6 @@ import time import random # Import salt libs -import salt.utils import salt.loader import salt.minion import salt.pillar @@ -38,10 +37,9 @@ import salt.utils.dictupdate import salt.utils.event import salt.utils.files import salt.utils.immutabletypes as immutabletypes -import salt.utils.url import salt.utils.platform import salt.utils.process -import salt.utils.files +import salt.utils.url import salt.syspaths as syspaths from salt.template import compile_template, compile_template_str from salt.exceptions import ( @@ -165,6 +163,18 @@ def _l_tag(name, id_): return _gen_tag(low) +def get_accumulator_dir(cachedir): + ''' + Return the directory that accumulator data is stored in, creating it if it + doesn't exist. + ''' + fn_ = os.path.join(cachedir, 'accumulator') + if not os.path.isdir(fn_): + # accumulator_dir is not present, create it + os.makedirs(fn_) + return fn_ + + def trim_req(req): ''' Trim any function off of a requisite @@ -1825,7 +1835,7 @@ class State(object): self.load_modules(low) state_func_name = u'{0[state]}.{0[fun]}'.format(low) - cdata = salt.utils.format_call( + cdata = salt.utils.args.format_call( self.states[state_func_name], low, initial_ret={u'full': state_func_name}, @@ -2593,7 +2603,7 @@ class State(object): def _cleanup_accumulator_data(): accum_data_path = os.path.join( - salt.utils.get_accumulator_dir(self.opts[u'cachedir']), + get_accumulator_dir(self.opts[u'cachedir']), self.instance_id ) try: diff --git a/salt/states/cmd.py b/salt/states/cmd.py index 7b7ef0aecd..3d5a13b959 100644 --- a/salt/states/cmd.py +++ b/salt/states/cmd.py @@ -240,8 +240,8 @@ import json import logging # Import salt libs -import salt.utils import salt.utils.args +import salt.utils.functools from salt.exceptions import CommandExecutionError, SaltRenderError from salt.ext.six import string_types @@ -511,7 +511,7 @@ def wait(name, # Alias "cmd.watch" to "cmd.wait", as this is a common misconfiguration -watch = salt.utils.alias_function(wait, 'watch') +watch = salt.utils.functools.alias_function(wait, 'watch') def wait_script(name, diff --git a/salt/states/docker_container.py b/salt/states/docker_container.py index 3e5a7646dc..ca7f2492cc 100644 --- a/salt/states/docker_container.py +++ b/salt/states/docker_container.py @@ -51,7 +51,6 @@ import logging # Import salt libs from salt.exceptions import CommandExecutionError import copy -import salt.utils import salt.utils.args import salt.utils.docker from salt.ext import six diff --git a/salt/states/event.py b/salt/states/event.py index 74861594c2..9352ffab2b 100644 --- a/salt/states/event.py +++ b/salt/states/event.py @@ -5,7 +5,7 @@ Send events through Salt's event system during state runs from __future__ import absolute_import # import salt libs -import salt.utils +import salt.utils.functools def send(name, @@ -90,5 +90,5 @@ def wait(name, sfun=None): return {'name': name, 'changes': {}, 'result': True, 'comment': ''} -mod_watch = salt.utils.alias_function(send, 'mod_watch') -fire_master = salt.utils.alias_function(send, 'fire_master') +mod_watch = salt.utils.functools.alias_function(send, 'mod_watch') +fire_master = salt.utils.functools.alias_function(send, 'fire_master') diff --git a/salt/states/file.py b/salt/states/file.py index 06011f3fb9..043683df7e 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -282,16 +282,18 @@ from datetime import datetime # python3 problem in the making? # Import salt libs import salt.loader import salt.payload -import salt.utils +import salt.utils.dateutils import salt.utils.dictupdate import salt.utils.files import salt.utils.hashutils import salt.utils.platform +import salt.utils.stringutils import salt.utils.templates import salt.utils.url import salt.utils.versions from salt.utils.locales import sdecode from salt.exceptions import CommandExecutionError, SaltInvocationError +from salt.state import get_accumulator_dir as _get_accumulator_dir if salt.utils.platform.is_windows(): import salt.utils.win_dacl @@ -315,8 +317,10 @@ def _get_accumulator_filepath(): ''' Return accumulator data path. ''' - return os.path.join(salt.utils.get_accumulator_dir(__opts__['cachedir']), - __instance_id__) + return os.path.join( + _get_accumulator_dir(__opts__['cachedir']), + __instance_id__ + ) def _load_accumulators(): @@ -424,7 +428,7 @@ def _gen_recurse_managed_files( srelpath = posixpath.relpath(lname, srcpath) if not _is_valid_relpath(srelpath, maxdepth=maxdepth): continue - if not salt.utils.check_include_exclude( + if not salt.utils.stringutils.check_include_exclude( srelpath, include_pat, exclude_pat): continue # Check for all paths that begin with the symlink @@ -481,7 +485,7 @@ def _gen_recurse_managed_files( # Check if it is to be excluded. Match only part of the path # relative to the target directory - if not salt.utils.check_include_exclude( + if not salt.utils.stringutils.check_include_exclude( relname, include_pat, exclude_pat): continue dest = full_path(relname) @@ -502,7 +506,7 @@ def _gen_recurse_managed_files( relname = posixpath.relpath(mdir, srcpath) if not _is_valid_relpath(relname, maxdepth=maxdepth): continue - if not salt.utils.check_include_exclude( + if not salt.utils.stringutils.check_include_exclude( relname, include_pat, exclude_pat): continue mdest = full_path(relname) @@ -640,7 +644,7 @@ def _clean_dir(root, keep, exclude_pat): if nfn not in real_keep: # -- check if this is a part of exclude_pat(only). No need to # check include_pat - if not salt.utils.check_include_exclude( + if not salt.utils.stringutils.check_include_exclude( os.path.relpath(nfn, root), None, exclude_pat): return removed.add(nfn) @@ -730,7 +734,7 @@ def _check_directory(name, if path in keep: return {} else: - if not salt.utils.check_include_exclude( + if not salt.utils.stringutils.check_include_exclude( os.path.relpath(path, name), None, exclude_pat): return {} else: @@ -3554,7 +3558,7 @@ def retention_schedule(name, retain, strptime_format=None, timezone=None): def get_file_time_from_strptime(f): try: ts = datetime.strptime(f, strptime_format) - ts_epoch = salt.utils.total_seconds(ts - beginning_of_unix_time) + ts_epoch = salt.utils.dateutils.total_seconds(ts - beginning_of_unix_time) return (ts, ts_epoch) except ValueError: # Files which don't match the pattern are not relevant files. @@ -4720,7 +4724,7 @@ def append(name, if ignore_whitespace: if __salt__['file.search']( name, - salt.utils.build_whitespace_split_regex(chunk), + salt.utils.stringutils.build_whitespace_split_regex(chunk), multiline=True): continue elif __salt__['file.search']( @@ -4916,7 +4920,7 @@ def prepend(name, if not header: if __salt__['file.search']( name, - salt.utils.build_whitespace_split_regex(chunk), + salt.utils.stringutils.build_whitespace_split_regex(chunk), multiline=True): continue diff --git a/salt/states/libcloud_dns.py b/salt/states/libcloud_dns.py index 519ae4c380..17c8d44559 100644 --- a/salt/states/libcloud_dns.py +++ b/salt/states/libcloud_dns.py @@ -51,7 +51,6 @@ Example: from __future__ import absolute_import # Import salt libs -import salt.utils import salt.utils.compat diff --git a/salt/states/libcloud_loadbalancer.py b/salt/states/libcloud_loadbalancer.py index b9763231e8..5b9c41dc70 100644 --- a/salt/states/libcloud_loadbalancer.py +++ b/salt/states/libcloud_loadbalancer.py @@ -50,7 +50,6 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils import salt.utils.compat log = logging.getLogger(__name__) diff --git a/salt/states/libcloud_storage.py b/salt/states/libcloud_storage.py index 10d5476bef..5938624598 100644 --- a/salt/states/libcloud_storage.py +++ b/salt/states/libcloud_storage.py @@ -69,7 +69,6 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils import salt.utils.compat log = logging.getLogger(__name__) diff --git a/salt/states/module.py b/salt/states/module.py index a253db9ae9..4274db164f 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -176,7 +176,8 @@ from __future__ import absolute_import # Import salt libs import salt.loader -import salt.utils +import salt.utils.args +import salt.utils.functools import salt.utils.jid from salt.ext.six.moves import range from salt.ext.six.moves import zip @@ -212,7 +213,7 @@ def wait(name, **kwargs): 'comment': ''} # Alias module.watch to module.wait -watch = salt.utils.alias_function(wait, 'watch') +watch = salt.utils.functools.alias_function(wait, 'watch') @with_deprecated(globals(), "Sodium", policy=with_deprecated.OPT_IN) @@ -523,4 +524,4 @@ def _get_result(func_ret, changes): return res -mod_watch = salt.utils.alias_function(run, 'mod_watch') +mod_watch = salt.utils.functools.alias_function(run, 'mod_watch') diff --git a/salt/states/pkg.py b/salt/states/pkg.py index 159f110cbc..3b2d9ea495 100644 --- a/salt/states/pkg.py +++ b/salt/states/pkg.py @@ -85,7 +85,7 @@ import salt.utils.pkg import salt.utils.platform import salt.utils.versions from salt.output import nested -from salt.utils import namespaced_function as _namespaced_function +from salt.utils.functools import namespaced_function as _namespaced_function from salt.utils.odict import OrderedDict as _OrderedDict from salt.exceptions import ( CommandExecutionError, MinionError, SaltInvocationError diff --git a/salt/states/rabbitmq_cluster.py b/salt/states/rabbitmq_cluster.py index 22316e99e0..6deff50d71 100644 --- a/salt/states/rabbitmq_cluster.py +++ b/salt/states/rabbitmq_cluster.py @@ -18,7 +18,7 @@ from __future__ import absolute_import import logging # Import salt libs -import salt.utils +import salt.utils.functools import salt.utils.path log = logging.getLogger(__name__) @@ -78,4 +78,4 @@ def joined(name, host, user='rabbit', ram_node=None, runas='root'): # Alias join to preserve backward compat -join = salt.utils.alias_function(joined, 'join') +join = salt.utils.functools.alias_function(joined, 'join') diff --git a/salt/states/user.py b/salt/states/user.py index 03a2ac1590..1bee7c9660 100644 --- a/salt/states/user.py +++ b/salt/states/user.py @@ -29,7 +29,7 @@ import os import logging # Import Salt libs -import salt.utils # TODO: Remove this once date_format is moved +import salt.utils.dateutils import salt.utils.platform import salt.utils.user from salt.utils.locales import sdecode, sdecode_if_string @@ -148,7 +148,7 @@ def _changes(name, if expire and lshad['expire'] != expire: change['expire'] = expire elif 'shadow.info' in __salt__ and salt.utils.platform.is_windows(): - if expire and expire is not -1 and salt.utils.date_format(lshad['expire']) != salt.utils.date_format(expire): + if expire and expire is not -1 and salt.utils.dateutils.strftime(lshad['expire']) != salt.utils.dateutils.strftime(expire): change['expire'] = expire # GECOS fields @@ -752,7 +752,7 @@ def present(name, if expire: __salt__['shadow.set_expire'](name, expire) spost = __salt__['shadow.info'](name) - if salt.utils.date_format(spost['expire']) != salt.utils.date_format(expire): + if salt.utils.dateutils.strftime(spost['expire']) != salt.utils.dateutils.strftime(expire): ret['comment'] = 'User {0} created but failed to set' \ ' expire days to' \ ' {1}'.format(name, expire) diff --git a/salt/states/virtualenv_mod.py b/salt/states/virtualenv_mod.py index bd394bfd0b..c3a9a452a8 100644 --- a/salt/states/virtualenv_mod.py +++ b/salt/states/virtualenv_mod.py @@ -12,7 +12,7 @@ import os # Import Salt libs import salt.version -import salt.utils # TODO: Remove this once alias_function is moved +import salt.utils.functools import salt.utils.platform import salt.utils.versions from salt.exceptions import CommandExecutionError, CommandNotFoundError @@ -313,4 +313,4 @@ def managed(name, 'old': old if old else ''} return ret -manage = salt.utils.alias_function(managed, 'manage') +manage = salt.utils.functools.alias_function(managed, 'manage') diff --git a/salt/states/win_system.py b/salt/states/win_system.py index 59a6d23fcd..8d45032aed 100644 --- a/salt/states/win_system.py +++ b/salt/states/win_system.py @@ -22,7 +22,7 @@ from __future__ import absolute_import import logging # Import Salt libs -import salt.utils +import salt.utils.functools import salt.utils.platform log = logging.getLogger(__name__) @@ -77,7 +77,7 @@ def computer_desc(name): '\'{0}\''.format(name)) return ret -computer_description = salt.utils.alias_function(computer_desc, 'computer_description') +computer_description = salt.utils.functools.alias_function(computer_desc, 'computer_description') def computer_name(name): diff --git a/salt/states/winrepo.py b/salt/states/winrepo.py index 3e5bfdab4a..c3cd1132d2 100644 --- a/salt/states/winrepo.py +++ b/salt/states/winrepo.py @@ -11,7 +11,6 @@ import itertools # Salt Modules import salt.runner -import salt.utils import salt.config import salt.syspaths diff --git a/salt/states/x509.py b/salt/states/x509.py index 58fbabac34..c182bcf7c1 100644 --- a/salt/states/x509.py +++ b/salt/states/x509.py @@ -163,7 +163,6 @@ import copy # Import Salt Libs import salt.exceptions -import salt.utils # Import 3rd-party libs from salt.ext import six diff --git a/salt/states/zone.py b/salt/states/zone.py index 02a324111e..1f50782cc4 100644 --- a/salt/states/zone.py +++ b/salt/states/zone.py @@ -115,10 +115,9 @@ from __future__ import absolute_import import logging # Import Salt libs -import salt.utils import salt.utils.args -import salt.utils.files import salt.utils.atomicfile +import salt.utils.files from salt.modules.zonecfg import _parse_value, _zonecfg_resource_default_selectors from salt.exceptions import CommandExecutionError from salt.utils.odict import OrderedDict diff --git a/salt/thorium/check.py b/salt/thorium/check.py index 1196280cbe..d7e656d6aa 100644 --- a/salt/thorium/check.py +++ b/salt/thorium/check.py @@ -9,7 +9,7 @@ of having a command execution get gated by a check state via a requisite. from __future__ import absolute_import import logging -import salt.utils +import salt.utils.stringutils log = logging.getLogger(__file__) @@ -299,7 +299,7 @@ def event(name): 'result': False} for event in __events__: - if salt.utils.expr_match(event['tag'], name): + if salt.utils.stringutils.expr_match(event['tag'], name): ret['result'] = True return ret diff --git a/salt/thorium/file.py b/salt/thorium/file.py index 931bb819dd..41afec83b7 100644 --- a/salt/thorium/file.py +++ b/salt/thorium/file.py @@ -44,7 +44,7 @@ import os import json # Import salt libs -import salt.utils +import salt.utils.data import salt.utils.files @@ -81,7 +81,7 @@ def save(name, filter=False): with salt.utils.files.fopen(fn_, 'w+') as fp_: if filter is True: fp_.write(json.dumps( - salt.utils.simple_types_filter(__reg__)) + salt.utils.data.simple_types_filter(__reg__)) ) else: fp_.write(json.dumps(__reg__)) diff --git a/salt/thorium/reg.py b/salt/thorium/reg.py index 99b856a736..c5859a8375 100644 --- a/salt/thorium/reg.py +++ b/salt/thorium/reg.py @@ -6,7 +6,7 @@ values are stored and computed, such as averages etc. # import python libs from __future__ import absolute_import, division -import salt.utils +import salt.utils.stringutils __func_alias__ = { 'set_': 'set', @@ -35,7 +35,7 @@ def set_(name, add, match): __reg__[name] = {} __reg__[name]['val'] = set() for event in __events__: - if salt.utils.expr_match(event['tag'], match): + if salt.utils.stringutils.expr_match(event['tag'], match): try: val = event['data']['data'].get(add) except KeyError: @@ -79,7 +79,7 @@ def list_(name, add, match, stamp=False, prune=0): event_data = event['data']['data'] except KeyError: event_data = event['data'] - if salt.utils.expr_match(event['tag'], match): + if salt.utils.stringutils.expr_match(event['tag'], match): item = {} for key in add: if key in event_data: @@ -121,7 +121,7 @@ def mean(name, add, match): event_data = event['data']['data'] except KeyError: event_data = event['data'] - if salt.utils.expr_match(event['tag'], match): + if salt.utils.stringutils.expr_match(event['tag'], match): if add in event_data: try: comp = int(event_data) diff --git a/salt/tokens/localfs.py b/salt/tokens/localfs.py index e2c45c254a..29bd9864e8 100644 --- a/salt/tokens/localfs.py +++ b/salt/tokens/localfs.py @@ -10,7 +10,7 @@ import hashlib import os import logging -import salt.utils +import salt.utils.files import salt.payload log = logging.getLogger(__name__) diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py index ffc6871949..b85b04b89f 100644 --- a/salt/transport/tcp.py +++ b/salt/transport/tcp.py @@ -19,7 +19,6 @@ import errno # Import Salt Libs import salt.crypt -import salt.utils import salt.utils.async import salt.utils.event import salt.utils.platform diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py index 57ffebcc63..1d4a4e4516 100644 --- a/salt/transport/zeromq.py +++ b/salt/transport/zeromq.py @@ -18,11 +18,12 @@ from random import randint # Import Salt Libs import salt.auth import salt.crypt -import salt.utils -import salt.utils.verify import salt.utils.event +import salt.utils.minions import salt.utils.process import salt.utils.stringutils +import salt.utils.verify +import salt.utils.zeromq import salt.payload import salt.transport.client import salt.transport.server diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 0fa4232bbf..3b0bcd0af3 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -2,237 +2,53 @@ ''' Some of the utils used by salt -NOTE: The dev team is working on splitting up this file for the Oxygen release. -Please do not add any new functions to this file. New functions should be -organized in other files under salt/utils/. Please consult the dev team if you -are unsure where a new function should go. +PLEASE DO NOT ADD ANY NEW FUNCTIONS TO THIS FILE. + +New functions should be organized in other files under salt/utils/. Please +consult the dev team if you are unsure where a new function should go. ''' -# Import python libs -from __future__ import absolute_import, division, print_function -import contextlib -import copy -import collections -import datetime -import errno -import fnmatch -import hashlib -import json -import logging -import os -import random -import re -import shlex -import shutil -import sys -import pstats -import time -import types -import string -import subprocess +# Import Python libs +from __future__ import absolute_import + +# Import Salt libs +from salt.defaults import DEFAULT_TARGET_DELIM # Import 3rd-party libs from salt.ext import six -# pylint: disable=import-error -# pylint: disable=redefined-builtin -from salt.ext.six.moves import range -# pylint: enable=import-error,redefined-builtin - -if six.PY3: - import importlib.util # pylint: disable=no-name-in-module,import-error -else: - import imp - -try: - import cProfile - HAS_CPROFILE = True -except ImportError: - HAS_CPROFILE = False - -try: - import timelib - HAS_TIMELIB = True -except ImportError: - HAS_TIMELIB = False - -try: - import parsedatetime - HAS_PARSEDATETIME = True -except ImportError: - HAS_PARSEDATETIME = False - -try: - import win32api - HAS_WIN32API = True -except ImportError: - HAS_WIN32API = False - -# Import salt libs -from salt.defaults import DEFAULT_TARGET_DELIM -import salt.defaults.exitcodes -import salt.log -import salt.utils.dictupdate -import salt.utils.versions -import salt.version -from salt.utils.decorators.jinja import jinja_filter -from salt.exceptions import ( - CommandExecutionError, SaltClientError, - CommandNotFoundError, SaltSystemExit, - SaltInvocationError, SaltException -) -log = logging.getLogger(__name__) - - -def get_context(template, line, num_lines=5, marker=None): +# +# DEPRECATED FUNCTIONS +# +# These are not referenced anywhere in the codebase and are slated for removal. +def option(value, default='', opts=None, pillar=None): ''' - Returns debugging context around a line in a given string - - Returns:: string + Pass in a generic option and receive the value that will be assigned ''' - import salt.utils.stringutils - template_lines = template.splitlines() - num_template_lines = len(template_lines) - - # in test, a single line template would return a crazy line number like, - # 357. do this sanity check and if the given line is obviously wrong, just - # return the entire template - if line > num_template_lines: - return template - - context_start = max(0, line - num_lines - 1) # subt 1 for 0-based indexing - context_end = min(num_template_lines, line + num_lines) - error_line_in_context = line - context_start - 1 # subtr 1 for 0-based idx - - buf = [] - if context_start > 0: - buf.append('[...]') - error_line_in_context += 1 - - buf.extend(template_lines[context_start:context_end]) - - if context_end < num_template_lines: - buf.append('[...]') - - if marker: - buf[error_line_in_context] += marker - - return u'---\n{0}\n---'.format(u'\n'.join(buf)) - - -def get_master_key(key_user, opts, skip_perm_errors=False): # Late import to avoid circular import. - import salt.utils.files - import salt.utils.verify - import salt.utils.platform + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.option\' detected. This function has been ' + 'deprecated and will be removed in Salt Neon.' + ) - if key_user == 'root': - if opts.get('user', 'root') != 'root': - key_user = opts.get('user', 'root') - if key_user.startswith('sudo_'): - key_user = opts.get('user', 'root') - if salt.utils.platform.is_windows(): - # The username may contain '\' if it is in Windows - # 'DOMAIN\username' format. Fix this for the keyfile path. - key_user = key_user.replace('\\', '_') - keyfile = os.path.join(opts['cachedir'], - '.{0}_key'.format(key_user)) - # Make sure all key parent directories are accessible - salt.utils.verify.check_path_traversal(opts['cachedir'], - key_user, - skip_perm_errors) - - try: - with salt.utils.files.fopen(keyfile, 'r') as key: - return key.read() - except (OSError, IOError): - # Fall back to eauth - return '' - - -def profile_func(filename=None): - ''' - Decorator for adding profiling to a nested function in Salt - ''' - def proffunc(fun): - def profiled_func(*args, **kwargs): - logging.info('Profiling function {0}'.format(fun.__name__)) - try: - profiler = cProfile.Profile() - retval = profiler.runcall(fun, *args, **kwargs) - profiler.dump_stats((filename or '{0}_func.profile' - .format(fun.__name__))) - except IOError: - logging.exception( - 'Could not open profile file {0}'.format(filename) - ) - - return retval - return profiled_func - return proffunc - - -def activate_profile(test=True): - pr = None - if test: - if HAS_CPROFILE: - pr = cProfile.Profile() - pr.enable() - else: - log.error('cProfile is not available on your platform') - return pr - - -def output_profile(pr, stats_path='/tmp/stats', stop=False, id_=None): - # Late import to avoid circular import. - import salt.utils.files - import salt.utils.hashutils - import salt.utils.path - import salt.utils.stringutils - - if pr is not None and HAS_CPROFILE: - try: - pr.disable() - if not os.path.isdir(stats_path): - os.makedirs(stats_path) - date = datetime.datetime.now().isoformat() - if id_ is None: - id_ = salt.utils.hashutils.random_hash(size=32) - ficp = os.path.join(stats_path, '{0}.{1}.pstats'.format(id_, date)) - fico = os.path.join(stats_path, '{0}.{1}.dot'.format(id_, date)) - ficn = os.path.join(stats_path, '{0}.{1}.stats'.format(id_, date)) - if not os.path.exists(ficp): - pr.dump_stats(ficp) - with salt.utils.files.fopen(ficn, 'w') as fic: - pstats.Stats(pr, stream=fic).sort_stats('cumulative') - log.info('PROFILING: {0} generated'.format(ficp)) - log.info('PROFILING (cumulative): {0} generated'.format(ficn)) - pyprof = salt.utils.path.which('pyprof2calltree') - cmd = [pyprof, '-i', ficp, '-o', fico] - if pyprof: - failed = False - try: - pro = subprocess.Popen( - cmd, shell=False, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except OSError: - failed = True - if pro.returncode: - failed = True - if failed: - log.error('PROFILING (dot problem') - else: - log.info('PROFILING (dot): {0} generated'.format(fico)) - log.trace('pyprof2calltree output:') - log.trace(salt.utils.stringutils.to_str(pro.stdout.read()).strip() + - salt.utils.stringutils.to_str(pro.stderr.read()).strip()) - else: - log.info('You can run {0} for additional stats.'.format(cmd)) - finally: - if not stop: - pr.enable() - return pr + if opts is None: + opts = {} + if pillar is None: + pillar = {} + sources = ( + (opts, value), + (pillar, 'master:{0}'.format(value)), + (pillar, value), + ) + for source, val in sources: + out = salt.utils.data.traverse_dict_and_list(source, val, default) + if out is not default: + return out + return default def required_module_list(docstring=None): @@ -241,7 +57,18 @@ def required_module_list(docstring=None): in stdlib and don't exist on the current pythonpath. ''' # Late import to avoid circular import. + import salt.utils.versions import salt.utils.doc + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.required_module_list\' detected. This function ' + 'has been deprecated and will be removed in Salt Neon.' + ) + + if six.PY3: + import importlib.util # pylint: disable=no-name-in-module,import-error + else: + import imp if not docstring: return [] @@ -264,711 +91,52 @@ def required_modules_error(name, docstring): Pretty print error messages in critical salt modules which are missing deps not always in stdlib such as win32api on windows. ''' + # Late import to avoid circular import. + import salt.utils.versions + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.required_modules_error\' detected. This function ' + 'has been deprecated and will be removed in Salt Neon.' + ) modules = required_module_list(docstring) if not modules: return '' + import os filename = os.path.basename(name).split('.')[0] msg = '\'{0}\' requires these python modules: {1}' return msg.format(filename, ', '.join(modules)) -def get_accumulator_dir(cachedir): - ''' - Return the directory that accumulator data is stored in, creating it if it - doesn't exist. - ''' - fn_ = os.path.join(cachedir, 'accumulator') - if not os.path.isdir(fn_): - # accumulator_dir is not present, create it - os.makedirs(fn_) - return fn_ - - -def check_or_die(command): - ''' - Simple convenience function for modules to use for gracefully blowing up - if a required tool is not available in the system path. - - Lazily import `salt.modules.cmdmod` to avoid any sort of circular - dependencies. - ''' - import salt.utils.path - if command is None: - raise CommandNotFoundError('\'None\' is not a valid command.') - - if not salt.utils.path.which(command): - raise CommandNotFoundError('\'{0}\' is not in the path'.format(command)) - - -def backup_minion(path, bkroot): - ''' - Backup a file on the minion - ''' - import salt.utils.platform - dname, bname = os.path.split(path) - if salt.utils.platform.is_windows(): - src_dir = dname.replace(':', '_') - else: - src_dir = dname[1:] - if not salt.utils.platform.is_windows(): - fstat = os.stat(path) - msecs = str(int(time.time() * 1000000))[-6:] - if salt.utils.platform.is_windows(): - # ':' is an illegal filesystem path character on Windows - stamp = time.strftime('%a_%b_%d_%H-%M-%S_%Y') - else: - stamp = time.strftime('%a_%b_%d_%H:%M:%S_%Y') - stamp = '{0}{1}_{2}'.format(stamp[:-4], msecs, stamp[-4:]) - bkpath = os.path.join(bkroot, - src_dir, - '{0}_{1}'.format(bname, stamp)) - if not os.path.isdir(os.path.dirname(bkpath)): - os.makedirs(os.path.dirname(bkpath)) - shutil.copyfile(path, bkpath) - if not salt.utils.platform.is_windows(): - os.chown(bkpath, fstat.st_uid, fstat.st_gid) - os.chmod(bkpath, fstat.st_mode) - - -def build_whitespace_split_regex(text): - ''' - Create a regular expression at runtime which should match ignoring the - addition or deletion of white space or line breaks, unless between commas - - Example: - - .. code-block:: python - - >>> import re - >>> import salt.utils - >>> regex = salt.utils.build_whitespace_split_regex( - ... """if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then""" - ... ) - - >>> regex - '(?:[\\s]+)?if(?:[\\s]+)?\\[(?:[\\s]+)?\\-z(?:[\\s]+)?\\"\\$debian' - '\\_chroot\\"(?:[\\s]+)?\\](?:[\\s]+)?\\&\\&(?:[\\s]+)?\\[(?:[\\s]+)?' - '\\-r(?:[\\s]+)?\\/etc\\/debian\\_chroot(?:[\\s]+)?\\]\\;(?:[\\s]+)?' - 'then(?:[\\s]+)?' - >>> re.search( - ... regex, - ... """if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then""" - ... ) - - <_sre.SRE_Match object at 0xb70639c0> - >>> - - ''' - def __build_parts(text): - lexer = shlex.shlex(text) - lexer.whitespace_split = True - lexer.commenters = '' - if '\'' in text: - lexer.quotes = '"' - elif '"' in text: - lexer.quotes = '\'' - return list(lexer) - - regex = r'' - for line in text.splitlines(): - parts = [re.escape(s) for s in __build_parts(line)] - regex += r'(?:[\s]+)?{0}(?:[\s]+)?'.format(r'(?:[\s]+)?'.join(parts)) - return r'(?m)^{0}$'.format(regex) - - -def format_call(fun, - data, - initial_ret=None, - expected_extra_kws=(), - is_class_method=None): - ''' - Build the required arguments and keyword arguments required for the passed - function. - - :param fun: The function to get the argspec from - :param data: A dictionary containing the required data to build the - arguments and keyword arguments. - :param initial_ret: The initial return data pre-populated as dictionary or - None - :param expected_extra_kws: Any expected extra keyword argument names which - should not trigger a :ref:`SaltInvocationError` - :param is_class_method: Pass True if you are sure that the function being passed - is a class method. The reason for this is that on Python 3 - ``inspect.ismethod`` only returns ``True`` for bound methods, - while on Python 2, it returns ``True`` for bound and unbound - methods. So, on Python 3, in case of a class method, you'd - need the class to which the function belongs to be instantiated - and this is not always wanted. - :returns: A dictionary with the function required arguments and keyword - arguments. - ''' - # Late import to avoid circular import - import salt.utils.versions - import salt.utils.args - ret = initial_ret is not None and initial_ret or {} - - ret['args'] = [] - ret['kwargs'] = {} - - aspec = salt.utils.args.get_function_argspec(fun, is_class_method=is_class_method) - - arg_data = salt.utils.args.arg_lookup(fun, aspec) - args = arg_data['args'] - kwargs = arg_data['kwargs'] - - # Since we WILL be changing the data dictionary, let's change a copy of it - data = data.copy() - - missing_args = [] - - for key in kwargs: - try: - kwargs[key] = data.pop(key) - except KeyError: - # Let's leave the default value in place - pass - - while args: - arg = args.pop(0) - try: - ret['args'].append(data.pop(arg)) - except KeyError: - missing_args.append(arg) - - if missing_args: - used_args_count = len(ret['args']) + len(args) - args_count = used_args_count + len(missing_args) - raise SaltInvocationError( - '{0} takes at least {1} argument{2} ({3} given)'.format( - fun.__name__, - args_count, - args_count > 1 and 's' or '', - used_args_count - ) - ) - - ret['kwargs'].update(kwargs) - - if aspec.keywords: - # The function accepts **kwargs, any non expected extra keyword - # arguments will made available. - for key, value in six.iteritems(data): - if key in expected_extra_kws: - continue - ret['kwargs'][key] = value - - # No need to check for extra keyword arguments since they are all - # **kwargs now. Return - return ret - - # Did not return yet? Lets gather any remaining and unexpected keyword - # arguments - extra = {} - for key, value in six.iteritems(data): - if key in expected_extra_kws: - continue - extra[key] = copy.deepcopy(value) - - # We'll be showing errors to the users until Salt Oxygen comes out, after - # which, errors will be raised instead. - salt.utils.versions.warn_until( - 'Oxygen', - 'It\'s time to start raising `SaltInvocationError` instead of ' - 'returning warnings', - # Let's not show the deprecation warning on the console, there's no - # need. - _dont_call_warnings=True - ) - - if extra: - # Found unexpected keyword arguments, raise an error to the user - if len(extra) == 1: - msg = '\'{0[0]}\' is an invalid keyword argument for \'{1}\''.format( - list(extra.keys()), - ret.get( - # In case this is being called for a state module - 'full', - # Not a state module, build the name - '{0}.{1}'.format(fun.__module__, fun.__name__) - ) - ) - else: - msg = '{0} and \'{1}\' are invalid keyword arguments for \'{2}\''.format( - ', '.join(['\'{0}\''.format(e) for e in extra][:-1]), - list(extra.keys())[-1], - ret.get( - # In case this is being called for a state module - 'full', - # Not a state module, build the name - '{0}.{1}'.format(fun.__module__, fun.__name__) - ) - ) - - # Return a warning to the user explaining what's going on - ret.setdefault('warnings', []).append( - '{0}. If you were trying to pass additional data to be used ' - 'in a template context, please populate \'context\' with ' - '\'key: value\' pairs. Your approach will work until Salt ' - 'Oxygen is out.{1}'.format( - msg, - '' if 'full' not in ret else ' Please update your state files.' - ) - ) - - # Lets pack the current extra kwargs as template context - ret.setdefault('context', {}).update(extra) - return ret - - -@jinja_filter('mysql_to_dict') -def mysql_to_dict(data, key): - ''' - Convert MySQL-style output to a python dictionary - ''' - import salt.utils.stringutils - ret = {} - headers = [''] - for line in data: - if not line: - continue - if line.startswith('+'): - continue - comps = line.split('|') - for comp in range(len(comps)): - comps[comp] = comps[comp].strip() - if len(headers) > 1: - index = len(headers) - 1 - row = {} - for field in range(index): - if field < 1: - continue - else: - row[headers[field]] = salt.utils.stringutils.to_num(comps[field]) - ret[row[key]] = row - else: - headers = comps - return ret - - -def expr_match(line, expr): - ''' - Evaluate a line of text against an expression. First try a full-string - match, next try globbing, and then try to match assuming expr is a regular - expression. Originally designed to match minion IDs for - whitelists/blacklists. - ''' - if line == expr: - return True - if fnmatch.fnmatch(line, expr): - return True - try: - if re.match(r'\A{0}\Z'.format(expr), line): - return True - except re.error: - pass - return False - - -@jinja_filter('check_whitelist_blacklist') -def check_whitelist_blacklist(value, whitelist=None, blacklist=None): - ''' - Check a whitelist and/or blacklist to see if the value matches it. - - value - The item to check the whitelist and/or blacklist against. - - whitelist - The list of items that are white-listed. If ``value`` is found - in the whitelist, then the function returns ``True``. Otherwise, - it returns ``False``. - - blacklist - The list of items that are black-listed. If ``value`` is found - in the blacklist, then the function returns ``False``. Otherwise, - it returns ``True``. - - If both a whitelist and a blacklist are provided, value membership - in the blacklist will be examined first. If the value is not found - in the blacklist, then the whitelist is checked. If the value isn't - found in the whitelist, the function returns ``False``. - ''' - if blacklist is not None: - if not hasattr(blacklist, '__iter__'): - blacklist = [blacklist] - try: - for expr in blacklist: - if expr_match(value, expr): - return False - except TypeError: - log.error('Non-iterable blacklist {0}'.format(blacklist)) - - if whitelist: - if not hasattr(whitelist, '__iter__'): - whitelist = [whitelist] - try: - for expr in whitelist: - if expr_match(value, expr): - return True - except TypeError: - log.error('Non-iterable whitelist {0}'.format(whitelist)) - else: - return True - - return False - - -def get_values_of_matching_keys(pattern_dict, user_name): - ''' - Check a whitelist and/or blacklist to see if the value matches it. - ''' - ret = [] - for expr in pattern_dict: - if expr_match(user_name, expr): - ret.extend(pattern_dict[expr]) - return ret - - -def sanitize_win_path_string(winpath): - ''' - Remove illegal path characters for windows - ''' - intab = '<>:|?*' - outtab = '_' * len(intab) - trantab = ''.maketrans(intab, outtab) if six.PY3 else string.maketrans(intab, outtab) # pylint: disable=no-member - if isinstance(winpath, six.string_types): - winpath = winpath.translate(trantab) - elif isinstance(winpath, six.text_type): - winpath = winpath.translate(dict((ord(c), u'_') for c in intab)) - return winpath - - -def check_include_exclude(path_str, include_pat=None, exclude_pat=None): - ''' - Check for glob or regexp patterns for include_pat and exclude_pat in the - 'path_str' string and return True/False conditions as follows. - - Default: return 'True' if no include_pat or exclude_pat patterns are - supplied - - If only include_pat or exclude_pat is supplied: return 'True' if string - passes the include_pat test or fails exclude_pat test respectively - - If both include_pat and exclude_pat are supplied: return 'True' if - include_pat matches AND exclude_pat does not match - ''' - ret = True # -- default true - # Before pattern match, check if it is regexp (E@'') or glob(default) - if include_pat: - if re.match('E@', include_pat): - retchk_include = True if re.search( - include_pat[2:], - path_str - ) else False - else: - retchk_include = True if fnmatch.fnmatch( - path_str, - include_pat - ) else False - - if exclude_pat: - if re.match('E@', exclude_pat): - retchk_exclude = False if re.search( - exclude_pat[2:], - path_str - ) else True - else: - retchk_exclude = False if fnmatch.fnmatch( - path_str, - exclude_pat - ) else True - - # Now apply include/exclude conditions - if include_pat and not exclude_pat: - ret = retchk_include - elif exclude_pat and not include_pat: - ret = retchk_exclude - elif include_pat and exclude_pat: - ret = retchk_include and retchk_exclude - else: - ret = True - - return ret - - -def option(value, default='', opts=None, pillar=None): - ''' - Pass in a generic option and receive the value that will be assigned - ''' - if opts is None: - opts = {} - if pillar is None: - pillar = {} - sources = ( - (opts, value), - (pillar, 'master:{0}'.format(value)), - (pillar, value), - ) - for source, val in sources: - out = traverse_dict_and_list(source, val, default) - if out is not default: - return out - return default - - -def print_cli(msg, retries=10, step=0.01): - ''' - Wrapper around print() that suppresses tracebacks on broken pipes (i.e. - when salt output is piped to less and less is stopped prematurely). - ''' - while retries: - try: - try: - print(msg) - except UnicodeEncodeError: - print(msg.encode('utf-8')) - except IOError as exc: - err = "{0}".format(exc) - if exc.errno != errno.EPIPE: - if ( - ("temporarily unavailable" in err or - exc.errno in (errno.EAGAIN,)) and - retries - ): - time.sleep(step) - retries -= 1 - continue - else: - raise - break - - -def namespaced_function(function, global_dict, defaults=None, preserve_context=False): - ''' - Redefine (clone) a function under a different globals() namespace scope - - preserve_context: - Allow keeping the context taken from orignal namespace, - and extend it with globals() taken from - new targetted namespace. - ''' - if defaults is None: - defaults = function.__defaults__ - - if preserve_context: - _global_dict = function.__globals__.copy() - _global_dict.update(global_dict) - global_dict = _global_dict - new_namespaced_function = types.FunctionType( - function.__code__, - global_dict, - name=function.__name__, - argdefs=defaults, - closure=function.__closure__ - ) - new_namespaced_function.__dict__.update(function.__dict__) - return new_namespaced_function - - -def alias_function(fun, name, doc=None): - ''' - Copy a function - ''' - alias_fun = types.FunctionType(fun.__code__, - fun.__globals__, - name, - fun.__defaults__, - fun.__closure__) - alias_fun.__dict__.update(fun.__dict__) - - if doc and isinstance(doc, six.string_types): - alias_fun.__doc__ = doc - else: - orig_name = fun.__name__ - alias_msg = ('\nThis function is an alias of ' - '``{0}``.\n'.format(orig_name)) - alias_fun.__doc__ = alias_msg + fun.__doc__ - - return alias_fun - - -def _win_console_event_handler(event): - if event == 5: - # Do nothing on CTRL_LOGOFF_EVENT - return True - return False - - -def enable_ctrl_logoff_handler(): - if HAS_WIN32API: - win32api.SetConsoleCtrlHandler(_win_console_event_handler, 1) - - -def date_cast(date): - ''' - Casts any object into a datetime.datetime object - - date - any datetime, time string representation... - ''' - if date is None: - return datetime.datetime.now() - elif isinstance(date, datetime.datetime): - return date - - # fuzzy date - try: - if isinstance(date, six.string_types): - try: - if HAS_TIMELIB: - # py3: yes, timelib.strtodatetime wants bytes, not str :/ - return timelib.strtodatetime(to_bytes(date)) - except ValueError: - pass - - # not parsed yet, obviously a timestamp? - if date.isdigit(): - date = int(date) - else: - date = float(date) - - return datetime.datetime.fromtimestamp(date) - except Exception: - if HAS_TIMELIB: - raise ValueError('Unable to parse {0}'.format(date)) - - raise RuntimeError( - 'Unable to parse {0}. Consider installing timelib'.format(date)) - - -@jinja_filter('strftime') -def date_format(date=None, format="%Y-%m-%d"): - ''' - Converts date into a time-based string - - date - any datetime, time string representation... - - format - :ref:`strftime` format - - >>> import datetime - >>> src = datetime.datetime(2002, 12, 25, 12, 00, 00, 00) - >>> date_format(src) - '2002-12-25' - >>> src = '2002/12/25' - >>> date_format(src) - '2002-12-25' - >>> src = 1040814000 - >>> date_format(src) - '2002-12-25' - >>> src = '1040814000' - >>> date_format(src) - '2002-12-25' - ''' - return date_cast(date).strftime(format) - - -def find_json(raw): - ''' - Pass in a raw string and load the json when it starts. This allows for a - string to start with garbage and end with json but be cleanly loaded - ''' - ret = {} - for ind in range(len(raw)): - working = '\n'.join(raw.splitlines()[ind:]) - try: - ret = json.loads(working, object_hook=decode_dict) - except ValueError: - continue - if ret: - return ret - if not ret: - # Not json, raise an error - raise ValueError - - -def total_seconds(td): - ''' - Takes a timedelta and returns the total number of seconds - represented by the object. Wrapper for the total_seconds() - method which does not exist in versions of Python < 2.7. - ''' - return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 - - -def import_json(): - ''' - Import a json module, starting with the quick ones and going down the list) - ''' - for fast_json in ('ujson', 'yajl', 'json'): - try: - mod = __import__(fast_json) - log.trace('loaded {0} json lib'.format(fast_json)) - return mod - except ImportError: - continue - - -def simple_types_filter(data): - ''' - Convert the data list, dictionary into simple types, i.e., int, float, string, - bool, etc. - ''' - if data is None: - return data - - simpletypes_keys = (six.string_types, six.text_type, six.integer_types, float, bool) - simpletypes_values = tuple(list(simpletypes_keys) + [list, tuple]) - - if isinstance(data, (list, tuple)): - simplearray = [] - for value in data: - if value is not None: - if isinstance(value, (dict, list)): - value = simple_types_filter(value) - elif not isinstance(value, simpletypes_values): - value = repr(value) - simplearray.append(value) - return simplearray - - if isinstance(data, dict): - simpledict = {} - for key, value in six.iteritems(data): - if key is not None and not isinstance(key, simpletypes_keys): - key = repr(key) - if value is not None and isinstance(value, (dict, list, tuple)): - value = simple_types_filter(value) - elif value is not None and not isinstance(value, simpletypes_values): - value = repr(value) - simpledict[key] = value - return simpledict - - return data - - -def fnmatch_multiple(candidates, pattern): - ''' - Convenience function which runs fnmatch.fnmatch() on each element of passed - iterable. The first matching candidate is returned, or None if there is no - matching candidate. - ''' - # Make sure that candidates is iterable to avoid a TypeError when we try to - # iterate over its items. - try: - candidates_iter = iter(candidates) - except TypeError: - return None - - for candidate in candidates_iter: - try: - if fnmatch.fnmatch(candidate, pattern): - return candidate - except TypeError: - pass - return None - - # # MOVED FUNCTIONS # # These are deprecated and will be removed in Neon. +def get_accumulator_dir(cachedir): + # Late import to avoid circular import. + import salt.utils.versions + import salt.state + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_accumulator_dir\' detected. This function ' + 'has been moved to \'salt.state.accumulator_dir\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.state.get_accumulator_dir(cachedir) + + +def fnmatch_multiple(candidates, pattern): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.itertools + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.fnmatch_multiple\' detected. This function has been ' + 'moved to \'salt.utils.itertools.fnmatch_multiple\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' + ) + return salt.utils.itertools.fnmatch_multiple(candidates, pattern) + + def appendproctitle(name): # Late import to avoid circular import. import salt.utils.versions @@ -1164,6 +332,76 @@ def contains_whitespace(text): return salt.utils.stringutils.contains_whitespace(text) +def build_whitespace_split_regex(text): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.stringutils + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.build_whitespace_split_regex\' detected. This ' + 'function has been moved to ' + '\'salt.utils.stringutils.build_whitespace_split_regex\' as of Salt ' + 'Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.stringutils.build_whitespace_split_regex(text) + + +def expr_match(line, expr): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.stringutils + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.expr_match\' detected. This function ' + 'has been moved to \'salt.utils.stringutils.expr_match\' as ' + 'of Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.stringutils.expr_match(line, expr) + + +def check_whitelist_blacklist(value, whitelist=None, blacklist=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.stringutils + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.check_whitelist_blacklist\' detected. This ' + 'function has been moved to ' + '\'salt.utils.stringutils.check_whitelist_blacklist\' as of Salt ' + 'Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.stringutils.check_whitelist_blacklist( + value, whitelist, blacklist) + + +def check_include_exclude(path_str, include_pat=None, exclude_pat=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.stringutils + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.check_include_exclude\' detected. This ' + 'function has been moved to ' + '\'salt.utils.stringutils.check_include_exclude\' as of Salt ' + 'Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.stringutils.check_include_exclude( + path_str, include_pat, exclude_pat) + + +def print_cli(msg, retries=10, step=0.01): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.stringutils + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.print_cli\' detected. This function ' + 'has been moved to \'salt.utils.stringutils.print_cli\' as ' + 'of Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.stringutils.print_cli(msg, retries, step) + + def clean_kwargs(**kwargs): # Late import to avoid circular import. import salt.utils.versions @@ -1255,6 +493,21 @@ def test_mode(**kwargs): return salt.utils.args.test_mode(**kwargs) +def format_call(fun, data, initial_ret=None, expected_extra_kws=(), + is_class_method=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.args + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.format_call\' detected. This function has been ' + 'moved to \'salt.utils.args.format_call\' as of Salt Oxygen. This ' + 'warning will be removed in Salt Neon.' + ) + return salt.utils.args.format_call( + fun, data, initial_ret, expected_extra_kws, is_class_method) + + def which(exe=None): # Late import to avoid circular import. import salt.utils.versions @@ -1294,6 +547,32 @@ def path_join(*parts, **kwargs): return salt.utils.path.join(*parts, **kwargs) +def check_or_die(command): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.path + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.check_or_die\' detected. This function has been ' + 'moved to \'salt.utils.path.check_or_die\' as of Salt Oxygen. This ' + 'warning will be removed in Salt Neon.' + ) + return salt.utils.path.check_or_die(command) + + +def sanitize_win_path_string(winpath): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.path + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.sanitize_win_path_string\' detected. This ' + 'function has been moved to \'salt.utils.path.sanitize_win_path\' as ' + 'of Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.path.sanitize_win_path(winpath) + + def rand_str(size=9999999999, hash_type=None): # Late import to avoid circular import. import salt.utils.versions @@ -1489,7 +768,6 @@ def safe_rm(tgt): return salt.utils.files.safe_rm(tgt) -@jinja_filter('is_empty') def is_empty(filename): # Late import to avoid circular import. import salt.utils.versions @@ -1516,7 +794,6 @@ def fopen(*args, **kwargs): return salt.utils.files.fopen(*args, **kwargs) # pylint: disable=W8470 -@contextlib.contextmanager def flopen(*args, **kwargs): # Late import to avoid circular import. import salt.utils.versions @@ -1530,7 +807,6 @@ def flopen(*args, **kwargs): return salt.utils.files.flopen(*args, **kwargs) -@contextlib.contextmanager def fpopen(*args, **kwargs): # Late import to avoid circular import. import salt.utils.versions @@ -1668,6 +944,20 @@ def human_size_to_bytes(human_size): return salt.utils.files.human_size_to_bytes(human_size) +def backup_minion(path, bkroot): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.files + + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.backup_minion\' detected. This function has ' + 'been moved to \'salt.utils.files.backup_minion\' as of Salt ' + 'Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.files.backup_minion(path, bkroot) + + def str_version_to_evr(verstring): # Late import to avoid circular import. import salt.utils.versions @@ -1983,6 +1273,20 @@ def get_gid(group=None): return salt.utils.user.get_gid(group) +def enable_ctrl_logoff_handler(): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.win_functions + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.enable_ctrl_logoff_handler\' detected. This ' + 'function has been moved to ' + '\'salt.utils.win_functions.enable_ctrl_logoff_handler\' as of Salt ' + 'Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.win_functions.enable_ctrl_logoff_handler() + + def traverse_dict(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): # Late import to avoid circular import. import salt.utils.versions @@ -2220,6 +1524,32 @@ def is_true(value=None): return salt.utils.data.is_true(value) +def mysql_to_dict(data, key): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.mysql_to_dict\' detected. This function ' + 'has been moved to \'salt.utils.data.mysql_to_dict\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.mysql_to_dict(data, key) + + +def simple_types_filter(data): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.data + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.simple_types_filter\' detected. This function ' + 'has been moved to \'salt.utils.data.simple_types_filter\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.data.simple_types_filter(data) + + def ip_bracket(addr): # Late import to avoid circular import. import salt.utils.versions @@ -2283,3 +1613,174 @@ def dns_check(addr, port, safe=False, ipv6=None): 'Salt Oxygen. This warning will be removed in Salt Neon.' ) return salt.utils.network.dns_check(addr, port, safe, ipv6) + + +def get_context(template, line, num_lines=5, marker=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.templates + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_context\' detected. This function ' + 'has been moved to \'salt.utils.templates.get_context\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.templates.get_context(template, line, num_lines, marker) + + +def get_master_key(key_user, opts, skip_perm_errors=False): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.master + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_master_key\' detected. This function ' + 'has been moved to \'salt.utils.master.get_master_key\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.master.get_master_key(key_user, opts, skip_perm_errors) + + +def get_values_of_matching_keys(pattern_dict, user_name): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.master + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.get_values_of_matching_keys\' detected. ' + 'This function has been moved to \'salt.utils.master.get_master_key\' ' + 'as of Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.master.get_values_of_matching_keys(pattern_dict, user_name) + + +def date_cast(date): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.dateutils + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.date_cast\' detected. This function ' + 'has been moved to \'salt.utils.dateutils.date_cast\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.dateutils.date_cast(date) + + +def date_format(date=None, format="%Y-%m-%d"): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.dateutils + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.date_format\' detected. This function ' + 'has been moved to \'salt.utils.dateutils.strftime\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.dateutils.strftime(date, format) + + +def total_seconds(td): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.dateutils + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.total_seconds\' detected. This function ' + 'has been moved to \'salt.utils.dateutils.total_seconds\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.dateutils.total_seconds(td) + + +def find_json(raw): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.json + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.find_json\' detected. This function ' + 'has been moved to \'salt.utils.json.find_json\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.json.find_json(raw) + + +def import_json(): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.json + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.import_json\' detected. This function ' + 'has been moved to \'salt.utils.json.import_json\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.json.import_json() + + +def namespaced_function(function, global_dict, defaults=None, + preserve_context=False): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.functools + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.namespaced_function\' detected. This function ' + 'has been moved to \'salt.utils.functools.namespaced_function\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.functools.namespaced_function( + function, global_dict, defaults, preserve_context) + + +def alias_function(fun, name, doc=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.functools + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.alias_function\' detected. This function ' + 'has been moved to \'salt.utils.functools.alias_function\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.functools.alias_function(fun, name, doc) + + +def profile_func(filename=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.profile + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.profile_func\' detected. This function ' + 'has been moved to \'salt.utils.profile.profile_func\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.profile.profile_func(filename) + + +def activate_profile(test=True): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.profile + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.activate_profile\' detected. This function ' + 'has been moved to \'salt.utils.profile.activate_profile\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.profile.activate_profile(test) + + +def output_profile(pr, stats_path='/tmp/stats', stop=False, id_=None): + # Late import to avoid circular import. + import salt.utils.versions + import salt.utils.profile + salt.utils.versions.warn_until( + 'Neon', + 'Use of \'salt.utils.output_profile\' detected. This function ' + 'has been moved to \'salt.utils.profile.output_profile\' as of ' + 'Salt Oxygen. This warning will be removed in Salt Neon.' + ) + return salt.utils.profile.output_profile(pr, stats_path, stop, id_) diff --git a/salt/utils/args.py b/salt/utils/args.py index 484359114f..b5fcc6f588 100644 --- a/salt/utils/args.py +++ b/salt/utils/args.py @@ -5,6 +5,7 @@ Functions used for CLI argument handling # Import python libs from __future__ import absolute_import +import copy import fnmatch import inspect import re @@ -16,6 +17,7 @@ from salt.ext import six from salt.ext.six.moves import zip # pylint: disable=import-error,redefined-builtin import salt.utils.data import salt.utils.jid +import salt.utils.versions if six.PY3: @@ -357,3 +359,144 @@ def test_mode(**kwargs): except AttributeError: continue return False + + +def format_call(fun, + data, + initial_ret=None, + expected_extra_kws=(), + is_class_method=None): + ''' + Build the required arguments and keyword arguments required for the passed + function. + + :param fun: The function to get the argspec from + :param data: A dictionary containing the required data to build the + arguments and keyword arguments. + :param initial_ret: The initial return data pre-populated as dictionary or + None + :param expected_extra_kws: Any expected extra keyword argument names which + should not trigger a :ref:`SaltInvocationError` + :param is_class_method: Pass True if you are sure that the function being passed + is a class method. The reason for this is that on Python 3 + ``inspect.ismethod`` only returns ``True`` for bound methods, + while on Python 2, it returns ``True`` for bound and unbound + methods. So, on Python 3, in case of a class method, you'd + need the class to which the function belongs to be instantiated + and this is not always wanted. + :returns: A dictionary with the function required arguments and keyword + arguments. + ''' + ret = initial_ret is not None and initial_ret or {} + + ret['args'] = [] + ret['kwargs'] = {} + + aspec = get_function_argspec(fun, is_class_method=is_class_method) + + arg_data = arg_lookup(fun, aspec) + args = arg_data['args'] + kwargs = arg_data['kwargs'] + + # Since we WILL be changing the data dictionary, let's change a copy of it + data = data.copy() + + missing_args = [] + + for key in kwargs: + try: + kwargs[key] = data.pop(key) + except KeyError: + # Let's leave the default value in place + pass + + while args: + arg = args.pop(0) + try: + ret['args'].append(data.pop(arg)) + except KeyError: + missing_args.append(arg) + + if missing_args: + used_args_count = len(ret['args']) + len(args) + args_count = used_args_count + len(missing_args) + raise SaltInvocationError( + '{0} takes at least {1} argument{2} ({3} given)'.format( + fun.__name__, + args_count, + args_count > 1 and 's' or '', + used_args_count + ) + ) + + ret['kwargs'].update(kwargs) + + if aspec.keywords: + # The function accepts **kwargs, any non expected extra keyword + # arguments will made available. + for key, value in six.iteritems(data): + if key in expected_extra_kws: + continue + ret['kwargs'][key] = value + + # No need to check for extra keyword arguments since they are all + # **kwargs now. Return + return ret + + # Did not return yet? Lets gather any remaining and unexpected keyword + # arguments + extra = {} + for key, value in six.iteritems(data): + if key in expected_extra_kws: + continue + extra[key] = copy.deepcopy(value) + + # We'll be showing errors to the users until Salt Oxygen comes out, after + # which, errors will be raised instead. + salt.utils.versions.warn_until( + 'Oxygen', + 'It\'s time to start raising `SaltInvocationError` instead of ' + 'returning warnings', + # Let's not show the deprecation warning on the console, there's no + # need. + _dont_call_warnings=True + ) + + if extra: + # Found unexpected keyword arguments, raise an error to the user + if len(extra) == 1: + msg = '\'{0[0]}\' is an invalid keyword argument for \'{1}\''.format( + list(extra.keys()), + ret.get( + # In case this is being called for a state module + 'full', + # Not a state module, build the name + '{0}.{1}'.format(fun.__module__, fun.__name__) + ) + ) + else: + msg = '{0} and \'{1}\' are invalid keyword arguments for \'{2}\''.format( + ', '.join(['\'{0}\''.format(e) for e in extra][:-1]), + list(extra.keys())[-1], + ret.get( + # In case this is being called for a state module + 'full', + # Not a state module, build the name + '{0}.{1}'.format(fun.__module__, fun.__name__) + ) + ) + + # Return a warning to the user explaining what's going on + ret.setdefault('warnings', []).append( + '{0}. If you were trying to pass additional data to be used ' + 'in a template context, please populate \'context\' with ' + '\'key: value\' pairs. Your approach will work until Salt ' + 'Oxygen is out.{1}'.format( + msg, + '' if 'full' not in ret else ' Please update your state files.' + ) + ) + + # Lets pack the current extra kwargs as template context + ret.setdefault('context', {}).update(extra) + return ret diff --git a/salt/utils/boto.py b/salt/utils/boto.py index ba2c43a3b4..88a91b07f5 100644 --- a/salt/utils/boto.py +++ b/salt/utils/boto.py @@ -47,7 +47,6 @@ from salt.ext import six from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin from salt.exceptions import SaltInvocationError from salt.utils.versions import LooseVersion as _LooseVersion -import salt.utils import salt.utils.stringutils # Import third party libs diff --git a/salt/utils/boto3.py b/salt/utils/boto3.py index 1f2e025c4c..e3c7f0985d 100644 --- a/salt/utils/boto3.py +++ b/salt/utils/boto3.py @@ -46,7 +46,6 @@ from salt.ext.six.moves import range # pylint: disable=import-error,redefined-b from salt.exceptions import SaltInvocationError from salt.utils.versions import LooseVersion as _LooseVersion from salt.ext import six -import salt.utils import salt.utils.stringutils # Import third party libs diff --git a/salt/utils/cloud.py b/salt/utils/cloud.py index 9fd3aee822..aab4fe4ac1 100644 --- a/salt/utils/cloud.py +++ b/salt/utils/cloud.py @@ -51,6 +51,7 @@ import salt.utils.crypt import salt.utils.event import salt.utils.files import salt.utils.platform +import salt.utils.versions import salt.utils.vt from salt.utils.nb_popen import NonBlockingPopen from salt.utils.yamldumper import SafeOrderedDumper @@ -2808,7 +2809,7 @@ def cache_nodes_ip(opts, base=None): Retrieve a list of all nodes from Salt Cloud cache, and any associated IP addresses. Returns a dict. ''' - salt.utils.warn_until( + salt.utils.versions.warn_until( 'Flourine', 'This function is incomplete and non-functional ' 'and will be removed in Salt Flourine.' diff --git a/salt/utils/data.py b/salt/utils/data.py index 286095b642..4dac969e2a 100644 --- a/salt/utils/data.py +++ b/salt/utils/data.py @@ -12,6 +12,7 @@ import yaml # Import Salt libs import salt.utils.dictupdate +import salt.utils.stringutils from salt.defaults import DEFAULT_TARGET_DELIM from salt.exceptions import SaltException from salt.utils.decorators.jinja import jinja_filter @@ -116,22 +117,6 @@ def exactly_one(l): return exactly_n(l) -def traverse_dict(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): - ''' - Traverse a dict using a colon-delimited (or otherwise delimited, using the - 'delimiter' param) target string. The target 'foo:bar:baz' will return - data['foo']['bar']['baz'] if this value exists, and will otherwise return - the dict in the default argument. - ''' - try: - for each in key.split(delimiter): - data = data[each] - except (KeyError, IndexError, TypeError): - # Encountered a non-indexable value in the middle of traversing - return default - return data - - def filter_by(lookup_dict, lookup, traverse, @@ -185,6 +170,22 @@ def filter_by(lookup_dict, return ret +def traverse_dict(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): + ''' + Traverse a dict using a colon-delimited (or otherwise delimited, using the + 'delimiter' param) target string. The target 'foo:bar:baz' will return + data['foo']['bar']['baz'] if this value exists, and will otherwise return + the dict in the default argument. + ''' + try: + for each in key.split(delimiter): + data = data[each] + except (KeyError, IndexError, TypeError): + # Encountered a non-indexable value in the middle of traversing + return default + return data + + def traverse_dict_and_list(data, key, default=None, delimiter=DEFAULT_TARGET_DELIM): ''' Traverse a dict or list using a colon-delimited (or otherwise delimited, @@ -495,3 +496,69 @@ def is_true(value=None): return str(value).lower() == 'true' else: return bool(value) + + +@jinja_filter('mysql_to_dict') +def mysql_to_dict(data, key): + ''' + Convert MySQL-style output to a python dictionary + ''' + ret = {} + headers = [''] + for line in data: + if not line: + continue + if line.startswith('+'): + continue + comps = line.split('|') + for comp in range(len(comps)): + comps[comp] = comps[comp].strip() + if len(headers) > 1: + index = len(headers) - 1 + row = {} + for field in range(index): + if field < 1: + continue + else: + row[headers[field]] = salt.utils.stringutils.to_num(comps[field]) + ret[row[key]] = row + else: + headers = comps + return ret + + +def simple_types_filter(data): + ''' + Convert the data list, dictionary into simple types, i.e., int, float, string, + bool, etc. + ''' + if data is None: + return data + + simpletypes_keys = (six.string_types, six.text_type, six.integer_types, float, bool) + simpletypes_values = tuple(list(simpletypes_keys) + [list, tuple]) + + if isinstance(data, (list, tuple)): + simplearray = [] + for value in data: + if value is not None: + if isinstance(value, (dict, list)): + value = simple_types_filter(value) + elif not isinstance(value, simpletypes_values): + value = repr(value) + simplearray.append(value) + return simplearray + + if isinstance(data, dict): + simpledict = {} + for key, value in six.iteritems(data): + if key is not None and not isinstance(key, simpletypes_keys): + key = repr(key) + if value is not None and isinstance(value, (dict, list, tuple)): + value = simple_types_filter(value) + elif value is not None and not isinstance(value, simpletypes_values): + value = repr(value) + simpledict[key] = value + return simpledict + + return data diff --git a/salt/utils/dateutils.py b/salt/utils/dateutils.py new file mode 100644 index 0000000000..f5fd9f44cf --- /dev/null +++ b/salt/utils/dateutils.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division + +# Import Python libs +import datetime + +# Import Salt libs +import salt.utils.stringutils +from salt.utils.decorators.jinja import jinja_filter + +# Import 3rd-party libs +from salt.ext import six + +try: + import timelib + HAS_TIMELIB = True +except ImportError: + HAS_TIMELIB = False + + +def date_cast(date): + ''' + Casts any object into a datetime.datetime object + + date + any datetime, time string representation... + ''' + if date is None: + return datetime.datetime.now() + elif isinstance(date, datetime.datetime): + return date + + # fuzzy date + try: + if isinstance(date, six.string_types): + try: + if HAS_TIMELIB: + # py3: yes, timelib.strtodatetime wants bytes, not str :/ + return timelib.strtodatetime( + salt.utils.stringutils.to_bytes(date)) + except ValueError: + pass + + # not parsed yet, obviously a timestamp? + if date.isdigit(): + date = int(date) + else: + date = float(date) + + return datetime.datetime.fromtimestamp(date) + except Exception: + if HAS_TIMELIB: + raise ValueError('Unable to parse {0}'.format(date)) + + raise RuntimeError( + 'Unable to parse {0}. Consider installing timelib'.format(date)) + + +@jinja_filter('strftime') +def strftime(date=None, format="%Y-%m-%d"): + ''' + Converts date into a time-based string + + date + any datetime, time string representation... + + format + :ref:`strftime` format + + >>> import datetime + >>> src = datetime.datetime(2002, 12, 25, 12, 00, 00, 00) + >>> strftime(src) + '2002-12-25' + >>> src = '2002/12/25' + >>> strftime(src) + '2002-12-25' + >>> src = 1040814000 + >>> strftime(src) + '2002-12-25' + >>> src = '1040814000' + >>> strftime(src) + '2002-12-25' + ''' + return date_cast(date).strftime(format) + + +def total_seconds(td): + ''' + Takes a timedelta and returns the total number of seconds + represented by the object. Wrapper for the total_seconds() + method which does not exist in versions of Python < 2.7. + ''' + return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 diff --git a/salt/utils/files.py b/salt/utils/files.py index 9a4a14eebd..d9e76218cf 100644 --- a/salt/utils/files.py +++ b/salt/utils/files.py @@ -16,7 +16,6 @@ import time import urllib # Import Salt libs -import salt.utils # TODO: Remove this when backup_minion is moved import salt.utils.path import salt.utils.platform import salt.utils.stringutils @@ -125,7 +124,7 @@ def copyfile(source, dest, backup_mode='', cachedir=''): bkroot = os.path.join(cachedir, 'file_backup') if backup_mode == 'minion' or backup_mode == 'both' and bkroot: if os.path.exists(dest): - salt.utils.backup_minion(dest, bkroot) + backup_minion(dest, bkroot) if backup_mode == 'master' or backup_mode == 'both' and bkroot: # TODO, backup to master pass @@ -705,3 +704,32 @@ def human_size_to_bytes(human_size): size_num = int(match.group(1)) unit_multiplier = 1024 ** size_exp_map.get(match.group(2), 0) return size_num * unit_multiplier + + +def backup_minion(path, bkroot): + ''' + Backup a file on the minion + ''' + dname, bname = os.path.split(path) + if salt.utils.platform.is_windows(): + src_dir = dname.replace(':', '_') + else: + src_dir = dname[1:] + if not salt.utils.platform.is_windows(): + fstat = os.stat(path) + msecs = str(int(time.time() * 1000000))[-6:] + if salt.utils.platform.is_windows(): + # ':' is an illegal filesystem path character on Windows + stamp = time.strftime('%a_%b_%d_%H-%M-%S_%Y') + else: + stamp = time.strftime('%a_%b_%d_%H:%M:%S_%Y') + stamp = '{0}{1}_{2}'.format(stamp[:-4], msecs, stamp[-4:]) + bkpath = os.path.join(bkroot, + src_dir, + '{0}_{1}'.format(bname, stamp)) + if not os.path.isdir(os.path.dirname(bkpath)): + os.makedirs(os.path.dirname(bkpath)) + shutil.copyfile(path, bkpath) + if not salt.utils.platform.is_windows(): + os.chown(bkpath, fstat.st_uid, fstat.st_gid) + os.chmod(bkpath, fstat.st_mode) diff --git a/salt/utils/functools.py b/salt/utils/functools.py new file mode 100644 index 0000000000..4dbada06f2 --- /dev/null +++ b/salt/utils/functools.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Import Python libs +import types + +# Import 3rd-party libs +from salt.ext import six + + +def namespaced_function(function, global_dict, defaults=None, preserve_context=False): + ''' + Redefine (clone) a function under a different globals() namespace scope + + preserve_context: + Allow keeping the context taken from orignal namespace, + and extend it with globals() taken from + new targetted namespace. + ''' + if defaults is None: + defaults = function.__defaults__ + + if preserve_context: + _global_dict = function.__globals__.copy() + _global_dict.update(global_dict) + global_dict = _global_dict + new_namespaced_function = types.FunctionType( + function.__code__, + global_dict, + name=function.__name__, + argdefs=defaults, + closure=function.__closure__ + ) + new_namespaced_function.__dict__.update(function.__dict__) + return new_namespaced_function + + +def alias_function(fun, name, doc=None): + ''' + Copy a function + ''' + alias_fun = types.FunctionType(fun.__code__, + fun.__globals__, + name, + fun.__defaults__, + fun.__closure__) + alias_fun.__dict__.update(fun.__dict__) + + if doc and isinstance(doc, six.string_types): + alias_fun.__doc__ = doc + else: + orig_name = fun.__name__ + alias_msg = ('\nThis function is an alias of ' + '``{0}``.\n'.format(orig_name)) + alias_fun.__doc__ = alias_msg + fun.__doc__ + + return alias_fun diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py index ae028463a0..ad1154d1e5 100644 --- a/salt/utils/gitfs.py +++ b/salt/utils/gitfs.py @@ -20,7 +20,6 @@ import time from datetime import datetime # Import salt libs -import salt.utils # TODO: Remove this once check_whitelist_blacklist is moved import salt.utils.configparser import salt.utils.data import salt.utils.files @@ -869,7 +868,7 @@ class GitProvider(object): Check if an environment is exposed by comparing it against a whitelist and blacklist. ''' - return salt.utils.check_whitelist_blacklist( + return salt.utils.stringutils.check_whitelist_blacklist( tgt_env, whitelist=self.saltenv_whitelist, blacklist=self.saltenv_blacklist, @@ -2622,7 +2621,7 @@ class GitFS(GitBase): with salt.utils.files.fopen(fpath, 'rb') as fp_: fp_.seek(load['loc']) data = fp_.read(self.opts['file_buffer_size']) - if data and six.PY3 and not salt.utils.is_bin_file(fpath): + if data and six.PY3 and not salt.utils.files.is_binary(fpath): data = data.decode(__salt_system_encoding__) if gzip and data: data = salt.utils.gzip_util.compress(data, gzip) diff --git a/salt/utils/itertools.py b/salt/utils/itertools.py index 7048b57821..570bbaa83e 100644 --- a/salt/utils/itertools.py +++ b/salt/utils/itertools.py @@ -5,6 +5,7 @@ Helpful generators and other tools # Import python libs from __future__ import absolute_import +import fnmatch import re # Import Salt libs @@ -63,3 +64,25 @@ def read_file(fh_, chunk_size=1048576): fh_.close() except AttributeError: pass + + +def fnmatch_multiple(candidates, pattern): + ''' + Convenience function which runs fnmatch.fnmatch() on each element of passed + iterable. The first matching candidate is returned, or None if there is no + matching candidate. + ''' + # Make sure that candidates is iterable to avoid a TypeError when we try to + # iterate over its items. + try: + candidates_iter = iter(candidates) + except TypeError: + return None + + for candidate in candidates_iter: + try: + if fnmatch.fnmatch(candidate, pattern): + return candidate + except TypeError: + pass + return None diff --git a/salt/utils/json.py b/salt/utils/json.py new file mode 100644 index 0000000000..ab05bc6a86 --- /dev/null +++ b/salt/utils/json.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Import Python libs +import json +import logging + +# Import Salt libs +import salt.utils.data + +log = logging.getLogger(__name__) + + +def find_json(raw): + ''' + Pass in a raw string and load the json when it starts. This allows for a + string to start with garbage and end with json but be cleanly loaded + ''' + ret = {} + for ind, _ in enumerate(raw): + working = '\n'.join(raw.splitlines()[ind:]) + try: + ret = json.loads(working, object_hook=salt.utils.data.decode_dict) + except ValueError: + continue + if ret: + return ret + if not ret: + # Not json, raise an error + raise ValueError + + +def import_json(): + ''' + Import a json module, starting with the quick ones and going down the list) + ''' + for fast_json in ('ujson', 'yajl', 'json'): + try: + mod = __import__(fast_json) + log.trace('loaded %s json lib', fast_json) + return mod + except ImportError: + continue diff --git a/salt/utils/mac_utils.py b/salt/utils/mac_utils.py index baf6ca16c0..552ff4e59c 100644 --- a/salt/utils/mac_utils.py +++ b/salt/utils/mac_utils.py @@ -12,7 +12,6 @@ import os import time # Import Salt Libs -import salt.utils import salt.utils.args import salt.utils.platform import salt.utils.stringutils diff --git a/salt/utils/master.py b/salt/utils/master.py index b57a81f93a..389ae47aa3 100644 --- a/salt/utils/master.py +++ b/salt/utils/master.py @@ -19,9 +19,11 @@ import salt.log import salt.cache import salt.client import salt.pillar -import salt.utils import salt.utils.atomicfile +import salt.utils.files import salt.utils.minions +import salt.utils.platform +import salt.utils.stringutils import salt.utils.verify import salt.utils.versions import salt.payload @@ -687,6 +689,42 @@ def ping_all_connected_minions(opts): form = 'glob' client.cmd(tgt, 'test.ping', tgt_type=form) + +def get_master_key(key_user, opts, skip_perm_errors=False): + if key_user == 'root': + if opts.get('user', 'root') != 'root': + key_user = opts.get('user', 'root') + if key_user.startswith('sudo_'): + key_user = opts.get('user', 'root') + if salt.utils.platform.is_windows(): + # The username may contain '\' if it is in Windows + # 'DOMAIN\username' format. Fix this for the keyfile path. + key_user = key_user.replace('\\', '_') + keyfile = os.path.join(opts['cachedir'], + '.{0}_key'.format(key_user)) + # Make sure all key parent directories are accessible + salt.utils.verify.check_path_traversal(opts['cachedir'], + key_user, + skip_perm_errors) + + try: + with salt.utils.files.fopen(keyfile, 'r') as key: + return key.read() + except (OSError, IOError): + # Fall back to eauth + return '' + + +def get_values_of_matching_keys(pattern_dict, user_name): + ''' + Check a whitelist and/or blacklist to see if the value matches it. + ''' + ret = [] + for expr in pattern_dict: + if salt.utils.stringutils.expr_match(user_name, expr): + ret.extend(pattern_dict[expr]) + return ret + # test code for the ConCache class if __name__ == '__main__': diff --git a/salt/utils/minions.py b/salt/utils/minions.py index 41c6b11cef..81060325d2 100644 --- a/salt/utils/minions.py +++ b/salt/utils/minions.py @@ -13,10 +13,10 @@ import logging # Import salt libs import salt.payload -import salt.utils # TODO: Remove this once expr_match is moved import salt.utils.data import salt.utils.files import salt.utils.network +import salt.utils.stringutils import salt.utils.versions from salt.defaults import DEFAULT_TARGET_DELIM from salt.exceptions import CommandExecutionError, SaltCacheError @@ -985,7 +985,7 @@ class CkMinions(object): if match.rstrip('%') in groups: auth_list.extend(auth_provider[match]) else: - if salt.utils.expr_match(match, name): + if salt.utils.stringutils.expr_match(match, name): name_matched = True auth_list.extend(auth_provider[match]) if not permissive and not name_matched and '*' in auth_provider: diff --git a/salt/utils/parsers.py b/salt/utils/parsers.py index 6be34f8c0d..6245d2cdde 100644 --- a/salt/utils/parsers.py +++ b/salt/utils/parsers.py @@ -30,13 +30,13 @@ import salt.defaults.exitcodes import salt.log.setup as log import salt.syspaths as syspaths import salt.version as version -import salt.utils import salt.utils.args import salt.utils.files import salt.utils.jid import salt.utils.kinds as kinds import salt.utils.platform import salt.utils.process +import salt.utils.stringutils import salt.utils.user import salt.utils.xdg from salt.defaults import DEFAULT_TARGET_DELIM @@ -779,7 +779,8 @@ class LogLevelMixIn(six.with_metaclass(MixInMeta, object)): # Yep, not the same user! # Is the current user in ACL? acl = self.config['publisher_acl'] - if salt.utils.check_whitelist_blacklist(current_user, whitelist=six.iterkeys(acl)): + if salt.utils.stringutils.check_whitelist_blacklist( + current_user, whitelist=six.iterkeys(acl)): # Yep, the user is in ACL! # Let's write the logfile to its home directory instead. xdg_dir = salt.utils.xdg.xdg_config_dir() diff --git a/salt/utils/path.py b/salt/utils/path.py index a72e43522f..28ece6b870 100644 --- a/salt/utils/path.py +++ b/salt/utils/path.py @@ -12,6 +12,7 @@ import logging import os import posixpath import re +import string import struct import sys @@ -19,6 +20,7 @@ import sys import salt.utils.args import salt.utils.platform import salt.utils.stringutils +from salt.exceptions import CommandNotFoundError from salt.utils.decorators import memoize as real_memoize from salt.utils.decorators.jinja import jinja_filter @@ -313,3 +315,32 @@ def join(*parts, **kwargs): ret = pathlib.join(root.decode('UTF-8'), *[x.decode('UTF-8') for x in stripped]) return pathlib.normpath(ret) + + +def check_or_die(command): + ''' + Simple convenience function for modules to use for gracefully blowing up + if a required tool is not available in the system path. + + Lazily import `salt.modules.cmdmod` to avoid any sort of circular + dependencies. + ''' + if command is None: + raise CommandNotFoundError('\'None\' is not a valid command.') + + if not which(command): + raise CommandNotFoundError('\'{0}\' is not in the path'.format(command)) + + +def sanitize_win_path(winpath): + ''' + Remove illegal path characters for windows + ''' + intab = '<>:|?*' + outtab = '_' * len(intab) + trantab = ''.maketrans(intab, outtab) if six.PY3 else string.maketrans(intab, outtab) # pylint: disable=no-member + if isinstance(winpath, six.string_types): + winpath = winpath.translate(trantab) + elif isinstance(winpath, six.text_type): + winpath = winpath.translate(dict((ord(c), u'_') for c in intab)) + return winpath diff --git a/salt/utils/profile.py b/salt/utils/profile.py new file mode 100644 index 0000000000..ce8267ca37 --- /dev/null +++ b/salt/utils/profile.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Import Python libs +import datetime +import logging +import os +import pstats +import subprocess + +# Import Salt libs +import salt.utils.files +import salt.utils.hashutils +import salt.utils.path +import salt.utils.stringutils + +log = logging.getLogger(__name__) + +try: + import cProfile + HAS_CPROFILE = True +except ImportError: + HAS_CPROFILE = False + + +def profile_func(filename=None): + ''' + Decorator for adding profiling to a nested function in Salt + ''' + def proffunc(fun): + def profiled_func(*args, **kwargs): + logging.info('Profiling function %s', fun.__name__) + try: + profiler = cProfile.Profile() + retval = profiler.runcall(fun, *args, **kwargs) + profiler.dump_stats((filename or '{0}_func.profile' + .format(fun.__name__))) + except IOError: + logging.exception('Could not open profile file %s', filename) + + return retval + return profiled_func + return proffunc + + +def activate_profile(test=True): + pr = None + if test: + if HAS_CPROFILE: + pr = cProfile.Profile() + pr.enable() + else: + log.error('cProfile is not available on your platform') + return pr + + +def output_profile(pr, stats_path='/tmp/stats', stop=False, id_=None): + if pr is not None and HAS_CPROFILE: + try: + pr.disable() + if not os.path.isdir(stats_path): + os.makedirs(stats_path) + date = datetime.datetime.now().isoformat() + if id_ is None: + id_ = salt.utils.hashutils.random_hash(size=32) + ficp = os.path.join(stats_path, '{0}.{1}.pstats'.format(id_, date)) + fico = os.path.join(stats_path, '{0}.{1}.dot'.format(id_, date)) + ficn = os.path.join(stats_path, '{0}.{1}.stats'.format(id_, date)) + if not os.path.exists(ficp): + pr.dump_stats(ficp) + with salt.utils.files.fopen(ficn, 'w') as fic: + pstats.Stats(pr, stream=fic).sort_stats('cumulative') + log.info('PROFILING: %s generated', ficp) + log.info('PROFILING (cumulative): %s generated', ficn) + pyprof = salt.utils.path.which('pyprof2calltree') + cmd = [pyprof, '-i', ficp, '-o', fico] + if pyprof: + failed = False + try: + pro = subprocess.Popen( + cmd, shell=False, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError: + failed = True + if pro.returncode: + failed = True + if failed: + log.error('PROFILING (dot problem') + else: + log.info('PROFILING (dot): %s generated', fico) + log.trace('pyprof2calltree output:') + log.trace(salt.utils.stringutils.to_str(pro.stdout.read()).strip() + + salt.utils.stringutils.to_str(pro.stderr.read()).strip()) + else: + log.info('You can run %s for additional stats.', cmd) + finally: + if not stop: + pr.enable() + return pr diff --git a/salt/utils/pycrypto.py b/salt/utils/pycrypto.py index f2220b368a..fa2e84af44 100644 --- a/salt/utils/pycrypto.py +++ b/salt/utils/pycrypto.py @@ -1,4 +1,3 @@ - # -*- coding: utf-8 -*- ''' Use pycrypto to generate random passwords on the fly. @@ -28,7 +27,6 @@ except ImportError: HAS_CRYPT = False # Import salt libs -import salt.utils import salt.utils.stringutils from salt.exceptions import SaltInvocationError diff --git a/salt/utils/reactor.py b/salt/utils/reactor.py index 5bc60b8bd4..e5021ec6fe 100644 --- a/salt/utils/reactor.py +++ b/salt/utils/reactor.py @@ -10,7 +10,7 @@ import logging import salt.client import salt.runner import salt.state -import salt.utils +import salt.utils.args import salt.utils.cache import salt.utils.data import salt.utils.event @@ -338,7 +338,7 @@ class ReactWrap(object): ) try: - wrap_call = salt.utils.format_call(l_fun, low) + wrap_call = salt.utils.args.format_call(l_fun, low) args = wrap_call.get('args', ()) kwargs = wrap_call.get('kwargs', {}) # TODO: Setting user doesn't seem to work for actual remote pubs @@ -403,7 +403,7 @@ class ReactWrap(object): ) return - react_call = salt.utils.format_call( + react_call = salt.utils.args.format_call( react_fun, low, expected_extra_kws=REACTOR_INTERNAL_KEYWORDS diff --git a/salt/utils/shlex.py b/salt/utils/shlex.py deleted file mode 100644 index 5b45174d83..0000000000 --- a/salt/utils/shlex.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -''' -Modified versions of functions from shlex module -''' -from __future__ import absolute_import - -# Import Python libs -import shlex - -# Import 3rd-party libs -from salt.ext import six - - -def split(s, **kwargs): - ''' - Only split if variable is a string - ''' - if isinstance(s, six.string_types): - return shlex.split(s, **kwargs) - else: - return s diff --git a/salt/utils/stringutils.py b/salt/utils/stringutils.py index 89e91b0be7..d30cfb1b0a 100644 --- a/salt/utils/stringutils.py +++ b/salt/utils/stringutils.py @@ -1,9 +1,15 @@ # -*- coding: utf-8 -*- # Import Python libs -from __future__ import absolute_import +from __future__ import absolute_import, print_function +import errno +import fnmatch +import logging import os +import shlex +import re import string +import time # Import Salt libs from salt.utils.decorators.jinja import jinja_filter @@ -12,6 +18,8 @@ from salt.utils.decorators.jinja import jinja_filter from salt.ext import six from salt.ext.six.moves import range # pylint: disable=redefined-builtin +log = logging.getLogger(__name__) + @jinja_filter('to_bytes') def to_bytes(s, encoding=None): @@ -197,3 +205,193 @@ def human_to_bytes(size): else: sbytes = 0 return sbytes + + +def build_whitespace_split_regex(text): + ''' + Create a regular expression at runtime which should match ignoring the + addition or deletion of white space or line breaks, unless between commas + + Example: + + .. code-block:: python + + >>> import re + >>> import salt.utils.stringutils + >>> regex = salt.utils.stringutils.build_whitespace_split_regex( + ... """if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then""" + ... ) + + >>> regex + '(?:[\\s]+)?if(?:[\\s]+)?\\[(?:[\\s]+)?\\-z(?:[\\s]+)?\\"\\$debian' + '\\_chroot\\"(?:[\\s]+)?\\](?:[\\s]+)?\\&\\&(?:[\\s]+)?\\[(?:[\\s]+)?' + '\\-r(?:[\\s]+)?\\/etc\\/debian\\_chroot(?:[\\s]+)?\\]\\;(?:[\\s]+)?' + 'then(?:[\\s]+)?' + >>> re.search( + ... regex, + ... """if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then""" + ... ) + + <_sre.SRE_Match object at 0xb70639c0> + >>> + + ''' + def __build_parts(text): + lexer = shlex.shlex(text) + lexer.whitespace_split = True + lexer.commenters = '' + if '\'' in text: + lexer.quotes = '"' + elif '"' in text: + lexer.quotes = '\'' + return list(lexer) + + regex = r'' + for line in text.splitlines(): + parts = [re.escape(s) for s in __build_parts(line)] + regex += r'(?:[\s]+)?{0}(?:[\s]+)?'.format(r'(?:[\s]+)?'.join(parts)) + return r'(?m)^{0}$'.format(regex) + + +def expr_match(line, expr): + ''' + Evaluate a line of text against an expression. First try a full-string + match, next try globbing, and then try to match assuming expr is a regular + expression. Originally designed to match minion IDs for + whitelists/blacklists. + ''' + if line == expr: + return True + if fnmatch.fnmatch(line, expr): + return True + try: + if re.match(r'\A{0}\Z'.format(expr), line): + return True + except re.error: + pass + return False + + +@jinja_filter('check_whitelist_blacklist') +def check_whitelist_blacklist(value, whitelist=None, blacklist=None): + ''' + Check a whitelist and/or blacklist to see if the value matches it. + + value + The item to check the whitelist and/or blacklist against. + + whitelist + The list of items that are white-listed. If ``value`` is found + in the whitelist, then the function returns ``True``. Otherwise, + it returns ``False``. + + blacklist + The list of items that are black-listed. If ``value`` is found + in the blacklist, then the function returns ``False``. Otherwise, + it returns ``True``. + + If both a whitelist and a blacklist are provided, value membership + in the blacklist will be examined first. If the value is not found + in the blacklist, then the whitelist is checked. If the value isn't + found in the whitelist, the function returns ``False``. + ''' + if blacklist is not None: + if not hasattr(blacklist, '__iter__'): + blacklist = [blacklist] + try: + for expr in blacklist: + if expr_match(value, expr): + return False + except TypeError: + log.error('Non-iterable blacklist %s', blacklist) + + if whitelist: + if not hasattr(whitelist, '__iter__'): + whitelist = [whitelist] + try: + for expr in whitelist: + if expr_match(value, expr): + return True + except TypeError: + log.error('Non-iterable whitelist %s', whitelist) + else: + return True + + return False + + +def check_include_exclude(path_str, include_pat=None, exclude_pat=None): + ''' + Check for glob or regexp patterns for include_pat and exclude_pat in the + 'path_str' string and return True/False conditions as follows. + - Default: return 'True' if no include_pat or exclude_pat patterns are + supplied + - If only include_pat or exclude_pat is supplied: return 'True' if string + passes the include_pat test or fails exclude_pat test respectively + - If both include_pat and exclude_pat are supplied: return 'True' if + include_pat matches AND exclude_pat does not match + ''' + ret = True # -- default true + # Before pattern match, check if it is regexp (E@'') or glob(default) + if include_pat: + if re.match('E@', include_pat): + retchk_include = True if re.search( + include_pat[2:], + path_str + ) else False + else: + retchk_include = True if fnmatch.fnmatch( + path_str, + include_pat + ) else False + + if exclude_pat: + if re.match('E@', exclude_pat): + retchk_exclude = False if re.search( + exclude_pat[2:], + path_str + ) else True + else: + retchk_exclude = False if fnmatch.fnmatch( + path_str, + exclude_pat + ) else True + + # Now apply include/exclude conditions + if include_pat and not exclude_pat: + ret = retchk_include + elif exclude_pat and not include_pat: + ret = retchk_exclude + elif include_pat and exclude_pat: + ret = retchk_include and retchk_exclude + else: + ret = True + + return ret + + +def print_cli(msg, retries=10, step=0.01): + ''' + Wrapper around print() that suppresses tracebacks on broken pipes (i.e. + when salt output is piped to less and less is stopped prematurely). + ''' + while retries: + try: + try: + print(msg) + except UnicodeEncodeError: + print(msg.encode('utf-8')) + except IOError as exc: + err = "{0}".format(exc) + if exc.errno != errno.EPIPE: + if ( + ("temporarily unavailable" in err or + exc.errno in (errno.EAGAIN,)) and + retries + ): + time.sleep(step) + retries -= 1 + continue + else: + raise + break diff --git a/salt/utils/systemd.py b/salt/utils/systemd.py index b41dce546b..dc9dd86c3a 100644 --- a/salt/utils/systemd.py +++ b/salt/utils/systemd.py @@ -10,7 +10,6 @@ import subprocess # Import Salt libs from salt.exceptions import SaltInvocationError -import salt.utils import salt.utils.stringutils log = logging.getLogger(__name__) diff --git a/salt/utils/templates.py b/salt/utils/templates.py index f8e7682538..b5379e5555 100644 --- a/salt/utils/templates.py +++ b/salt/utils/templates.py @@ -27,13 +27,14 @@ else: USE_IMPORTLIB = False # Import Salt libs -import salt.utils +import salt.utils.data import salt.utils.http import salt.utils.files import salt.utils.platform import salt.utils.yamlencoding import salt.utils.locales import salt.utils.hashutils +import salt.utils.stringutils from salt.exceptions import ( SaltRenderError, CommandExecutionError, SaltInvocationError ) @@ -94,6 +95,41 @@ class AliasedModule(object): return getattr(self.wrapped, name) +def get_context(template, line, num_lines=5, marker=None): + ''' + Returns debugging context around a line in a given string + + Returns:: string + ''' + template_lines = template.splitlines() + num_template_lines = len(template_lines) + + # in test, a single line template would return a crazy line number like, + # 357. do this sanity check and if the given line is obviously wrong, just + # return the entire template + if line > num_template_lines: + return template + + context_start = max(0, line - num_lines - 1) # subt 1 for 0-based indexing + context_end = min(num_template_lines, line + num_lines) + error_line_in_context = line - context_start - 1 # subtr 1 for 0-based idx + + buf = [] + if context_start > 0: + buf.append('[...]') + error_line_in_context += 1 + + buf.extend(template_lines[context_start:context_end]) + + if context_end < num_template_lines: + buf.append('[...]') + + if marker: + buf[error_line_in_context] += marker + + return u'---\n{0}\n---'.format(u'\n'.join(buf)) + + def wrap_tmpl_func(render_str): def render_tmpl(tmplsrc, @@ -275,7 +311,7 @@ def _get_jinja_error(trace, context=None): out = '\n{0}\n'.format(msg.splitlines()[0]) with salt.utils.files.fopen(template_path) as fp_: template_contents = fp_.read() - out += salt.utils.get_context( + out += get_context( template_contents, line, marker=' <======================') @@ -339,7 +375,7 @@ def render_jinja_tmpl(tmplstr, context, tmplpath=None): jinja_env.globals['odict'] = OrderedDict jinja_env.globals['show_full_context'] = salt.utils.jinja.show_full_context - jinja_env.tests['list'] = salt.utils.is_list + jinja_env.tests['list'] = salt.utils.data.is_list decoded_context = {} for key, value in six.iteritems(context): diff --git a/salt/utils/thin.py b/salt/utils/thin.py index bd13b9f11c..08924c044d 100644 --- a/salt/utils/thin.py +++ b/salt/utils/thin.py @@ -70,10 +70,10 @@ except ImportError: # Import salt libs import salt -import salt.utils import salt.utils.files import salt.utils.hashutils import salt.exceptions +import salt.version SALTCALL = ''' import os diff --git a/salt/utils/url.py b/salt/utils/url.py index ff02517f9d..7f7be23150 100644 --- a/salt/utils/url.py +++ b/salt/utils/url.py @@ -10,7 +10,7 @@ import sys # Import salt libs from salt.ext.six.moves.urllib.parse import urlparse, urlunparse # pylint: disable=import-error,no-name-in-module -import salt.utils +import salt.utils.path import salt.utils.platform import salt.utils.versions from salt.utils.locales import sdecode @@ -35,7 +35,7 @@ def parse(url): path, saltenv = resource, None if salt.utils.platform.is_windows(): - path = salt.utils.sanitize_win_path_string(path) + path = salt.utils.path.sanitize_win_path(path) return path, saltenv @@ -45,7 +45,7 @@ def create(path, saltenv=None): join `path` and `saltenv` into a 'salt://' URL. ''' if salt.utils.platform.is_windows(): - path = salt.utils.sanitize_win_path_string(path) + path = salt.utils.path.sanitize_win_path(path) path = sdecode(path) query = u'saltenv={0}'.format(saltenv) if saltenv else '' diff --git a/salt/utils/win_functions.py b/salt/utils/win_functions.py index 4e3ec9663c..35d0fabddc 100644 --- a/salt/utils/win_functions.py +++ b/salt/utils/win_functions.py @@ -157,3 +157,12 @@ def get_sam_name(username): username = '{0}\\{1}'.format(platform.node(), username) return username.lower() + + +def enable_ctrl_logoff_handler(): + if HAS_WIN32: + ctrl_logoff_event = 5 + win32api.SetConsoleCtrlHandler( + lambda event: True if event == ctrl_logoff_event else False, + 1 + ) diff --git a/tests/integration/netapi/rest_cherrypy/test_app.py b/tests/integration/netapi/rest_cherrypy/test_app.py index 78a9071301..8eb6529f8b 100644 --- a/tests/integration/netapi/rest_cherrypy/test_app.py +++ b/tests/integration/netapi/rest_cherrypy/test_app.py @@ -5,7 +5,6 @@ from __future__ import absolute_import import json # Import salt libs -import salt.utils import salt.utils.stringutils # Import test support libs diff --git a/tests/integration/netapi/rest_tornado/test_app.py b/tests/integration/netapi/rest_tornado/test_app.py index 6597329a7c..6672508132 100644 --- a/tests/integration/netapi/rest_tornado/test_app.py +++ b/tests/integration/netapi/rest_tornado/test_app.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- # Import Python Libs -from __future__ import absolute_import -from __future__ import print_function +from __future__ import absolute_import, print_function import json import time import threading # Import Salt Libs -import salt.utils import salt.utils.stringutils from salt.netapi.rest_tornado import saltnado from salt.utils.versions import StrictVersion diff --git a/tests/integration/runners/test_state.py b/tests/integration/runners/test_state.py index e14dd06251..36c8141264 100644 --- a/tests/integration/runners/test_state.py +++ b/tests/integration/runners/test_state.py @@ -21,7 +21,6 @@ from tests.support.unit import skipIf from tests.support.paths import TMP # Import Salt Libs -import salt.utils import salt.utils.platform import salt.utils.event import salt.utils.files diff --git a/tests/integration/states/test_pkg.py b/tests/integration/states/test_pkg.py index 27103072c7..8406f6d631 100644 --- a/tests/integration/states/test_pkg.py +++ b/tests/integration/states/test_pkg.py @@ -22,6 +22,7 @@ from tests.support.helpers import ( # Import Salt libs import salt.utils.path +import salt.utils.pkg.rpm import salt.utils.platform # Import 3rd-party libs @@ -754,7 +755,7 @@ class PkgTest(ModuleCase, SaltReturnAssertsMixin): ret = self.run_state( 'pkg.installed', name=target, - version=salt.utils.str_version_to_evr(version)[1], + version=salt.utils.pkg.rpm.version_to_evr(version)[1], refresh=False, ) self.assertSaltTrueReturn(ret) diff --git a/tests/support/mixins.py b/tests/support/mixins.py index 036c6fa3ba..16e1cabe35 100644 --- a/tests/support/mixins.py +++ b/tests/support/mixins.py @@ -31,9 +31,9 @@ from tests.support.paths import CODE_DIR # Import salt libs import salt.config -import salt.utils # TODO: Remove this once namespaced_function is moved import salt.utils.event import salt.utils.files +import salt.utils.functools import salt.utils.path import salt.utils.stringutils import salt.version @@ -444,7 +444,7 @@ class LoaderModuleMockMixin(six.with_metaclass(_FixLoaderModuleMockMixinMroOrder # used to patch above import salt.utils for func in minion_funcs: - minion_funcs[func] = salt.utils.namespaced_function( + minion_funcs[func] = salt.utils.functools.namespaced_function( minion_funcs[func], module_globals, preserve_context=True diff --git a/tests/support/unit.py b/tests/support/unit.py index 841e714e32..19d0bdc48f 100644 --- a/tests/support/unit.py +++ b/tests/support/unit.py @@ -40,6 +40,17 @@ log = logging.getLogger(__name__) # i.e. [CPU:15.1%|MEM:48.3%|Z:0] SHOW_PROC = 'NO_SHOW_PROC' not in os.environ +LOREM_IPSUM = '''\ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eget urna a arcu lacinia sagittis. +Sed scelerisque, lacus eget malesuada vestibulum, justo diam facilisis tortor, in sodales dolor +nibh eu urna. Aliquam iaculis massa risus, sed elementum risus accumsan id. Suspendisse mattis, +metus sed lacinia dictum, leo orci dapibus sapien, at porttitor sapien nulla ac velit. +Duis ac cursus leo, non varius metus. Sed laoreet felis magna, vel tempor diam malesuada nec. +Quisque cursus odio tortor. In consequat augue nisl, eget lacinia odio vestibulum eget. +Donec venenatis elementum arcu at rhoncus. Nunc pharetra erat in lacinia convallis. Ut condimentum +eu mauris sit amet convallis. Morbi vulputate vel odio non laoreet. Nullam in suscipit tellus. +Sed quis posuere urna.''' + # support python < 2.7 via unittest2 if sys.version_info < (2, 7): try: diff --git a/tests/unit/beacons/test_journald_beacon.py b/tests/unit/beacons/test_journald_beacon.py index 7b9b109bf0..ee3c6d1c3d 100644 --- a/tests/unit/beacons/test_journald_beacon.py +++ b/tests/unit/beacons/test_journald_beacon.py @@ -12,7 +12,7 @@ from tests.support.mixins import LoaderModuleMockMixin # Salt libs import salt.beacons.journald as journald -import salt.utils +import salt.utils.data import logging log = logging.getLogger(__name__) @@ -113,7 +113,7 @@ class JournaldBeaconTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(ret, (True, 'Valid beacon configuration')) - _expected_return = salt.utils.simple_types_filter(_STUB_JOURNALD_ENTRY) + _expected_return = salt.utils.data.simple_types_filter(_STUB_JOURNALD_ENTRY) _expected_return['tag'] = 'sshd' ret = journald.beacon(config) diff --git a/tests/unit/cache/test_cache.py b/tests/unit/cache/test_cache.py index 27c8c45b7a..42e1325e7e 100644 --- a/tests/unit/cache/test_cache.py +++ b/tests/unit/cache/test_cache.py @@ -17,7 +17,6 @@ from tests.support.mock import ( # Import Salt libs import salt.payload -import salt.utils import salt.cache diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 948d2ee35e..72c7eca5bd 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -22,9 +22,9 @@ from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch # Import Salt libs import salt.minion -import salt.utils import salt.utils.files import salt.utils.network +import salt.utils.platform from salt.syspaths import CONFIG_DIR from salt import config as sconfig from salt.exceptions import ( @@ -453,7 +453,7 @@ class ConfigTestCase(TestCase, AdaptedConfigurationTestCaseMixin): tempdir = tempfile.mkdtemp(dir=TMP) master_config = os.path.join(tempdir, 'master') - with salt.utils.fopen(master_config, 'w') as fp_: + with salt.utils.files.fopen(master_config, 'w') as fp_: fp_.write( 'id_function:\n' ' test.echo:\n' @@ -537,7 +537,7 @@ class ConfigTestCase(TestCase, AdaptedConfigurationTestCaseMixin): tempdir = tempfile.mkdtemp(dir=TMP) minion_config = os.path.join(tempdir, 'minion') - with salt.utils.fopen(minion_config, 'w') as fp_: + with salt.utils.files.fopen(minion_config, 'w') as fp_: fp_.write( 'id_function:\n' ' test.echo:\n' diff --git a/tests/unit/grains/test_core.py b/tests/unit/grains/test_core.py index 5fb2d85531..3655953e36 100644 --- a/tests/unit/grains/test_core.py +++ b/tests/unit/grains/test_core.py @@ -19,7 +19,6 @@ from tests.support.mock import ( ) # Import Salt Libs -import salt.utils import salt.utils.platform import salt.grains.core as core diff --git a/tests/unit/modules/test_boto_lambda.py b/tests/unit/modules/test_boto_lambda.py index 4c26a10a96..95f4178dc4 100644 --- a/tests/unit/modules/test_boto_lambda.py +++ b/tests/unit/modules/test_boto_lambda.py @@ -30,7 +30,6 @@ import salt.loader import salt.modules.boto_lambda as boto_lambda from salt.exceptions import SaltInvocationError from salt.utils.versions import LooseVersion -import salt.utils import salt.utils.stringutils # Import 3rd-party libs diff --git a/tests/unit/modules/test_disk.py b/tests/unit/modules/test_disk.py index fc4715b0d2..b2ddb4fafb 100644 --- a/tests/unit/modules/test_disk.py +++ b/tests/unit/modules/test_disk.py @@ -152,7 +152,7 @@ class DiskTestCase(TestCase, LoaderModuleMockMixin): with patch.dict(disk.__salt__, {'cmd.retcode': mock}): self.assertEqual(disk.format_(device), True) - @skipIf(not salt.utils.which('lsblk') and not salt.utils.which('df'), + @skipIf(not salt.utils.path.which('lsblk') and not salt.utils.path.which('df'), 'lsblk or df not found') def test_fstype(self): ''' diff --git a/tests/unit/modules/test_file.py b/tests/unit/modules/test_file.py index 8f1b3a97a0..540c805ff5 100644 --- a/tests/unit/modules/test_file.py +++ b/tests/unit/modules/test_file.py @@ -17,6 +17,8 @@ from tests.support.mock import MagicMock, patch import salt.config import salt.loader import salt.utils.files +import salt.utils.platform +import salt.utils.stringutils import salt.modules.file as filemod import salt.modules.config as configmod import salt.modules.cmdmod as cmdmod @@ -96,7 +98,7 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): # File ending with a newline, no match with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: - tfile.write(salt.utils.to_bytes(base + os.linesep)) + tfile.write(salt.utils.stringutils.to_bytes(base + os.linesep)) tfile.flush() filemod.replace(tfile.name, **args) expected = os.linesep.join([base, 'baz=\\g']) + os.linesep @@ -106,7 +108,7 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): # File not ending with a newline, no match with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: - tfile.write(salt.utils.to_bytes(base)) + tfile.write(salt.utils.stringutils.to_bytes(base)) tfile.flush() filemod.replace(tfile.name, **args) with salt.utils.files.fopen(tfile.name) as tfile2: @@ -124,7 +126,7 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): # Using not_found_content, rather than repl with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: - tfile.write(salt.utils.to_bytes(base)) + tfile.write(salt.utils.stringutils.to_bytes(base)) tfile.flush() args['not_found_content'] = 'baz=3' expected = os.linesep.join([base, 'baz=3']) + os.linesep @@ -136,7 +138,7 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin): # not appending if matches with tempfile.NamedTemporaryFile('w+b', delete=False) as tfile: base = os.linesep.join(['foo=1', 'baz=42', 'bar=2']) - tfile.write(salt.utils.to_bytes(base)) + tfile.write(salt.utils.stringutils.to_bytes(base)) tfile.flush() expected = base filemod.replace(tfile.name, **args) @@ -275,7 +277,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): with salt.utils.files.fopen(self.tfile.name, 'rb') as fp: filecontent = fp.read() - self.assertIn(salt.utils.to_bytes( + self.assertIn(salt.utils.stringutils.to_bytes( os.linesep.join([ '#-- START BLOCK 1', new_multiline_content, '#-- END BLOCK 1'])), filecontent) @@ -308,7 +310,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): append_if_not_found=True) with salt.utils.files.fopen(self.tfile.name, 'rb') as fp: - self.assertIn(salt.utils.to_bytes( + self.assertIn(salt.utils.stringutils.to_bytes( os.linesep.join([ '#-- START BLOCK 2', '{0}#-- END BLOCK 2'.format(new_content)])), @@ -329,7 +331,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): block = os.linesep.join(['#start', 'baz#stop']) + os.linesep # File ending with a newline with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write(salt.utils.to_bytes(base + os.linesep)) + tfile.write(salt.utils.stringutils.to_bytes(base + os.linesep)) tfile.flush() filemod.blockreplace(tfile.name, **args) expected = os.linesep.join([base, block]) @@ -339,7 +341,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): # File not ending with a newline with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write(salt.utils.to_bytes(base)) + tfile.write(salt.utils.stringutils.to_bytes(base)) tfile.flush() filemod.blockreplace(tfile.name, **args) with salt.utils.files.fopen(tfile.name) as tfile2: @@ -368,7 +370,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): backup=False ) with salt.utils.files.fopen(self.tfile.name, 'rb') as fp: - self.assertNotIn(salt.utils.to_bytes( + self.assertNotIn(salt.utils.stringutils.to_bytes( os.linesep.join([ '#-- START BLOCK 2', '{0}#-- END BLOCK 2'.format(new_content)])), @@ -382,7 +384,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): with salt.utils.files.fopen(self.tfile.name, 'rb') as fp: self.assertTrue( - fp.read().startswith(salt.utils.to_bytes( + fp.read().startswith(salt.utils.stringutils.to_bytes( os.linesep.join([ '#-- START BLOCK 2', '{0}#-- END BLOCK 2'.format(new_content)])))) @@ -502,7 +504,7 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): } } - @skipIf(salt.utils.is_windows(), 'SED is not available on Windows') + @skipIf(salt.utils.platform.is_windows(), 'SED is not available on Windows') def test_sed_limit_escaped(self): with tempfile.NamedTemporaryFile(mode='w+') as tfile: tfile.write(SED_CONTENT) @@ -528,7 +530,7 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): ''' # File ending with a newline with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write(salt.utils.to_bytes('foo' + os.linesep)) + tfile.write(salt.utils.stringutils.to_bytes('foo' + os.linesep)) tfile.flush() filemod.append(tfile.name, 'bar') expected = os.linesep.join(['foo', 'bar']) + os.linesep @@ -537,10 +539,10 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): # File not ending with a newline with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write(salt.utils.to_bytes('foo')) + tfile.write(salt.utils.stringutils.to_bytes('foo')) tfile.flush() filemod.append(tfile.name, 'bar') - with salt.utils.fopen(tfile.name) as tfile2: + with salt.utils.files.fopen(tfile.name) as tfile2: self.assertEqual(tfile2.read(), expected) # A newline should be added in empty files @@ -555,7 +557,7 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): ''' # With file name with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write(salt.utils.to_bytes( + tfile.write(salt.utils.stringutils.to_bytes( 'rc.conf ef6e82e4006dee563d98ada2a2a80a27\n' 'ead48423703509d37c4a90e6a0d53e143b6fc268 example.tar.gz\n' 'fe05bcdcdc4928012781a5f1a2a77cbb5398e106 ./subdir/example.tar.gz\n' @@ -640,7 +642,7 @@ class FileModuleTestCase(TestCase, LoaderModuleMockMixin): # Since there is no name match, the first checksum in the file will # always be returned, never the second. with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tfile: - tfile.write(salt.utils.to_bytes( + tfile.write(salt.utils.stringutils.to_bytes( 'ead48423703509d37c4a90e6a0d53e143b6fc268\n' 'ad782ecdac770fc6eb9a62e44f90873fb97fb26b\n')) tfile.flush() @@ -804,7 +806,7 @@ class FileBasicsTestCase(TestCase, LoaderModuleMockMixin): self.addCleanup(os.remove, self.myfile) self.addCleanup(delattr, self, 'myfile') - @skipIf(salt.utils.is_windows(), 'os.symlink is not available on Windows') + @skipIf(salt.utils.platform.is_windows(), 'os.symlink is not available on Windows') def test_symlink_already_in_desired_state(self): os.symlink(self.tfile.name, self.directory + '/a_link') self.addCleanup(os.remove, self.directory + '/a_link') diff --git a/tests/unit/modules/test_hosts.py b/tests/unit/modules/test_hosts.py index 7cd7699453..43aaf3b59d 100644 --- a/tests/unit/modules/test_hosts.py +++ b/tests/unit/modules/test_hosts.py @@ -16,7 +16,7 @@ from tests.support.mock import ( ) # Import Salt Libs import salt.modules.hosts as hosts -import salt.utils +import salt.utils.platform from salt.ext.six.moves import StringIO diff --git a/tests/unit/modules/test_ini_manage.py b/tests/unit/modules/test_ini_manage.py index f3550262a9..0f81322ac3 100644 --- a/tests/unit/modules/test_ini_manage.py +++ b/tests/unit/modules/test_ini_manage.py @@ -10,6 +10,7 @@ from tests.support.unit import TestCase # Import Salt libs import salt.utils.files +import salt.utils.stringutils import salt.modules.ini_manage as ini @@ -46,7 +47,7 @@ class IniManageTestCase(TestCase): def setUp(self): self.tfile = tempfile.NamedTemporaryFile(delete=False, mode='w+b') - self.tfile.write(salt.utils.to_bytes(self.TEST_FILE_CONTENT)) + self.tfile.write(salt.utils.stringutils.to_bytes(self.TEST_FILE_CONTENT)) self.tfile.close() def tearDown(self): diff --git a/tests/unit/modules/test_launchctl.py b/tests/unit/modules/test_launchctl.py index 108ba90c34..ccf628ecc0 100644 --- a/tests/unit/modules/test_launchctl.py +++ b/tests/unit/modules/test_launchctl.py @@ -18,7 +18,6 @@ from tests.support.mock import ( # Import Salt Libs from salt.ext import six -import salt.utils import salt.utils.stringutils import salt.modules.launchctl as launchctl diff --git a/tests/unit/modules/test_pagerduty.py b/tests/unit/modules/test_pagerduty.py index 3a4b975e86..2e7e59cbb9 100644 --- a/tests/unit/modules/test_pagerduty.py +++ b/tests/unit/modules/test_pagerduty.py @@ -18,7 +18,7 @@ from tests.support.mock import ( ) # Import Salt Libs -import salt.utils +import salt.utils.pagerduty import salt.modules.pagerduty as pagerduty diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index 6f72eb091d..d083b7b1e5 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -15,7 +15,6 @@ import salt.modules.virt as virt import salt.modules.config as config from salt._compat import ElementTree as ET import salt.config -import salt.utils # Import third party libs import yaml diff --git a/tests/unit/modules/test_win_network.py b/tests/unit/modules/test_win_network.py index e849a0d8bf..986dc7e57d 100644 --- a/tests/unit/modules/test_win_network.py +++ b/tests/unit/modules/test_win_network.py @@ -19,7 +19,7 @@ from tests.support.mock import ( ) # Import Salt Libs -import salt.utils +import salt.utils.network import salt.modules.win_network as win_network diff --git a/tests/unit/output/test_highstate_out.py b/tests/unit/output/test_highstate_out.py index d02f1076fa..4f6893769f 100644 --- a/tests/unit/output/test_highstate_out.py +++ b/tests/unit/output/test_highstate_out.py @@ -11,7 +11,6 @@ from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import TestCase # Import Salt Libs -import salt.utils import salt.utils.stringutils import salt.output.highstate as highstate diff --git a/tests/unit/runners/test_cache.py b/tests/unit/runners/test_cache.py index 6922dc83b1..442ce41f2c 100644 --- a/tests/unit/runners/test_cache.py +++ b/tests/unit/runners/test_cache.py @@ -18,7 +18,7 @@ from tests.support.mock import ( # Import Salt Libs import salt.runners.cache as cache -import salt.utils +import salt.utils.master @skipIf(NO_MOCK, NO_MOCK_REASON) diff --git a/tests/unit/states/test_file.py b/tests/unit/states/test_file.py index 77ffe469bd..a3f24b8cb1 100644 --- a/tests/unit/states/test_file.py +++ b/tests/unit/states/test_file.py @@ -31,7 +31,6 @@ from tests.support.mock import ( import yaml # Import salt libs -import salt.utils import salt.utils.files import salt.utils.platform import salt.states.file as filestate diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py index d08f9e71b1..77e75afaf2 100644 --- a/tests/unit/test_auth.py +++ b/tests/unit/test_auth.py @@ -61,7 +61,7 @@ class LoadAuthTestCase(TestCase): 'show_timeout': False, 'test_password': '', 'eauth': 'pam'} - with patch('salt.utils.format_call') as format_call_mock: + with patch('salt.utils.args.format_call') as format_call_mock: expected_ret = call('fake_groups_function_str', { 'username': 'test_user', 'test_password': '', diff --git a/tests/unit/test_crypt.py b/tests/unit/test_crypt.py index 9e3708402c..9e37d6f52b 100644 --- a/tests/unit/test_crypt.py +++ b/tests/unit/test_crypt.py @@ -9,7 +9,6 @@ from tests.support.unit import TestCase, skipIf from tests.support.mock import patch, call, mock_open, NO_MOCK, NO_MOCK_REASON, MagicMock # salt libs -import salt.utils import salt.utils.files from salt import crypt diff --git a/tests/unit/test_ssh_config_roster.py b/tests/unit/test_ssh_config_roster.py index f3479caf89..7d074dd863 100644 --- a/tests/unit/test_ssh_config_roster.py +++ b/tests/unit/test_ssh_config_roster.py @@ -73,14 +73,14 @@ class SSHConfigRosterTestCase(TestCase, mixins.LoaderModuleMockMixin): return {sshconfig: {}} def test_all(self): - with mock.patch('salt.utils.fopen', self.mock_fp): + with mock.patch('salt.utils.files.fopen', self.mock_fp): with mock.patch('salt.roster.sshconfig._get_ssh_config_file'): self.mock_fp.return_value.__iter__.return_value = _SAMPLE_SSH_CONFIG.splitlines() targets = sshconfig.targets('*') self.assertEqual(targets, _ALL) def test_abc_glob(self): - with mock.patch('salt.utils.fopen', self.mock_fp): + with mock.patch('salt.utils.files.fopen', self.mock_fp): with mock.patch('salt.roster.sshconfig._get_ssh_config_file'): self.mock_fp.return_value.__iter__.return_value = _SAMPLE_SSH_CONFIG.splitlines() targets = sshconfig.targets('abc*') diff --git a/tests/unit/transport/test_ipc.py b/tests/unit/transport/test_ipc.py index 2a45d9cb2d..24b280b13f 100644 --- a/tests/unit/transport/test_ipc.py +++ b/tests/unit/transport/test_ipc.py @@ -14,7 +14,6 @@ import tornado.gen import tornado.ioloop import tornado.testing -import salt.utils import salt.config import salt.exceptions import salt.transport.ipc diff --git a/tests/unit/transport/test_zeromq.py b/tests/unit/transport/test_zeromq.py index fee8002322..730a6896a2 100644 --- a/tests/unit/transport/test_zeromq.py +++ b/tests/unit/transport/test_zeromq.py @@ -26,7 +26,7 @@ import tornado.gen # Import Salt libs import salt.config from salt.ext import six -import salt.utils +import salt.utils.process import salt.transport.server import salt.transport.client import salt.exceptions diff --git a/tests/unit/utils/test_args.py b/tests/unit/utils/test_args.py index 9fdbde7c82..00138ff7a2 100644 --- a/tests/unit/utils/test_args.py +++ b/tests/unit/utils/test_args.py @@ -74,16 +74,50 @@ class ArgsTestCase(TestCase): args=['first', 'second', 'third', 'fourth'], varargs=None, keywords=None, defaults=('fifth',)) # Make sure we raise an error if we don't pass in the requisite number of arguments - self.assertRaises(SaltInvocationError, salt.utils.format_call, dummy_func, {'1': 2}) + self.assertRaises(SaltInvocationError, salt.utils.args.format_call, dummy_func, {'1': 2}) # Make sure we warn on invalid kwargs - ret = salt.utils.format_call(dummy_func, {'first': 2, 'second': 2, 'third': 3}) + ret = salt.utils.args.format_call(dummy_func, {'first': 2, 'second': 2, 'third': 3}) self.assertGreaterEqual(len(ret['warnings']), 1) - ret = salt.utils.format_call(dummy_func, {'first': 2, 'second': 2, 'third': 3}, + ret = salt.utils.args.format_call(dummy_func, {'first': 2, 'second': 2, 'third': 3}, expected_extra_kws=('first', 'second', 'third')) self.assertDictEqual(ret, {'args': [], 'kwargs': {}}) + def test_format_call_simple_args(self): + def foo(one, two=2, three=3): + pass + + self.assertEqual( + salt.utils.args.format_call(foo, dict(one=10, two=20, three=30)), + {'args': [10], 'kwargs': dict(two=20, three=30)} + ) + self.assertEqual( + salt.utils.args.format_call(foo, dict(one=10, two=20)), + {'args': [10], 'kwargs': dict(two=20, three=3)} + ) + self.assertEqual( + salt.utils.args.format_call(foo, dict(one=2)), + {'args': [2], 'kwargs': dict(two=2, three=3)} + ) + + def test_format_call_mimic_typeerror_exceptions(self): + def foo(one, two=2, three=3): + pass + + def foo2(one, two, three=3): + pass + + with self.assertRaisesRegex( + SaltInvocationError, + r'foo takes at least 1 argument \(0 given\)'): + salt.utils.args.format_call(foo, dict(two=3)) + + with self.assertRaisesRegex( + TypeError, + r'foo2 takes at least 2 arguments \(1 given\)'): + salt.utils.args.format_call(foo2, dict(one=1)) + @skipIf(NO_MOCK, NO_MOCK_REASON) def test_argspec_report(self): def _test_spec(arg1, arg2, kwarg1=None): diff --git a/tests/unit/utils/test_data.py b/tests/unit/utils/test_data.py index be7ddf9f52..b73cf38042 100644 --- a/tests/unit/utils/test_data.py +++ b/tests/unit/utils/test_data.py @@ -8,7 +8,7 @@ from __future__ import absolute_import # Import Salt libs import salt.utils.data -from tests.support.unit import TestCase +from tests.support.unit import TestCase, LOREM_IPSUM class DataTestCase(TestCase): @@ -17,3 +17,196 @@ class DataTestCase(TestCase): expected_list = ['bar', 'Bar', 'foo', 'Foo'] self.assertEqual( salt.utils.data.sorted_ignorecase(test_list), expected_list) + + def test_mysql_to_dict(self): + test_mysql_output = ['+----+------+-----------+------+---------+------+-------+------------------+', + '| Id | User | Host | db | Command | Time | State | Info |', + '+----+------+-----------+------+---------+------+-------+------------------+', + '| 7 | root | localhost | NULL | Query | 0 | init | show processlist |', + '+----+------+-----------+------+---------+------+-------+------------------+'] + + ret = salt.utils.data.mysql_to_dict(test_mysql_output, 'Info') + expected_dict = { + 'show processlist': {'Info': 'show processlist', 'db': 'NULL', 'State': 'init', 'Host': 'localhost', + 'Command': 'Query', 'User': 'root', 'Time': 0, 'Id': 7}} + + self.assertDictEqual(ret, expected_dict) + + def test_subdict_match(self): + test_two_level_dict = {'foo': {'bar': 'baz'}} + test_two_level_comb_dict = {'foo': {'bar': 'baz:woz'}} + test_two_level_dict_and_list = { + 'abc': ['def', 'ghi', {'lorem': {'ipsum': [{'dolor': 'sit'}]}}], + } + test_three_level_dict = {'a': {'b': {'c': 'v'}}} + + self.assertTrue( + salt.utils.data.subdict_match( + test_two_level_dict, 'foo:bar:baz' + ) + ) + # In test_two_level_comb_dict, 'foo:bar' corresponds to 'baz:woz', not + # 'baz'. This match should return False. + self.assertFalse( + salt.utils.data.subdict_match( + test_two_level_comb_dict, 'foo:bar:baz' + ) + ) + # This tests matching with the delimiter in the value part (in other + # words, that the path 'foo:bar' corresponds to the string 'baz:woz'). + self.assertTrue( + salt.utils.data.subdict_match( + test_two_level_comb_dict, 'foo:bar:baz:woz' + ) + ) + # This would match if test_two_level_comb_dict['foo']['bar'] was equal + # to 'baz:woz:wiz', or if there was more deep nesting. But it does not, + # so this should return False. + self.assertFalse( + salt.utils.data.subdict_match( + test_two_level_comb_dict, 'foo:bar:baz:woz:wiz' + ) + ) + # This tests for cases when a key path corresponds to a list. The + # value part 'ghi' should be successfully matched as it is a member of + # the list corresponding to key path 'abc'. It is somewhat a + # duplication of a test within test_traverse_dict_and_list, but + # salt.utils.data.subdict_match() does more than just invoke + # salt.utils.traverse_list_and_dict() so this particular assertion is a + # sanity check. + self.assertTrue( + salt.utils.data.subdict_match( + test_two_level_dict_and_list, 'abc:ghi' + ) + ) + # This tests the use case of a dict embedded in a list, embedded in a + # list, embedded in a dict. This is a rather absurd case, but it + # confirms that match recursion works properly. + self.assertTrue( + salt.utils.data.subdict_match( + test_two_level_dict_and_list, 'abc:lorem:ipsum:dolor:sit' + ) + ) + # Test four level dict match for reference + self.assertTrue( + salt.utils.data.subdict_match( + test_three_level_dict, 'a:b:c:v' + ) + ) + self.assertFalse( + # Test regression in 2015.8 where 'a:c:v' would match 'a:b:c:v' + salt.utils.data.subdict_match( + test_three_level_dict, 'a:c:v' + ) + ) + # Test wildcard match + self.assertTrue( + salt.utils.data.subdict_match( + test_three_level_dict, 'a:*:c:v' + ) + ) + + def test_traverse_dict(self): + test_two_level_dict = {'foo': {'bar': 'baz'}} + + self.assertDictEqual( + {'not_found': 'nope'}, + salt.utils.data.traverse_dict( + test_two_level_dict, 'foo:bar:baz', {'not_found': 'nope'} + ) + ) + self.assertEqual( + 'baz', + salt.utils.data.traverse_dict( + test_two_level_dict, 'foo:bar', {'not_found': 'not_found'} + ) + ) + + def test_traverse_dict_and_list(self): + test_two_level_dict = {'foo': {'bar': 'baz'}} + test_two_level_dict_and_list = { + 'foo': ['bar', 'baz', {'lorem': {'ipsum': [{'dolor': 'sit'}]}}] + } + + # Check traversing too far: salt.utils.data.traverse_dict_and_list() returns + # the value corresponding to a given key path, and baz is a value + # corresponding to the key path foo:bar. + self.assertDictEqual( + {'not_found': 'nope'}, + salt.utils.data.traverse_dict_and_list( + test_two_level_dict, 'foo:bar:baz', {'not_found': 'nope'} + ) + ) + # Now check to ensure that foo:bar corresponds to baz + self.assertEqual( + 'baz', + salt.utils.data.traverse_dict_and_list( + test_two_level_dict, 'foo:bar', {'not_found': 'not_found'} + ) + ) + # Check traversing too far + self.assertDictEqual( + {'not_found': 'nope'}, + salt.utils.data.traverse_dict_and_list( + test_two_level_dict_and_list, 'foo:bar', {'not_found': 'nope'} + ) + ) + # Check index 1 (2nd element) of list corresponding to path 'foo' + self.assertEqual( + 'baz', + salt.utils.data.traverse_dict_and_list( + test_two_level_dict_and_list, 'foo:1', {'not_found': 'not_found'} + ) + ) + # Traverse a couple times into dicts embedded in lists + self.assertEqual( + 'sit', + salt.utils.data.traverse_dict_and_list( + test_two_level_dict_and_list, + 'foo:lorem:ipsum:dolor', + {'not_found': 'not_found'} + ) + ) + + def test_compare_dicts(self): + ret = salt.utils.data.compare_dicts(old={'foo': 'bar'}, new={'foo': 'bar'}) + self.assertEqual(ret, {}) + + ret = salt.utils.data.compare_dicts(old={'foo': 'bar'}, new={'foo': 'woz'}) + expected_ret = {'foo': {'new': 'woz', 'old': 'bar'}} + self.assertDictEqual(ret, expected_ret) + + def test_decode_list(self): + test_data = [u'unicode_str', [u'unicode_item_in_list', 'second_item_in_list'], {'dict_key': u'dict_val'}] + expected_ret = ['unicode_str', ['unicode_item_in_list', 'second_item_in_list'], {'dict_key': 'dict_val'}] + ret = salt.utils.data.decode_list(test_data) + self.assertEqual(ret, expected_ret) + + def test_decode_dict(self): + test_data = {u'test_unicode_key': u'test_unicode_val', + 'test_list_key': ['list_1', u'unicode_list_two'], + u'test_dict_key': {'test_sub_dict_key': 'test_sub_dict_val'}} + expected_ret = {'test_unicode_key': 'test_unicode_val', + 'test_list_key': ['list_1', 'unicode_list_two'], + 'test_dict_key': {'test_sub_dict_key': 'test_sub_dict_val'}} + ret = salt.utils.data.decode_dict(test_data) + self.assertDictEqual(ret, expected_ret) + + def test_repack_dict(self): + list_of_one_element_dicts = [{'dict_key_1': 'dict_val_1'}, + {'dict_key_2': 'dict_val_2'}, + {'dict_key_3': 'dict_val_3'}] + expected_ret = {'dict_key_1': 'dict_val_1', + 'dict_key_2': 'dict_val_2', + 'dict_key_3': 'dict_val_3'} + ret = salt.utils.data.repack_dictlist(list_of_one_element_dicts) + self.assertDictEqual(ret, expected_ret) + + # Try with yaml + yaml_key_val_pair = '- key1: val1' + ret = salt.utils.data.repack_dictlist(yaml_key_val_pair) + self.assertDictEqual(ret, {'key1': 'val1'}) + + # Make sure we handle non-yaml junk data + ret = salt.utils.data.repack_dictlist(LOREM_IPSUM) + self.assertDictEqual(ret, {}) diff --git a/tests/unit/utils/test_dateutils.py b/tests/unit/utils/test_dateutils.py new file mode 100644 index 0000000000..a917e3908e --- /dev/null +++ b/tests/unit/utils/test_dateutils.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +''' +Tests for salt.utils.dateutils +''' + +# Import Python libs +from __future__ import absolute_import +import datetime + +# Import Salt Testing libs +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + patch, + NO_MOCK, + NO_MOCK_REASON +) + +# Import Salt libs +import salt.utils.dateutils + +# Import 3rd-party libs +try: + import timelib # pylint: disable=import-error,unused-import + HAS_TIMELIB = True +except ImportError: + HAS_TIMELIB = False + + +class DateutilsTestCase(TestCase): + + @skipIf(NO_MOCK, NO_MOCK_REASON) + def test_date_cast(self): + now = datetime.datetime.now() + with patch('datetime.datetime'): + datetime.datetime.now.return_value = now + self.assertEqual(now, salt.utils.dateutils.date_cast(None)) + self.assertEqual(now, salt.utils.dateutils.date_cast(now)) + try: + ret = salt.utils.dateutils.date_cast('Mon Dec 23 10:19:15 MST 2013') + expected_ret = datetime.datetime(2013, 12, 23, 10, 19, 15) + self.assertEqual(ret, expected_ret) + except RuntimeError: + if not HAS_TIMELIB: + # Unparseable without timelib installed + self.skipTest('\'timelib\' is not installed') + else: + raise + + @skipIf(not HAS_TIMELIB, '\'timelib\' is not installed') + def test_strftime(self): + + # Taken from doctests + + expected_ret = '2002-12-25' + + src = datetime.datetime(2002, 12, 25, 12, 00, 00, 00) + ret = salt.utils.dateutils.strftime(src) + self.assertEqual(ret, expected_ret) + + src = '2002/12/25' + ret = salt.utils.dateutils.strftime(src) + self.assertEqual(ret, expected_ret) + + src = 1040814000 + ret = salt.utils.dateutils.strftime(src) + self.assertEqual(ret, expected_ret) + + src = '1040814000' + ret = salt.utils.dateutils.strftime(src) + self.assertEqual(ret, expected_ret) diff --git a/tests/unit/utils/test_files.py b/tests/unit/utils/test_files.py index 9d092227bb..4f70092834 100644 --- a/tests/unit/utils/test_files.py +++ b/tests/unit/utils/test_files.py @@ -6,11 +6,14 @@ Unit Tests for functions located in salt.utils.files.py. # Import python libs from __future__ import absolute_import import os +import shutil +import tempfile # Import Salt libs import salt.utils.files # Import Salt Testing libs +from tests.support.paths import TMP from tests.support.unit import TestCase, skipIf from tests.support.mock import ( patch, @@ -38,3 +41,31 @@ class FilesUtilTestCase(TestCase): except (IOError, OSError): error = True self.assertFalse(error, 'salt.utils.files.safe_rm raised exception when it should not have') + + def test_safe_walk_symlink_recursion(self): + tmp = tempfile.mkdtemp(dir=TMP) + try: + if os.stat(tmp).st_ino == 0: + self.skipTest('inodes not supported in {0}'.format(tmp)) + os.mkdir(os.path.join(tmp, 'fax')) + os.makedirs(os.path.join(tmp, 'foo/bar')) + os.symlink('../..', os.path.join(tmp, 'foo/bar/baz')) + os.symlink('foo', os.path.join(tmp, 'root')) + expected = [ + (os.path.join(tmp, 'root'), ['bar'], []), + (os.path.join(tmp, 'root/bar'), ['baz'], []), + (os.path.join(tmp, 'root/bar/baz'), ['fax', 'foo', 'root'], []), + (os.path.join(tmp, 'root/bar/baz/fax'), [], []), + ] + paths = [] + for root, dirs, names in salt.utils.files.safe_walk(os.path.join(tmp, 'root')): + paths.append((root, sorted(dirs), names)) + if paths != expected: + raise AssertionError( + '\n'.join( + ['got:'] + [repr(p) for p in paths] + + ['', 'expected:'] + [repr(p) for p in expected] + ) + ) + finally: + shutil.rmtree(tmp) diff --git a/tests/unit/utils/test_format_call.py b/tests/unit/utils/test_format_call.py deleted file mode 100644 index a71218ebd0..0000000000 --- a/tests/unit/utils/test_format_call.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -''' - :codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)` - - - tests.unit.utils.format_call_test - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Test `salt.utils.format_call` -''' - -# Import python libs -from __future__ import absolute_import - -# Import Salt Testing libs -from tests.support.unit import TestCase - -# Import salt libs -from salt.utils import format_call -from salt.exceptions import SaltInvocationError - - -class TestFormatCall(TestCase): - def test_simple_args_passing(self): - def foo(one, two=2, three=3): - pass - - self.assertEqual( - format_call(foo, dict(one=10, two=20, three=30)), - {'args': [10], 'kwargs': dict(two=20, three=30)} - ) - self.assertEqual( - format_call(foo, dict(one=10, two=20)), - {'args': [10], 'kwargs': dict(two=20, three=3)} - ) - self.assertEqual( - format_call(foo, dict(one=2)), - {'args': [2], 'kwargs': dict(two=2, three=3)} - ) - - def test_mimic_typeerror_exceptions(self): - def foo(one, two=2, three=3): - pass - - def foo2(one, two, three=3): - pass - - with self.assertRaisesRegex( - SaltInvocationError, - r'foo takes at least 1 argument \(0 given\)'): - format_call(foo, dict(two=3)) - - with self.assertRaisesRegex( - TypeError, - r'foo2 takes at least 2 arguments \(1 given\)'): - format_call(foo2, dict(one=1)) diff --git a/tests/unit/utils/test_jid.py b/tests/unit/utils/test_jid.py new file mode 100644 index 0000000000..f7c909d804 --- /dev/null +++ b/tests/unit/utils/test_jid.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +''' +Tests for salt.utils.jid +''' + +# Import Python libs +from __future__ import absolute_import +import datetime +import os + +# Import Salt libs +import salt.utils.jid +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + patch, + NO_MOCK, + NO_MOCK_REASON +) + + +class JidTestCase(TestCase): + def test_jid_to_time(self): + test_jid = 20131219110700123489 + expected_jid = '2013, Dec 19 11:07:00.123489' + self.assertEqual(salt.utils.jid.jid_to_time(test_jid), expected_jid) + + # Test incorrect lengths + incorrect_jid_length = 2012 + self.assertEqual(salt.utils.jid.jid_to_time(incorrect_jid_length), '') + + def test_is_jid(self): + self.assertTrue(salt.utils.jid.is_jid('20131219110700123489')) # Valid JID + self.assertFalse(salt.utils.jid.is_jid(20131219110700123489)) # int + self.assertFalse(salt.utils.jid.is_jid('2013121911070012348911111')) # Wrong length + + @skipIf(NO_MOCK, NO_MOCK_REASON) + def test_gen_jid(self): + now = datetime.datetime(2002, 12, 25, 12, 00, 00, 00) + with patch('datetime.datetime'): + datetime.datetime.now.return_value = now + ret = salt.utils.jid.gen_jid({}) + self.assertEqual(ret, '20021225120000000000') + salt.utils.jid.LAST_JID_DATETIME = None + ret = salt.utils.jid.gen_jid({'unique_jid': True}) + self.assertEqual(ret, '20021225120000000000_{0}'.format(os.getpid())) + ret = salt.utils.jid.gen_jid({'unique_jid': True}) + self.assertEqual(ret, '20021225120000000001_{0}'.format(os.getpid())) diff --git a/tests/unit/utils/test_json.py b/tests/unit/utils/test_json.py new file mode 100644 index 0000000000..be3763e789 --- /dev/null +++ b/tests/unit/utils/test_json.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +''' +Tests for salt.utils.json +''' + +# Import Python libs +from __future__ import absolute_import + +# Import Salt libs +import salt.utils.json +from tests.support.unit import TestCase, LOREM_IPSUM + + +class JsonTestCase(TestCase): + + def test_find_json(self): + test_sample_json = ''' + { + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + } + } + ''' + expected_ret = {'glossary': {'GlossDiv': {'GlossList': {'GlossEntry': { + 'GlossDef': {'GlossSeeAlso': ['GML', 'XML'], + 'para': 'A meta-markup language, used to create markup languages such as DocBook.'}, + 'GlossSee': 'markup', 'Acronym': 'SGML', 'GlossTerm': 'Standard Generalized Markup Language', + 'SortAs': 'SGML', + 'Abbrev': 'ISO 8879:1986', 'ID': 'SGML'}}, 'title': 'S'}, 'title': 'example glossary'}} + + # First test the valid JSON + ret = salt.utils.json.find_json(test_sample_json) + self.assertDictEqual(ret, expected_ret) + + # Now pre-pend some garbage and re-test + garbage_prepend_json = '{0}{1}'.format(LOREM_IPSUM, test_sample_json) + ret = salt.utils.json.find_json(garbage_prepend_json) + self.assertDictEqual(ret, expected_ret) + + # Test to see if a ValueError is raised if no JSON is passed in + self.assertRaises(ValueError, salt.utils.json.find_json, LOREM_IPSUM) diff --git a/tests/unit/utils/test_path.py b/tests/unit/utils/test_path.py index c9bc6761a3..60e81a298c 100644 --- a/tests/unit/utils/test_path.py +++ b/tests/unit/utils/test_path.py @@ -23,6 +23,7 @@ from tests.support.mock import patch, NO_MOCK, NO_MOCK_REASON # Import Salt libs import salt.utils.path import salt.utils.platform +from salt.exceptions import CommandNotFoundError # Import 3rd-party libs from salt.ext import six @@ -140,7 +141,7 @@ class PathJoinTestCase(TestCase): @skipIf(NO_MOCK, NO_MOCK_REASON) -class WhichTestCase(TestCase): +class PathTestCase(TestCase): def test_which_bin(self): ret = salt.utils.path.which_bin('str') self.assertIs(None, ret) @@ -156,3 +157,15 @@ class WhichTestCase(TestCase): with patch('salt.utils.path.which', return_value=''): ret = salt.utils.path.which_bin(test_exes) self.assertIs(None, ret) + + def test_sanitize_win_path(self): + p = '\\windows\\system' + self.assertEqual(salt.utils.path.sanitize_win_path('\\windows\\system'), '\\windows\\system') + self.assertEqual(salt.utils.path.sanitize_win_path('\\bo:g|us\\p?at*h>'), '\\bo_g_us\\p_at_h_') + + @skipIf(NO_MOCK, NO_MOCK_REASON) + def test_check_or_die(self): + self.assertRaises(CommandNotFoundError, salt.utils.path.check_or_die, None) + + with patch('salt.utils.path.which', return_value=False): + self.assertRaises(CommandNotFoundError, salt.utils.path.check_or_die, 'FAKE COMMAND') diff --git a/tests/unit/utils/test_runtime_whitespace_regex.py b/tests/unit/utils/test_runtime_whitespace_regex.py index d582ab744e..092ffe1cad 100644 --- a/tests/unit/utils/test_runtime_whitespace_regex.py +++ b/tests/unit/utils/test_runtime_whitespace_regex.py @@ -15,7 +15,7 @@ import re from tests.support.unit import TestCase # Import salt libs -from salt.utils import build_whitespace_split_regex +from salt.utils.stringutils import build_whitespace_split_regex DOUBLE_TXT = '''\ # set variable identifying the chroot you work in (used in the prompt below) diff --git a/tests/unit/utils/test_safe_walk.py b/tests/unit/utils/test_safe_walk.py deleted file mode 100644 index 80aaa19f38..0000000000 --- a/tests/unit/utils/test_safe_walk.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- - -# Import python libs -from __future__ import absolute_import -import os -from os.path import join -from shutil import rmtree -from tempfile import mkdtemp - -# Import Salt Testing libs -from tests.support.unit import TestCase -from tests.support.paths import TMP - -# Import salt libs -import salt.utils -import salt.utils.find - - -class TestUtils(TestCase): - - def test_safe_walk_symlink_recursion(self): - tmp = mkdtemp(dir=TMP) - try: - if os.stat(tmp).st_ino == 0: - self.skipTest('inodes not supported in {0}'.format(tmp)) - os.mkdir(join(tmp, 'fax')) - os.makedirs(join(tmp, 'foo/bar')) - os.symlink('../..', join(tmp, 'foo/bar/baz')) - os.symlink('foo', join(tmp, 'root')) - expected = [ - (join(tmp, 'root'), ['bar'], []), - (join(tmp, 'root/bar'), ['baz'], []), - (join(tmp, 'root/bar/baz'), ['fax', 'foo', 'root'], []), - (join(tmp, 'root/bar/baz/fax'), [], []), - ] - paths = [] - for root, dirs, names in salt.utils.safe_walk(join(tmp, 'root')): - paths.append((root, sorted(dirs), names)) - if paths != expected: - raise AssertionError( - '\n'.join( - ['got:'] + [repr(p) for p in paths] + - ['', 'expected:'] + [repr(p) for p in expected] - ) - ) - finally: - rmtree(tmp) diff --git a/tests/unit/utils/test_stringutils.py b/tests/unit/utils/test_stringutils.py index 4c9249b7f4..142b492731 100644 --- a/tests/unit/utils/test_stringutils.py +++ b/tests/unit/utils/test_stringutils.py @@ -3,8 +3,7 @@ from __future__ import absolute_import # Import Salt libs -from tests.support.unit import TestCase -from tests.unit.utils.test_utils import LOREM_IPSUM +from tests.support.unit import TestCase, LOREM_IPSUM import salt.utils.stringutils # Import 3rd-party libs @@ -97,3 +96,9 @@ class StringutilsTestCase(TestCase): ut = '\xe4\xb8\xad\xe5\x9b\xbd\xe8\xaa\x9e (\xe7\xb9\x81\xe4\xbd\x93)' un = u'\u4e2d\u56fd\u8a9e (\u7e41\u4f53)' self.assertEqual(salt.utils.stringutils.to_unicode(ut, 'utf-8'), un) + + def test_build_whitespace_split_regex(self): + expected_regex = '(?m)^(?:[\\s]+)?Lorem(?:[\\s]+)?ipsum(?:[\\s]+)?dolor(?:[\\s]+)?sit(?:[\\s]+)?amet\\,' \ + '(?:[\\s]+)?$' + ret = salt.utils.stringutils.build_whitespace_split_regex(' '.join(LOREM_IPSUM.split()[:5])) + self.assertEqual(ret, expected_regex) diff --git a/tests/unit/utils/test_templates.py b/tests/unit/utils/test_templates.py new file mode 100644 index 0000000000..4b359cf18b --- /dev/null +++ b/tests/unit/utils/test_templates.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +''' +Tests for salt.utils.data +''' + +# Import Python libs +from __future__ import absolute_import +import textwrap + +# Import Salt libs +import salt.utils.templates +from tests.support.unit import TestCase, LOREM_IPSUM + + +class TemplatesTestCase(TestCase): + + def test_get_context(self): + expected_context = textwrap.dedent('''\ + --- + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eget urna a arcu lacinia sagittis. + Sed scelerisque, lacus eget malesuada vestibulum, justo diam facilisis tortor, in sodales dolor + [...] + ---''') + ret = salt.utils.templates.get_context(LOREM_IPSUM, 1, num_lines=1) + self.assertEqual(ret, expected_context) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py deleted file mode 100644 index 06d286eeff..0000000000 --- a/tests/unit/utils/test_utils.py +++ /dev/null @@ -1,419 +0,0 @@ -# -*- coding: utf-8 -*- -''' - :codeauthor: :email:`Mike Place ` -''' - -# Import python libs -from __future__ import absolute_import - -# Import Salt Testing libs -from tests.support.unit import TestCase, skipIf -from tests.support.mock import ( - patch, - NO_MOCK, - NO_MOCK_REASON -) - -# Import Salt libs -import salt.utils -import salt.utils.data -import salt.utils.jid -import salt.utils.process -import salt.utils.yamlencoding -import salt.utils.zeromq -from salt.exceptions import SaltSystemExit, CommandNotFoundError - -# Import Python libraries -import datetime -import os -import yaml -import zmq - -# Import 3rd-party libs -try: - import timelib # pylint: disable=import-error,unused-import - HAS_TIMELIB = True -except ImportError: - HAS_TIMELIB = False - -LOREM_IPSUM = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eget urna a arcu lacinia sagittis. \n' \ - 'Sed scelerisque, lacus eget malesuada vestibulum, justo diam facilisis tortor, in sodales dolor \n' \ - 'nibh eu urna. Aliquam iaculis massa risus, sed elementum risus accumsan id. Suspendisse mattis, \n' \ - 'metus sed lacinia dictum, leo orci dapibus sapien, at porttitor sapien nulla ac velit. \n' \ - 'Duis ac cursus leo, non varius metus. Sed laoreet felis magna, vel tempor diam malesuada nec. \n' \ - 'Quisque cursus odio tortor. In consequat augue nisl, eget lacinia odio vestibulum eget. \n' \ - 'Donec venenatis elementum arcu at rhoncus. Nunc pharetra erat in lacinia convallis. Ut condimentum \n' \ - 'eu mauris sit amet convallis. Morbi vulputate vel odio non laoreet. Nullam in suscipit tellus. \n' \ - 'Sed quis posuere urna.' - - -class UtilsTestCase(TestCase): - def test_get_context(self): - expected_context = '---\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eget urna a arcu ' \ - 'lacinia sagittis. \n' \ - 'Sed scelerisque, lacus eget malesuada vestibulum, justo diam facilisis tortor, in sodales' \ - ' dolor \n' \ - '[...]\n' \ - '---' - ret = salt.utils.get_context(LOREM_IPSUM, 1, num_lines=1) - self.assertEqual(ret, expected_context) - - def test_jid_to_time(self): - test_jid = 20131219110700123489 - expected_jid = '2013, Dec 19 11:07:00.123489' - self.assertEqual(salt.utils.jid.jid_to_time(test_jid), expected_jid) - - # Test incorrect lengths - incorrect_jid_length = 2012 - self.assertEqual(salt.utils.jid.jid_to_time(incorrect_jid_length), '') - - def test_is_jid(self): - self.assertTrue(salt.utils.jid.is_jid('20131219110700123489')) # Valid JID - self.assertFalse(salt.utils.jid.is_jid(20131219110700123489)) # int - self.assertFalse(salt.utils.jid.is_jid('2013121911070012348911111')) # Wrong length - - def test_build_whitespace_split_regex(self): - expected_regex = '(?m)^(?:[\\s]+)?Lorem(?:[\\s]+)?ipsum(?:[\\s]+)?dolor(?:[\\s]+)?sit(?:[\\s]+)?amet\\,' \ - '(?:[\\s]+)?$' - ret = salt.utils.build_whitespace_split_regex(' '.join(LOREM_IPSUM.split()[:5])) - self.assertEqual(ret, expected_regex) - - def test_mysql_to_dict(self): - test_mysql_output = ['+----+------+-----------+------+---------+------+-------+------------------+', - '| Id | User | Host | db | Command | Time | State | Info |', - '+----+------+-----------+------+---------+------+-------+------------------+', - '| 7 | root | localhost | NULL | Query | 0 | init | show processlist |', - '+----+------+-----------+------+---------+------+-------+------------------+'] - - ret = salt.utils.mysql_to_dict(test_mysql_output, 'Info') - expected_dict = { - 'show processlist': {'Info': 'show processlist', 'db': 'NULL', 'State': 'init', 'Host': 'localhost', - 'Command': 'Query', 'User': 'root', 'Time': 0, 'Id': 7}} - - self.assertDictEqual(ret, expected_dict) - - def test_subdict_match(self): - test_two_level_dict = {'foo': {'bar': 'baz'}} - test_two_level_comb_dict = {'foo': {'bar': 'baz:woz'}} - test_two_level_dict_and_list = { - 'abc': ['def', 'ghi', {'lorem': {'ipsum': [{'dolor': 'sit'}]}}], - } - test_three_level_dict = {'a': {'b': {'c': 'v'}}} - - self.assertTrue( - salt.utils.data.subdict_match( - test_two_level_dict, 'foo:bar:baz' - ) - ) - # In test_two_level_comb_dict, 'foo:bar' corresponds to 'baz:woz', not - # 'baz'. This match should return False. - self.assertFalse( - salt.utils.data.subdict_match( - test_two_level_comb_dict, 'foo:bar:baz' - ) - ) - # This tests matching with the delimiter in the value part (in other - # words, that the path 'foo:bar' corresponds to the string 'baz:woz'). - self.assertTrue( - salt.utils.data.subdict_match( - test_two_level_comb_dict, 'foo:bar:baz:woz' - ) - ) - # This would match if test_two_level_comb_dict['foo']['bar'] was equal - # to 'baz:woz:wiz', or if there was more deep nesting. But it does not, - # so this should return False. - self.assertFalse( - salt.utils.data.subdict_match( - test_two_level_comb_dict, 'foo:bar:baz:woz:wiz' - ) - ) - # This tests for cases when a key path corresponds to a list. The - # value part 'ghi' should be successfully matched as it is a member of - # the list corresponding to key path 'abc'. It is somewhat a - # duplication of a test within test_traverse_dict_and_list, but - # salt.utils.data.subdict_match() does more than just invoke - # salt.utils.traverse_list_and_dict() so this particular assertion is a - # sanity check. - self.assertTrue( - salt.utils.data.subdict_match( - test_two_level_dict_and_list, 'abc:ghi' - ) - ) - # This tests the use case of a dict embedded in a list, embedded in a - # list, embedded in a dict. This is a rather absurd case, but it - # confirms that match recursion works properly. - self.assertTrue( - salt.utils.data.subdict_match( - test_two_level_dict_and_list, 'abc:lorem:ipsum:dolor:sit' - ) - ) - # Test four level dict match for reference - self.assertTrue( - salt.utils.data.subdict_match( - test_three_level_dict, 'a:b:c:v' - ) - ) - self.assertFalse( - # Test regression in 2015.8 where 'a:c:v' would match 'a:b:c:v' - salt.utils.data.subdict_match( - test_three_level_dict, 'a:c:v' - ) - ) - # Test wildcard match - self.assertTrue( - salt.utils.data.subdict_match( - test_three_level_dict, 'a:*:c:v' - ) - ) - - def test_traverse_dict(self): - test_two_level_dict = {'foo': {'bar': 'baz'}} - - self.assertDictEqual( - {'not_found': 'nope'}, - salt.utils.data.traverse_dict( - test_two_level_dict, 'foo:bar:baz', {'not_found': 'nope'} - ) - ) - self.assertEqual( - 'baz', - salt.utils.data.traverse_dict( - test_two_level_dict, 'foo:bar', {'not_found': 'not_found'} - ) - ) - - def test_traverse_dict_and_list(self): - test_two_level_dict = {'foo': {'bar': 'baz'}} - test_two_level_dict_and_list = { - 'foo': ['bar', 'baz', {'lorem': {'ipsum': [{'dolor': 'sit'}]}}] - } - - # Check traversing too far: salt.utils.data.traverse_dict_and_list() returns - # the value corresponding to a given key path, and baz is a value - # corresponding to the key path foo:bar. - self.assertDictEqual( - {'not_found': 'nope'}, - salt.utils.data.traverse_dict_and_list( - test_two_level_dict, 'foo:bar:baz', {'not_found': 'nope'} - ) - ) - # Now check to ensure that foo:bar corresponds to baz - self.assertEqual( - 'baz', - salt.utils.data.traverse_dict_and_list( - test_two_level_dict, 'foo:bar', {'not_found': 'not_found'} - ) - ) - # Check traversing too far - self.assertDictEqual( - {'not_found': 'nope'}, - salt.utils.data.traverse_dict_and_list( - test_two_level_dict_and_list, 'foo:bar', {'not_found': 'nope'} - ) - ) - # Check index 1 (2nd element) of list corresponding to path 'foo' - self.assertEqual( - 'baz', - salt.utils.data.traverse_dict_and_list( - test_two_level_dict_and_list, 'foo:1', {'not_found': 'not_found'} - ) - ) - # Traverse a couple times into dicts embedded in lists - self.assertEqual( - 'sit', - salt.utils.data.traverse_dict_and_list( - test_two_level_dict_and_list, - 'foo:lorem:ipsum:dolor', - {'not_found': 'not_found'} - ) - ) - - def test_sanitize_win_path_string(self): - p = '\\windows\\system' - self.assertEqual(salt.utils.sanitize_win_path_string('\\windows\\system'), '\\windows\\system') - self.assertEqual(salt.utils.sanitize_win_path_string('\\bo:g|us\\p?at*h>'), '\\bo_g_us\\p_at_h_') - - @skipIf(NO_MOCK, NO_MOCK_REASON) - @skipIf(not hasattr(zmq, 'IPC_PATH_MAX_LEN'), "ZMQ does not have max length support.") - def test_check_ipc_length(self): - ''' - Ensure we throw an exception if we have a too-long IPC URI - ''' - with patch('zmq.IPC_PATH_MAX_LEN', 1): - self.assertRaises(SaltSystemExit, salt.utils.zeromq.check_ipc_path_max_len, '1' * 1024) - - def test_option(self): - test_two_level_dict = {'foo': {'bar': 'baz'}} - - self.assertDictEqual({'not_found': 'nope'}, salt.utils.option('foo:bar', {'not_found': 'nope'})) - self.assertEqual('baz', salt.utils.option('foo:bar', {'not_found': 'nope'}, opts=test_two_level_dict)) - self.assertEqual('baz', salt.utils.option('foo:bar', {'not_found': 'nope'}, pillar={'master': test_two_level_dict})) - self.assertEqual('baz', salt.utils.option('foo:bar', {'not_found': 'nope'}, pillar=test_two_level_dict)) - - @skipIf(NO_MOCK, NO_MOCK_REASON) - def test_date_cast(self): - now = datetime.datetime.now() - with patch('datetime.datetime'): - datetime.datetime.now.return_value = now - self.assertEqual(now, salt.utils.date_cast(None)) - self.assertEqual(now, salt.utils.date_cast(now)) - try: - ret = salt.utils.date_cast('Mon Dec 23 10:19:15 MST 2013') - expected_ret = datetime.datetime(2013, 12, 23, 10, 19, 15) - self.assertEqual(ret, expected_ret) - except RuntimeError: - # Unparseable without timelib installed - self.skipTest('\'timelib\' is not installed') - - @skipIf(not HAS_TIMELIB, '\'timelib\' is not installed') - def test_date_format(self): - - # Taken from doctests - - expected_ret = '2002-12-25' - - src = datetime.datetime(2002, 12, 25, 12, 00, 00, 00) - ret = salt.utils.date_format(src) - self.assertEqual(ret, expected_ret) - - src = '2002/12/25' - ret = salt.utils.date_format(src) - self.assertEqual(ret, expected_ret) - - src = 1040814000 - ret = salt.utils.date_format(src) - self.assertEqual(ret, expected_ret) - - src = '1040814000' - ret = salt.utils.date_format(src) - self.assertEqual(ret, expected_ret) - - def test_yaml_dquote(self): - for teststr in (r'"\ []{}"',): - self.assertEqual(teststr, yaml.safe_load(salt.utils.yamlencoding.yaml_dquote(teststr))) - - def test_yaml_dquote_doesNotAddNewLines(self): - teststr = '"' * 100 - self.assertNotIn('\n', salt.utils.yamlencoding.yaml_dquote(teststr)) - - def test_yaml_squote(self): - ret = salt.utils.yamlencoding.yaml_squote(r'"') - self.assertEqual(ret, r"""'"'""") - - def test_yaml_squote_doesNotAddNewLines(self): - teststr = "'" * 100 - self.assertNotIn('\n', salt.utils.yamlencoding.yaml_squote(teststr)) - - def test_yaml_encode(self): - for testobj in (None, True, False, '[7, 5]', '"monkey"', 5, 7.5, "2014-06-02 15:30:29.7"): - self.assertEqual(testobj, yaml.safe_load(salt.utils.yamlencoding.yaml_encode(testobj))) - - for testobj in ({}, [], set()): - self.assertRaises(TypeError, salt.utils.yamlencoding.yaml_encode, testobj) - - def test_compare_dicts(self): - ret = salt.utils.data.compare_dicts(old={'foo': 'bar'}, new={'foo': 'bar'}) - self.assertEqual(ret, {}) - - ret = salt.utils.data.compare_dicts(old={'foo': 'bar'}, new={'foo': 'woz'}) - expected_ret = {'foo': {'new': 'woz', 'old': 'bar'}} - self.assertDictEqual(ret, expected_ret) - - def test_decode_list(self): - test_data = [u'unicode_str', [u'unicode_item_in_list', 'second_item_in_list'], {'dict_key': u'dict_val'}] - expected_ret = ['unicode_str', ['unicode_item_in_list', 'second_item_in_list'], {'dict_key': 'dict_val'}] - ret = salt.utils.data.decode_list(test_data) - self.assertEqual(ret, expected_ret) - - def test_decode_dict(self): - test_data = {u'test_unicode_key': u'test_unicode_val', - 'test_list_key': ['list_1', u'unicode_list_two'], - u'test_dict_key': {'test_sub_dict_key': 'test_sub_dict_val'}} - expected_ret = {'test_unicode_key': 'test_unicode_val', - 'test_list_key': ['list_1', 'unicode_list_two'], - 'test_dict_key': {'test_sub_dict_key': 'test_sub_dict_val'}} - ret = salt.utils.data.decode_dict(test_data) - self.assertDictEqual(ret, expected_ret) - - def test_find_json(self): - test_sample_json = ''' - { - "glossary": { - "title": "example glossary", - "GlossDiv": { - "title": "S", - "GlossList": { - "GlossEntry": { - "ID": "SGML", - "SortAs": "SGML", - "GlossTerm": "Standard Generalized Markup Language", - "Acronym": "SGML", - "Abbrev": "ISO 8879:1986", - "GlossDef": { - "para": "A meta-markup language, used to create markup languages such as DocBook.", - "GlossSeeAlso": ["GML", "XML"] - }, - "GlossSee": "markup" - } - } - } - } - } - ''' - expected_ret = {'glossary': {'GlossDiv': {'GlossList': {'GlossEntry': { - 'GlossDef': {'GlossSeeAlso': ['GML', 'XML'], - 'para': 'A meta-markup language, used to create markup languages such as DocBook.'}, - 'GlossSee': 'markup', 'Acronym': 'SGML', 'GlossTerm': 'Standard Generalized Markup Language', - 'SortAs': 'SGML', - 'Abbrev': 'ISO 8879:1986', 'ID': 'SGML'}}, 'title': 'S'}, 'title': 'example glossary'}} - - # First test the valid JSON - ret = salt.utils.find_json(test_sample_json) - self.assertDictEqual(ret, expected_ret) - - # Now pre-pend some garbage and re-test - garbage_prepend_json = '{0}{1}'.format(LOREM_IPSUM, test_sample_json) - ret = salt.utils.find_json(garbage_prepend_json) - self.assertDictEqual(ret, expected_ret) - - # Test to see if a ValueError is raised if no JSON is passed in - self.assertRaises(ValueError, salt.utils.find_json, LOREM_IPSUM) - - def test_repack_dict(self): - list_of_one_element_dicts = [{'dict_key_1': 'dict_val_1'}, - {'dict_key_2': 'dict_val_2'}, - {'dict_key_3': 'dict_val_3'}] - expected_ret = {'dict_key_1': 'dict_val_1', - 'dict_key_2': 'dict_val_2', - 'dict_key_3': 'dict_val_3'} - ret = salt.utils.data.repack_dictlist(list_of_one_element_dicts) - self.assertDictEqual(ret, expected_ret) - - # Try with yaml - yaml_key_val_pair = '- key1: val1' - ret = salt.utils.data.repack_dictlist(yaml_key_val_pair) - self.assertDictEqual(ret, {'key1': 'val1'}) - - # Make sure we handle non-yaml junk data - ret = salt.utils.data.repack_dictlist(LOREM_IPSUM) - self.assertDictEqual(ret, {}) - - @skipIf(NO_MOCK, NO_MOCK_REASON) - def test_gen_jid(self): - now = datetime.datetime(2002, 12, 25, 12, 00, 00, 00) - with patch('datetime.datetime'): - datetime.datetime.now.return_value = now - ret = salt.utils.jid.gen_jid({}) - self.assertEqual(ret, '20021225120000000000') - salt.utils.jid.LAST_JID_DATETIME = None - ret = salt.utils.jid.gen_jid({'unique_jid': True}) - self.assertEqual(ret, '20021225120000000000_{0}'.format(os.getpid())) - ret = salt.utils.jid.gen_jid({'unique_jid': True}) - self.assertEqual(ret, '20021225120000000001_{0}'.format(os.getpid())) - - @skipIf(NO_MOCK, NO_MOCK_REASON) - def test_check_or_die(self): - self.assertRaises(CommandNotFoundError, salt.utils.check_or_die, None) - - with patch('salt.utils.path.which', return_value=False): - self.assertRaises(CommandNotFoundError, salt.utils.check_or_die, 'FAKE COMMAND') diff --git a/tests/unit/utils/test_yamlencoding.py b/tests/unit/utils/test_yamlencoding.py new file mode 100644 index 0000000000..984c5377be --- /dev/null +++ b/tests/unit/utils/test_yamlencoding.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +''' +Tests for salt.utils.yamlencoding +''' + +# Import Python libs +from __future__ import absolute_import +import yaml + +# Import Salt libs +import salt.utils.yamlencoding +from tests.support.unit import TestCase + + +class YamlEncodingTestCase(TestCase): + + def test_yaml_dquote(self): + for teststr in (r'"\ []{}"',): + self.assertEqual(teststr, yaml.safe_load(salt.utils.yamlencoding.yaml_dquote(teststr))) + + def test_yaml_dquote_doesNotAddNewLines(self): + teststr = '"' * 100 + self.assertNotIn('\n', salt.utils.yamlencoding.yaml_dquote(teststr)) + + def test_yaml_squote(self): + ret = salt.utils.yamlencoding.yaml_squote(r'"') + self.assertEqual(ret, r"""'"'""") + + def test_yaml_squote_doesNotAddNewLines(self): + teststr = "'" * 100 + self.assertNotIn('\n', salt.utils.yamlencoding.yaml_squote(teststr)) + + def test_yaml_encode(self): + for testobj in (None, True, False, '[7, 5]', '"monkey"', 5, 7.5, "2014-06-02 15:30:29.7"): + self.assertEqual(testobj, yaml.safe_load(salt.utils.yamlencoding.yaml_encode(testobj))) + + for testobj in ({}, [], set()): + self.assertRaises(TypeError, salt.utils.yamlencoding.yaml_encode, testobj) diff --git a/tests/unit/utils/test_zeromq.py b/tests/unit/utils/test_zeromq.py index 2835359f2a..a6bf509d0c 100644 --- a/tests/unit/utils/test_zeromq.py +++ b/tests/unit/utils/test_zeromq.py @@ -5,12 +5,19 @@ Test salt.utils.zeromq # Import Python libs from __future__ import absolute_import +import zmq # Import Salt Testing libs -from tests.support.unit import TestCase +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + patch, + NO_MOCK, + NO_MOCK_REASON +) # Import salt libs import salt.utils.zeromq +from salt.exceptions import SaltSystemExit class UtilsTestCase(TestCase): @@ -19,3 +26,12 @@ class UtilsTestCase(TestCase): test_ipv6 = '::1' self.assertEqual(test_ipv4, salt.utils.zeromq.ip_bracket(test_ipv4)) self.assertEqual('[{0}]'.format(test_ipv6), salt.utils.zeromq.ip_bracket(test_ipv6)) + + @skipIf(NO_MOCK, NO_MOCK_REASON) + @skipIf(not hasattr(zmq, 'IPC_PATH_MAX_LEN'), "ZMQ does not have max length support.") + def test_check_ipc_length(self): + ''' + Ensure we throw an exception if we have a too-long IPC URI + ''' + with patch('zmq.IPC_PATH_MAX_LEN', 1): + self.assertRaises(SaltSystemExit, salt.utils.zeromq.check_ipc_path_max_len, '1' * 1024) From be94aca2c362c20bc3af01488a837a0246175e6a Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Mon, 16 Oct 2017 15:16:33 +0330 Subject: [PATCH 608/633] show_all remove show_all=True, removed from beacon execution module so it should remove here also --- salt/states/beacon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/states/beacon.py b/salt/states/beacon.py index 64d1905dc2..c134bfce05 100644 --- a/salt/states/beacon.py +++ b/salt/states/beacon.py @@ -166,7 +166,7 @@ def enabled(name, **kwargs): 'changes': {}, 'comment': []} - current_beacons = __salt__['beacons.list'](show_all=True, return_yaml=False) + current_beacons = __salt__['beacons.list'](return_yaml=False) if name in current_beacons: if 'test' in __opts__ and __opts__['test']: kwargs['test'] = True @@ -204,7 +204,7 @@ def disabled(name, **kwargs): 'changes': {}, 'comment': []} - current_beacons = __salt__['beacons.list'](show_all=True, return_yaml=False) + current_beacons = __salt__['beacons.list'](return_yaml=False) if name in current_beacons: if 'test' in __opts__ and __opts__['test']: kwargs['test'] = True From 7d21d4253aea70fb303db8dee60ff5cec6d7549c Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 16 Oct 2017 10:30:45 -0400 Subject: [PATCH 609/633] Reduce the number of days an issue is stale by 25 --- .github/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 38e95f8702..2aa60bdc61 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,8 +1,8 @@ # Probot Stale configuration file # Number of days of inactivity before an issue becomes stale -# 975 is approximately 2 years and 8 months -daysUntilStale: 975 +# 950 is approximately 2 years and 7 months +daysUntilStale: 950 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 From 16e1c1dfc86920b7a00dbf7c39b805c359e4d13b Mon Sep 17 00:00:00 2001 From: Matthew Summers Date: Mon, 16 Oct 2017 09:47:40 -0500 Subject: [PATCH 610/633] fixed test addressing issue #43307, disk.format_ to disk.format --- tests/unit/states/test_blockdev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/states/test_blockdev.py b/tests/unit/states/test_blockdev.py index e5899f1c70..9b559dddfe 100644 --- a/tests/unit/states/test_blockdev.py +++ b/tests/unit/states/test_blockdev.py @@ -100,7 +100,7 @@ class BlockdevTestCase(TestCase, LoaderModuleMockMixin): # Test state return when block device format fails with patch.dict(blockdev.__salt__, {'cmd.run': MagicMock(return_value=mock_ext4), - 'disk.format_': MagicMock(return_value=True)}): + 'disk.format': MagicMock(return_value=True)}): comt = ('Failed to format {0}'.format(name)) ret.update({'comment': comt, 'result': False}) with patch.object(salt.utils, 'which', From 1319c822bdbd2320c4809a4fedb517bcc92e556d Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Mon, 16 Oct 2017 17:23:23 +0200 Subject: [PATCH 611/633] Fixed code snippet in unit testing doc --- doc/topics/development/tests/unit.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/topics/development/tests/unit.rst b/doc/topics/development/tests/unit.rst index d90c770178..27622b9805 100644 --- a/doc/topics/development/tests/unit.rst +++ b/doc/topics/development/tests/unit.rst @@ -123,7 +123,7 @@ to the module being tests one should do: } Consider this more extensive example from -``tests/unit/modules/test_libcloud_dns.py``:: +``tests/unit/modules/test_libcloud_dns.py``: .. code-block:: python @@ -319,7 +319,7 @@ function into ``__salt__`` that's actually a MagicMock instance. def show_patch(self): with patch.dict(my_module.__salt__, - {'function.to_replace': MagicMock()}: + {'function.to_replace': MagicMock()}): # From this scope, carry on with testing, with a modified __salt__! From a4e2d8059d9f957b854dadfafe5d8cf5eba52391 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 4 Oct 2017 15:10:58 -0600 Subject: [PATCH 612/633] Fix `unit.templates.test_jinja` for Windows Fix problem with files that end with new line on Windows Fix some problems with regex Fix some unicode conversion issues --- salt/utils/templates.py | 9 +++++++-- tests/unit/templates/test_jinja.py | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/salt/utils/templates.py b/salt/utils/templates.py index b4bf049dc1..e6da2c119e 100644 --- a/salt/utils/templates.py +++ b/salt/utils/templates.py @@ -168,8 +168,13 @@ def wrap_tmpl_func(render_str): if six.PY2: output = output.encode(SLS_ENCODING) if salt.utils.is_windows(): + newline = False + if output.endswith(('\n', os.linesep)): + newline = True # Write out with Windows newlines output = os.linesep.join(output.splitlines()) + if newline: + output += os.linesep except SaltRenderError as exc: log.error("Rendering exception occurred: {0}".format(exc)) @@ -293,7 +298,7 @@ def render_jinja_tmpl(tmplstr, context, tmplpath=None): # http://jinja.pocoo.org/docs/api/#unicode tmplstr = tmplstr.decode(SLS_ENCODING) - if tmplstr.endswith('\n'): + if tmplstr.endswith(os.linesep): newline = True if not saltenv: @@ -462,7 +467,7 @@ def render_jinja_tmpl(tmplstr, context, tmplpath=None): # Workaround a bug in Jinja that removes the final newline # (https://github.com/mitsuhiko/jinja2/issues/75) if newline: - output += '\n' + output += os.linesep return output diff --git a/tests/unit/templates/test_jinja.py b/tests/unit/templates/test_jinja.py index 7a96608825..4ad4b618f8 100644 --- a/tests/unit/templates/test_jinja.py +++ b/tests/unit/templates/test_jinja.py @@ -177,7 +177,7 @@ class TestGetTemplate(TestCase): out = render_jinja_tmpl( fp_.read(), dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) - self.assertEqual(out, 'world\n') + self.assertEqual(out, 'world' + os.linesep) def test_fallback_noloader(self): ''' @@ -189,7 +189,7 @@ class TestGetTemplate(TestCase): out = render_jinja_tmpl( fp_.read(), dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) - self.assertEqual(out, 'Hey world !a b !\n') + self.assertEqual(out, 'Hey world !a b !' + os.linesep) def test_saltenv(self): ''' @@ -208,7 +208,7 @@ class TestGetTemplate(TestCase): 'file_roots': self.local_opts['file_roots'], 'pillar_roots': self.local_opts['pillar_roots']}, a='Hi', b='Salt', saltenv='test', salt=self.local_salt)) - self.assertEqual(out, 'Hey world !Hi Salt !\n') + self.assertEqual(out, 'Hey world !Hi Salt !' + os.linesep) self.assertEqual(fc.requests[0]['path'], 'salt://macro') def test_macro_additional_log_for_generalexc(self): @@ -217,7 +217,7 @@ class TestGetTemplate(TestCase): more output from trace. ''' expected = r'''Jinja error:.*division.* -.*/macrogeneral\(2\): +.*macrogeneral\(2\): --- \{% macro mymacro\(\) -%\} \{\{ 1/0 \}\} <====================== @@ -241,7 +241,7 @@ class TestGetTemplate(TestCase): more output from trace. ''' expected = r'''Jinja variable 'b' is undefined -.*/macroundefined\(2\): +.*macroundefined\(2\): --- \{% macro mymacro\(\) -%\} \{\{b.greetee\}\} <-- error is here <====================== @@ -264,7 +264,7 @@ class TestGetTemplate(TestCase): If we failed in a macro, get more output from trace. ''' expected = r'''Jinja syntax error: expected token .*end.*got '-'.* -.*/macroerror\(2\): +.*macroerror\(2\): --- # macro \{% macro mymacro\(greeting, greetee='world'\) -\} <-- error is here <====================== @@ -294,7 +294,7 @@ class TestGetTemplate(TestCase): 'file_roots': self.local_opts['file_roots'], 'pillar_roots': self.local_opts['pillar_roots']}, a='Hi', b='Sàlt', saltenv='test', salt=self.local_salt)) - self.assertEqual(out, u'Hey world !Hi Sàlt !\n') + self.assertEqual(out, salt.utils.to_unicode('Hey world !Hi Sàlt !' + os.linesep)) self.assertEqual(fc.requests[0]['path'], 'salt://macro') filename = os.path.join(TEMPLATES_DIR, 'files', 'test', 'non_ascii') @@ -305,7 +305,7 @@ class TestGetTemplate(TestCase): 'file_roots': self.local_opts['file_roots'], 'pillar_roots': self.local_opts['pillar_roots']}, a='Hi', b='Sàlt', saltenv='test', salt=self.local_salt)) - self.assertEqual(u'Assunção\n', out) + self.assertEqual(u'Assunção' + os.linesep, out) self.assertEqual(fc.requests[0]['path'], 'salt://macro') @skipIf(HAS_TIMELIB is False, 'The `timelib` library is not installed.') @@ -340,8 +340,8 @@ class TestGetTemplate(TestCase): with salt.utils.fopen(out['data']) as fp: result = fp.read() if six.PY2: - result = result.decode('utf-8') - self.assertEqual(u'Assunção\n', result) + result = salt.utils.to_unicode(result) + self.assertEqual(salt.utils.to_unicode('Assunção' + os.linesep), result) def test_get_context_has_enough_context(self): template = '1\n2\n3\n4\n5\n6\n7\n8\n9\na\nb\nc\nd\ne\nf' From 7fe23503657bb02586fb6f2501d16f2f782c7c7f Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 20 Sep 2017 15:38:54 -0600 Subject: [PATCH 613/633] Add support for multimaster setup Allows you to pass a comma-delimited list of masters to the master combo box or the /master command line switch Parses the existing minion config file to get multimaster settings Adds an checkbox to use the existing config in the minion config page When the box is checked, show the existing config grayed out When unchecked, show default values Adss the /use-existing-config= command line switch for use from the command line Adds support for a help switch on the command line (/?) to display the supported command line paramaters --- pkg/windows/installer/Salt-Minion-Setup.nsi | 355 ++++++++++++++++++-- 1 file changed, 334 insertions(+), 21 deletions(-) diff --git a/pkg/windows/installer/Salt-Minion-Setup.nsi b/pkg/windows/installer/Salt-Minion-Setup.nsi index a8efca2101..783ac57147 100644 --- a/pkg/windows/installer/Salt-Minion-Setup.nsi +++ b/pkg/windows/installer/Salt-Minion-Setup.nsi @@ -11,6 +11,7 @@ !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" !define PRODUCT_UNINST_KEY_OTHER "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME_OTHER}" !define PRODUCT_UNINST_ROOT_KEY "HKLM" +!define OUTFILE "Salt-Minion-${PRODUCT_VERSION}-Py${PYTHON_VERSION}-${CPUARCH}-Setup.exe" # Import Libraries !include "MUI2.nsh" @@ -52,6 +53,15 @@ ${StrStrAdv} Pop "${ResultVar}" !macroend +# Part of the Explode function for Strings +!define Explode "!insertmacro Explode" +!macro Explode Length Separator String + Push `${Separator}` + Push `${String}` + Call Explode + Pop `${Length}` +!macroend + ############################################################################### # Configure Pages, Ordering, and Configuration @@ -92,10 +102,17 @@ Var Dialog Var Label Var CheckBox_Minion_Start Var CheckBox_Minion_Start_Delayed +Var ConfigMasterHost Var MasterHost Var MasterHost_State +Var ConfigMinionName Var MinionName Var MinionName_State +Var ExistingConfigFound +Var UseExistingConfig +Var UseExistingConfig_State +Var WarningExistingConfig +Var WarningDefaultConfig Var StartMinion Var StartMinionDelayed Var DeleteInstallDir @@ -115,27 +132,105 @@ Function pageMinionConfig Abort ${EndIf} + # Master IP or Hostname Dialog Control ${NSD_CreateLabel} 0 0 100% 12u "Master IP or Hostname:" Pop $Label ${NSD_CreateText} 0 13u 100% 12u $MasterHost_State Pop $MasterHost + # Minion ID Dialog Control ${NSD_CreateLabel} 0 30u 100% 12u "Minion Name:" Pop $Label ${NSD_CreateText} 0 43u 100% 12u $MinionName_State Pop $MinionName + # Use Existing Config Checkbox + ${NSD_CreateCheckBox} 0 65u 100% 12u "&Use Existing Config" + Pop $UseExistingConfig + ${NSD_OnClick} $UseExistingConfig pageMinionConfig_OnClick + + # Add Existing Config Warning Label + ${NSD_CreateLabel} 0 80u 100% 60u "The values above are taken from an \ + existing configuration found in `c:\salt\conf\minion`. Configuration \ + settings defined in the `minion.d` directories, if they exist, are not \ + shown here.$\r$\n\ + $\r$\n\ + Clicking `Install` will leave the existing config unchanged." + Pop $WarningExistingConfig + CreateFont $0 "Arial" 10 500 /ITALIC + SendMessage $WarningExistingConfig ${WM_SETFONT} $0 1 + SetCtlColors $WarningExistingConfig 0xBB0000 transparent + + # Add Default Config Warning Label + ${NSD_CreateLabel} 0 80u 100% 60u "Clicking `Install` will remove the \ + the existing minion config file and remove the minion.d directories. \ + The values above will be used in the new default config." + Pop $WarningDefaultConfig + CreateFont $0 "Arial" 10 500 /ITALIC + SendMessage $WarningDefaultConfig ${WM_SETFONT} $0 1 + SetCtlColors $WarningDefaultConfig 0xBB0000 transparent + + # If no existing config found, disable the checkbox and stuff + # Set UseExistingConfig_State to 0 + ${If} $ExistingConfigFound == 0 + StrCpy $UseExistingConfig_State 0 + ShowWindow $UseExistingConfig ${SW_HIDE} + ShowWindow $WarningExistingConfig ${SW_HIDE} + ShowWindow $WarningDefaultConfig ${SW_HIDE} + ${Endif} + + ${NSD_SetState} $UseExistingConfig $UseExistingConfig_State + + Call pageMinionConfig_OnClick + nsDialogs::Show FunctionEnd +Function pageMinionConfig_OnClick + + # You have to pop the top handle to keep the stack clean + Pop $R0 + + # Assign the current checkbox state to the variable + ${NSD_GetState} $UseExistingConfig $UseExistingConfig_State + + # Validate the checkboxes + ${If} $UseExistingConfig_State == ${BST_CHECKED} + # Use Existing Config is checked, show warning + ShowWindow $WarningExistingConfig ${SW_SHOW} + EnableWindow $MasterHost 0 + EnableWindow $MinionName 0 + ${NSD_SetText} $MasterHost $ConfigMasterHost + ${NSD_SetText} $MinionName $ConfigMinionName + ${If} $ExistingConfigFound == 1 + ShowWindow $WarningDefaultConfig ${SW_HIDE} + ${Endif} + ${Else} + # Use Existing Config is not checked, hide the warning + ShowWindow $WarningExistingConfig ${SW_HIDE} + EnableWindow $MasterHost 1 + EnableWindow $MinionName 1 + ${NSD_SetText} $MasterHost $MasterHost_State + ${NSD_SetText} $MinionName $MinionName_State + ${If} $ExistingConfigFound == 1 + ShowWindow $WarningDefaultConfig ${SW_SHOW} + ${Endif} + ${EndIf} + +FunctionEnd + + Function pageMinionConfig_Leave ${NSD_GetText} $MasterHost $MasterHost_State ${NSD_GetText} $MinionName $MinionName_State + ${NSD_GetState} $UseExistingConfig $UseExistingConfig_State + + Call RemoveExistingConfig FunctionEnd @@ -194,7 +289,7 @@ FunctionEnd !else Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" !endif -OutFile "Salt-Minion-${PRODUCT_VERSION}-Py${PYTHON_VERSION}-${CPUARCH}-Setup.exe" +OutFile "${OutFile}" InstallDir "c:\salt" InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" "" ShowInstDetails show @@ -311,8 +406,6 @@ SectionEnd Function .onInit - Call getMinionConfig - Call parseCommandLineSwitches # Check for existing installation @@ -364,6 +457,23 @@ Function .onInit skipUninstall: + Call getMinionConfig + + IfSilent 0 +2 + Call RemoveExistingConfig + +FunctionEnd + + +Function RemoveExistingConfig + + ${If} $ExistingConfigFound == 1 + ${AndIf} $UseExistingConfig_State == 0 + # Wipe out the Existing Config + Delete "$INSTDIR\conf\minion" + RMDir /r "$INSTDIR\conf\minion.d" + ${EndIf} + FunctionEnd @@ -407,7 +517,9 @@ Section -Post nsExec::Exec "nssm.exe set salt-minion AppStopMethodConsole 24000" nsExec::Exec "nssm.exe set salt-minion AppStopMethodWindow 2000" - Call updateMinionConfig + ${If} $UseExistingConfig_State == 0 + Call updateMinionConfig + ${EndIf} Push "C:\salt" Call AddToPath @@ -534,18 +646,28 @@ FunctionEnd # Helper Functions ############################################################################### Function MsiQueryProductState + # Used for detecting VCRedist Installation + !define INSTALLSTATE_DEFAULT "5" - !define INSTALLSTATE_DEFAULT "5" - - Pop $R0 - StrCpy $NeedVcRedist "False" - System::Call "msi::MsiQueryProductStateA(t '$R0') i.r0" - StrCmp $0 ${INSTALLSTATE_DEFAULT} +2 0 - StrCpy $NeedVcRedist "True" + Pop $R0 + StrCpy $NeedVcRedist "False" + System::Call "msi::MsiQueryProductStateA(t '$R0') i.r0" + StrCmp $0 ${INSTALLSTATE_DEFAULT} +2 0 + StrCpy $NeedVcRedist "True" FunctionEnd +#------------------------------------------------------------------------------ +# Trim Function +# - Trim whitespace from the beginning and end of a string +# - Trims spaces, \r, \n, \t +# +# Usage: +# Push " some string " +# Call Trim +# Pop $0 ; "some string" +#------------------------------------------------------------------------------ Function Trim Exch $R1 # Original string @@ -580,6 +702,79 @@ Function Trim FunctionEnd +Function Explode + # Initialize variables + Var /GLOBAL explString + Var /GLOBAL explSeparator + Var /GLOBAL explStrLen + Var /GLOBAL explSepLen + Var /GLOBAL explOffset + Var /GLOBAL explTmp + Var /GLOBAL explTmp2 + Var /GLOBAL explTmp3 + Var /GLOBAL explArrCount + + # Get input from user + Pop $explString + Pop $explSeparator + + # Calculates initial values + StrLen $explStrLen $explString + StrLen $explSepLen $explSeparator + StrCpy $explArrCount 1 + + ${If} $explStrLen <= 1 # If we got a single character + ${OrIf} $explSepLen > $explStrLen # or separator is larger than the string, + Push $explString # then we return initial string with no change + Push 1 # and set array's length to 1 + Return + ${EndIf} + + # Set offset to the last symbol of the string + StrCpy $explOffset $explStrLen + IntOp $explOffset $explOffset - 1 + + # Clear temp string to exclude the possibility of appearance of occasional data + StrCpy $explTmp "" + StrCpy $explTmp2 "" + StrCpy $explTmp3 "" + + # Loop until the offset becomes negative + ${Do} + # If offset becomes negative, it is time to leave the function + ${IfThen} $explOffset == -1 ${|} ${ExitDo} ${|} + + # Remove everything before and after the searched part ("TempStr") + StrCpy $explTmp $explString $explSepLen $explOffset + + ${If} $explTmp == $explSeparator + # Calculating offset to start copy from + IntOp $explTmp2 $explOffset + $explSepLen # Offset equals to the current offset plus length of separator + StrCpy $explTmp3 $explString "" $explTmp2 + + Push $explTmp3 # Throwing array item to the stack + IntOp $explArrCount $explArrCount + 1 # Increasing array's counter + + StrCpy $explString $explString $explOffset 0 # Cutting all characters beginning with the separator entry + StrLen $explStrLen $explString + ${EndIf} + + ${If} $explOffset = 0 # If the beginning of the line met and there is no separator, + # copying the rest of the string + ${If} $explSeparator == "" # Fix for the empty separator + IntOp $explArrCount $explArrCount - 1 + ${Else} + Push $explString + ${EndIf} + ${EndIf} + + IntOp $explOffset $explOffset - 1 + ${Loop} + + Push $explArrCount +FunctionEnd + + #------------------------------------------------------------------------------ # StrStr Function # - find substring in a string @@ -816,6 +1011,9 @@ FunctionEnd ############################################################################### Function getMinionConfig + # Set Config Found Default Value + StrCpy $ExistingConfigFound 0 + confFind: IfFileExists "$INSTDIR\conf\minion" confFound confNotFound @@ -828,24 +1026,42 @@ Function getMinionConfig ${EndIf} confFound: + StrCpy $ExistingConfigFound 1 FileOpen $0 "$INSTDIR\conf\minion" r - ClearErrors confLoop: - FileRead $0 $1 - IfErrors EndOfFile - ${StrLoc} $2 $1 "master:" ">" - ${If} $2 == 0 - ${StrStrAdv} $2 $1 "master: " ">" ">" "0" "0" "0" - ${Trim} $2 $2 - StrCpy $MasterHost_State $2 + ClearErrors # Clear Errors + FileRead $0 $1 # Read the next line + IfErrors EndOfFile # Error is probably EOF + ${StrLoc} $2 $1 "master:" ">" # Find `master:` starting at the beginning + ${If} $2 == 0 # If it found it in the first position, then it is defined + ${StrStrAdv} $2 $1 "master: " ">" ">" "0" "0" "0" # Read everything after `master: ` + ${Trim} $2 $2 # Trim white space + ${If} $2 == "" # If it's empty, it's probably a list + masterLoop: + ClearErrors # Clear Errors + FileRead $0 $1 # Read the next line + IfErrors EndOfFile # Error is probably EOF + ${StrStrAdv} $2 $1 "- " ">" ">" "0" "0" "0" # Read everything after `- ` + ${Trim} $2 $2 # Trim white space + ${IfNot} $2 == "" # If it's not empty, we found something + ${If} $ConfigMasterHost == "" # Is the default `salt` there + StrCpy $ConfigMasterHost $2 # If so, make the first item the new entry + ${Else} + StrCpy $ConfigMasterHost "$ConfigMasterHost,$2" # Append the new master, comma separated + ${EndIf} + Goto masterLoop # Check the next one + ${EndIf} + ${Else} + StrCpy $ConfigMasterHost $2 # A single master entry ${EndIf} + ${EndIf} ${StrLoc} $2 $1 "id:" ">" ${If} $2 == 0 ${StrStrAdv} $2 $1 "id: " ">" ">" "0" "0" "0" ${Trim} $2 $2 - StrCpy $MinionName_State $2 + StrCpy $ConfigMinionName $2 ${EndIf} Goto confLoop @@ -855,6 +1071,14 @@ Function getMinionConfig confReallyNotFound: + # Set Default Config Values if not found + ${If} $ConfigMasterHost == "" + StrCpy $ConfigMasterHost "salt" + ${EndIf} + ${If} $ConfigMinionName == "" + StrCpy $ConfigMinionName "hostname" + ${EndIf} + FunctionEnd @@ -869,12 +1093,28 @@ Function updateMinionConfig FileRead $0 $2 # read line from target file IfErrors done # end if errors are encountered (end of line) + ${If} $MasterHost_State != "" # if master is empty ${AndIf} $MasterHost_State != "salt" # and if master is not 'salt' ${StrLoc} $3 $2 "master:" ">" # where is 'master:' in this line ${If} $3 == 0 # is it in the first... ${OrIf} $3 == 1 # or second position (account for comments) - StrCpy $2 "master: $MasterHost_State$\r$\n" # write the master + + ${Explode} $9 "," $MasterHost_state + ${If} $9 == 1 + StrCpy $2 "master: $MasterHost_State$\r$\n" # write the master + ${Else} + StrCpy $2 "master:" + + loop_explode: + pop $8 + ${Trim} $8 $8 + StrCpy $2 "$2$\r$\n - $8" + IntOp $9 $9 - 1 + ${If} $9 >= 1 + Goto loop_explode + ${EndIf} + ${EndIf} # close if statement ${EndIf} # close if statement ${EndIf} # close if statement @@ -905,6 +1145,67 @@ Function parseCommandLineSwitches # Load the parameters ${GetParameters} $R0 + # Display Help + ClearErrors + ${GetOptions} $R0 "/?" $R1 + IfErrors display_help_not_found + + System::Call 'kernel32::GetStdHandle(i -11)i.r0' + System::Call 'kernel32::AttachConsole(i -1)i.r1' + ${If} $0 = 0 + ${OrIf} $1 = 0 + System::Call 'kernel32::AllocConsole()' + System::Call 'kernel32::GetStdHandle(i -11)i.r0' + ${EndIf} + FileWrite $0 "$\n" + FileWrite $0 "$\n" + FileWrite $0 "Help for Salt Minion installation$\n" + FileWrite $0 "===============================================================================$\n" + FileWrite $0 "$\n" + FileWrite $0 "/minion-name=$\t$\tA string value to set the minion name. Default is$\n" + FileWrite $0 "$\t$\t$\t'hostname'. Setting the minion name will replace$\n" + FileWrite $0 "$\t$\t$\texisting config with a default config. Cannot be$\n" + FileWrite $0 "$\t$\t$\tused in conjunction with /use-existing-config=1$\n" + FileWrite $0 "$\n" + FileWrite $0 "/master=$\t$\tA string value to set the IP address or hostname of$\n" + FileWrite $0 "$\t$\t$\tthe master. Default value is 'salt'. You may pass a$\n" + FileWrite $0 "$\t$\t$\tsingle master, or a comma separated list of masters.$\n" + FileWrite $0 "$\t$\t$\tSetting the master will replace existing config with$\n" + FileWrite $0 "$\t$\t$\ta default config. Cannot be used in conjunction with$\n" + FileWrite $0 "$\t$\t$\t/use-existing-config=1$\n" + FileWrite $0 "$\n" + FileWrite $0 "/start-minion=$\t$\t1 will start the service, 0 will not. Default is 1$\n" + FileWrite $0 "$\n" + FileWrite $0 "/start-minion-delayed$\tSet the minion start type to 'Automatic (Delayed Start)'$\n" + FileWrite $0 "$\n" + FileWrite $0 "/use-existing-config=$\t1 will use the existing config if present, 0 will$\n" + FileWrite $0 "$\t$\t$\treplace existing config with a default config. Default$\n" + FileWrite $0 "$\t$\t$\tis 1. If this is set to 1, values passed in$\n" + FileWrite $0 "$\t$\t$\t/minion-name and /master will be ignored$\n" + FileWrite $0 "$\n" + FileWrite $0 "/S$\t$\t$\tInstall Salt silently$\n" + FileWrite $0 "$\n" + FileWrite $0 "/?$\t$\t$\tDisplay this help screen$\n" + FileWrite $0 "$\n" + FileWrite $0 "-------------------------------------------------------------------------------$\n" + FileWrite $0 "$\n" + FileWrite $0 "Examples:$\n" + FileWrite $0 "$\n" + FileWrite $0 "${OutFile} /S$\n" + FileWrite $0 "$\n" + FileWrite $0 "${OutFile} /S /minion-name=myminion /master=master.mydomain.com /start-minion-delayed$\n" + FileWrite $0 "$\n" + FileWrite $0 "===============================================================================$\n" + FileWrite $0 "Press Enter to continue..." + System::Free $0 + System::Free $1 + System::Call 'kernel32::FreeConsole()' + Abort + display_help_not_found: + + # Set default value for Use Existing Config + StrCpy $UseExistingConfig_State 1 + # Check for start-minion switches # /start-service is to be deprecated, so we must check for both ${GetOptions} $R0 "/start-service=" $R1 @@ -930,19 +1231,31 @@ Function parseCommandLineSwitches start_minion_delayed_not_found: # Minion Config: Master IP/Name + # If setting master, we don't want to use existing config ${GetOptions} $R0 "/master=" $R1 ${IfNot} $R1 == "" StrCpy $MasterHost_State $R1 + StrCpy $UseExistingConfig_State 0 ${ElseIf} $MasterHost_State == "" StrCpy $MasterHost_State "salt" ${EndIf} # Minion Config: Minion ID + # If setting minion id, we don't want to use existing config ${GetOptions} $R0 "/minion-name=" $R1 ${IfNot} $R1 == "" StrCpy $MinionName_State $R1 + StrCpy $UseExistingConfig_State 0 ${ElseIf} $MinionName_State == "" StrCpy $MinionName_State "hostname" ${EndIf} + # Use Existing Config + # Overrides above settings with user passed settings + ${GetOptions} $R0 "/use-existing-config=" $R1 + ${IfNot} $R1 == "" + # Use Existing Config was passed something, set it + StrCpy $UseExistingConfig_State $R1 + ${EndIf} + FunctionEnd From e9075725dc0a7f7f4a95c92984b404c5fc4580cc Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 21 Sep 2017 14:11:21 -0600 Subject: [PATCH 614/633] Add documenting comments --- pkg/windows/installer/Salt-Minion-Setup.nsi | 49 ++++++++++++++------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/pkg/windows/installer/Salt-Minion-Setup.nsi b/pkg/windows/installer/Salt-Minion-Setup.nsi index 783ac57147..6283d057a7 100644 --- a/pkg/windows/installer/Salt-Minion-Setup.nsi +++ b/pkg/windows/installer/Salt-Minion-Setup.nsi @@ -664,9 +664,13 @@ FunctionEnd # - Trims spaces, \r, \n, \t # # Usage: -# Push " some string " +# Push " some string " ; String to Trim # Call Trim -# Pop $0 ; "some string" +# Pop $0 ; Trimmed String: "some string" +# +# or +# +# ${Trim} $0 $1 ; Trimmed String, String to Trim #------------------------------------------------------------------------------ Function Trim @@ -702,6 +706,22 @@ Function Trim FunctionEnd +#------------------------------------------------------------------------------ +# Explode Function +# - Splits a string based off the passed separator +# - Each item in the string is pushed to the stack +# - The last item pushed to the stack is the length of the array +# +# Usage: +# Push "," ; Separator +# Push "string,to,separate" ; String to explode +# Call Explode +# Pop $0 ; Number of items in the array +# +# or +# +# ${Explode} $0 $1 $2 ; Length, Separator, String +#------------------------------------------------------------------------------ Function Explode # Initialize variables Var /GLOBAL explString @@ -1093,27 +1113,26 @@ Function updateMinionConfig FileRead $0 $2 # read line from target file IfErrors done # end if errors are encountered (end of line) - ${If} $MasterHost_State != "" # if master is empty ${AndIf} $MasterHost_State != "salt" # and if master is not 'salt' ${StrLoc} $3 $2 "master:" ">" # where is 'master:' in this line ${If} $3 == 0 # is it in the first... ${OrIf} $3 == 1 # or second position (account for comments) - ${Explode} $9 "," $MasterHost_state - ${If} $9 == 1 + ${Explode} $9 "," $MasterHost_state # Split the hostname on commas, $9 is the number of items found + ${If} $9 == 1 # 1 means only a single master was passed StrCpy $2 "master: $MasterHost_State$\r$\n" # write the master - ${Else} - StrCpy $2 "master:" + ${Else} # Make a multi-master entry + StrCpy $2 "master:" # Make the first line "master:" - loop_explode: - pop $8 - ${Trim} $8 $8 - StrCpy $2 "$2$\r$\n - $8" - IntOp $9 $9 - 1 - ${If} $9 >= 1 - Goto loop_explode - ${EndIf} + loop_explode: # Start a loop to go through the list in the config + pop $8 # Pop the next item off the stack + ${Trim} $8 $8 # Trim any whitespace + StrCpy $2 "$2$\r$\n - $8" # Add it to the master variable ($2) + IntOp $9 $9 - 1 # Decrement the list count + ${If} $9 >= 1 # If it's not 0 + Goto loop_explode # Do it again + ${EndIf} # close if statement ${EndIf} # close if statement ${EndIf} # close if statement ${EndIf} # close if statement From 7bcf8b48ecaa75b3da2aa13efbc871e2f81cbb40 Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 21 Sep 2017 15:16:10 -0600 Subject: [PATCH 615/633] Update documentation with new changes to installer --- doc/topics/installation/windows.rst | 49 ++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/doc/topics/installation/windows.rst b/doc/topics/installation/windows.rst index 06219e73e6..68ded4a510 100644 --- a/doc/topics/installation/windows.rst +++ b/doc/topics/installation/windows.rst @@ -45,11 +45,27 @@ but leave any existing config, cache, and PKI information. Salt Minion Installation ======================== +If the system is missing the appropriate version of the Visual C++ +Redistributable (vcredist) the user will be prompted to install it. Click ``OK`` +to install the vcredist. Click ``Cancel`` to abort the installation without +making modifications to the systeml. + +If Salt is already installed on the system the user will be prompted to remove +the previous installation. Click ``OK`` to uninstall Salt without removing the +configuration, PKI information, or cached files. Click ``Cancel`` to abort the +installation before making any modifications to the system. + After the Welcome and the License Agreement, the installer asks for two bits of information to configure the minion; the master hostname and the minion name. -The installer will update the minion config with these options. If the installer -finds an existing minion config file, these fields will be populated with values -from the existing config. +The installer will update the minion config with these options. + +If the installer finds an existing minion config file, these fields will be +populated with values from the existing config, but they will be grayed out. +There will also be a checkbox to use existing config. If you continue, the +existing config will be used. If the checkbox is unchecked, default values are +displayed and can be changed. If you continue the existing config file in +``c:\salt\conf`` will be removed along with the ``c:\salt\conf\minion.d` +directory. The values entered will be used with the default config. The final page allows you to start the minion service and optionally change its startup type. By default, the minion is set to ``Automatic``. You can change the @@ -71,11 +87,6 @@ be managed there or from the command line like any other Windows service. sc start salt-minion net start salt-minion -.. note:: - If the minion won't start, you may need to install the Microsoft Visual C++ - 2008 x64 SP1 redistributable. Allow all Windows updates to run salt-minion - smoothly. - Installation Prerequisites -------------------------- @@ -96,15 +107,29 @@ Minion silently: ========================= ===================================================== Option Description ========================= ===================================================== -``/minion-name=`` A string value to set the minion name. Default is - 'hostname' ``/master=`` A string value to set the IP address or host name of - the master. Default value is 'salt' + the master. Default value is 'salt'. You can pass a + single master or a comma-separated list of masters. + Setting the master will replace existing config with + the default config. Cannot be used in conjunction + with ``/use-existing-config`` +``/minion-name=`` A string value to set the minion name. Default is + 'hostname'. Setting the minion name will replace + existing config with the default config. Cannot be + used in conjunction with ``/use-existing-config`` ``/start-minion=`` Either a 1 or 0. '1' will start the salt-minion service, '0' will not. Default is to start the - service after installation. + service after installation ``/start-minion-delayed`` Set the minion start type to ``Automatic (Delayed Start)`` +``/use-existing-config`` Either a 1 or 0. '1' will use the existing config if + present. '0' will replace existing config with the + default config. Default is '1'. If this is set to '1' + values passed in ``/master`` and ``/minion-name`` + will be ignored +``/S`` Runs the installation silently. Uses the above + settings or the defaults +``/?`` Displays command line help ========================= ===================================================== .. note:: From 78124181cd2b1bee2cabbe4b1de990589b56b779 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 22 Sep 2017 12:23:11 -0600 Subject: [PATCH 616/633] Fix typo --- doc/topics/installation/windows.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/topics/installation/windows.rst b/doc/topics/installation/windows.rst index 68ded4a510..080ee59a9a 100644 --- a/doc/topics/installation/windows.rst +++ b/doc/topics/installation/windows.rst @@ -48,7 +48,7 @@ Salt Minion Installation If the system is missing the appropriate version of the Visual C++ Redistributable (vcredist) the user will be prompted to install it. Click ``OK`` to install the vcredist. Click ``Cancel`` to abort the installation without -making modifications to the systeml. +making modifications to the system. If Salt is already installed on the system the user will be prompted to remove the previous installation. Click ``OK`` to uninstall Salt without removing the From e22b2a48564ee841f1d2098bf7a16d265ccc863e Mon Sep 17 00:00:00 2001 From: twangboy Date: Mon, 25 Sep 2017 09:28:48 -0600 Subject: [PATCH 617/633] Fix some grammer issues in the docs --- doc/topics/installation/windows.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/topics/installation/windows.rst b/doc/topics/installation/windows.rst index 080ee59a9a..f2ec33e122 100644 --- a/doc/topics/installation/windows.rst +++ b/doc/topics/installation/windows.rst @@ -61,9 +61,9 @@ The installer will update the minion config with these options. If the installer finds an existing minion config file, these fields will be populated with values from the existing config, but they will be grayed out. -There will also be a checkbox to use existing config. If you continue, the +There will also be a checkbox to use the existing config. If you continue, the existing config will be used. If the checkbox is unchecked, default values are -displayed and can be changed. If you continue the existing config file in +displayed and can be changed. If you continue, the existing config file in ``c:\salt\conf`` will be removed along with the ``c:\salt\conf\minion.d` directory. The values entered will be used with the default config. From 601d8bc5af2764f678a0cafc5fd551ba45eb3492 Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 26 Sep 2017 14:33:23 -0600 Subject: [PATCH 618/633] Add release notes --- doc/topics/releases/oxygen.rst | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index ffe489b903..74e559e994 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -116,11 +116,36 @@ The ``state_output`` parameter now supports ``full_id``, ``changes_id`` and ``te Just like ``mixed_id``, these use the state ID as name in the highstate output. For more information on these output modes, see the docs for the :mod:`Highstate Outputter `. +Windows Installer: Changes to existing config handling +------------------------------------------------------ +Behavior with existing configuration has changed. With previous installers the +existing config was used and the master and minion id could be modified via the +installer. It was problematic in that it didn't account for configuration that +may be defined in the ``minion.d`` directory. This change gives you the option +via a checkbox to either use the existing config with out changes or the default +config using values you pass to the installer. If you choose to use the existing +config then no changes are made. If not, the existing config is deleted, to +include the ``minion.d`` directory, and the default config is used. A +command-line switch (``/use-existing-config``) has also been added to control +this behavior. + +Windows Installer: Multi-master configuration +--------------------------------------------- +The installer now has the ability to apply a multi-master configuration either +from the gui or the command line. The ``master`` field in the gui can accept +either a single master or a comma-separated list of masters. The command-line +switch (``/master=``) can accept the same. + +Windows Installer: Command-line help +------------------------------------ +The Windows installer will now display command-line help when a help switch +(``/?``) is passed. + Salt Cloud Features -------------------- +=================== Pre-Flight Commands -=================== +------------------- Support has been added for specified "preflight commands" to run on a VM before the deploy script is run. These must be defined as a list in a cloud configuration @@ -344,7 +369,7 @@ Solaris Logical Domains In Virtual Grain ---------------------------------------- Support has been added to the ``virtual`` grain for detecting Solaris LDOMs -running on T-Series SPARC hardware. The ``virtual_subtype`` grain is +running on T-Series SPARC hardware. The ``virtual_subtype`` grain is populated as a list of domain roles. Lists of comments in state returns @@ -359,7 +384,7 @@ Beacon configuration changes In order to remain consistent and to align with other Salt components such as states, support for configuring beacons using dictionary based configuration has been deprecated -in favor of list based configuration. All beacons have a validation function which will +in favor of list based configuration. All beacons have a validation function which will check the configuration for the correct format and only load if the validation passes. - ``avahi_announce`` beacon From a0972704e57be428779e0be5b40ef7baa80cb447 Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 26 Sep 2017 15:11:20 -0600 Subject: [PATCH 619/633] Fix bad headings --- doc/topics/releases/oxygen.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/topics/releases/oxygen.rst b/doc/topics/releases/oxygen.rst index 74e559e994..45e00c86ae 100644 --- a/doc/topics/releases/oxygen.rst +++ b/doc/topics/releases/oxygen.rst @@ -142,10 +142,10 @@ The Windows installer will now display command-line help when a help switch (``/?``) is passed. Salt Cloud Features -=================== +------------------- Pre-Flight Commands -------------------- +=================== Support has been added for specified "preflight commands" to run on a VM before the deploy script is run. These must be defined as a list in a cloud configuration From 473fbb18e9fb1d6d6b02a0cdd0cd2d5477924d8b Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 16 Oct 2017 13:38:41 -0500 Subject: [PATCH 620/633] Add top-level docstrings to a bunch of salt.utils modules which are missing them --- salt/utils/__init__.py | 13 +++++++++---- salt/utils/cache.py | 3 +++ salt/utils/configcomparer.py | 4 ++-- salt/utils/configparser.py | 3 +++ salt/utils/crypt.py | 3 +++ salt/utils/data.py | 4 ++++ salt/utils/dateutils.py | 3 +++ salt/utils/decorators/signature.py | 5 ++++- salt/utils/doc.py | 4 ++++ salt/utils/files.py | 3 +++ salt/utils/fsutils.py | 6 +++--- salt/utils/functools.py | 3 +++ salt/utils/gitfs.py | 3 +++ salt/utils/jid.py | 4 ++++ salt/utils/job.py | 3 +++ salt/utils/json.py | 3 +++ salt/utils/lazy.py | 3 +++ salt/utils/mako.py | 3 +++ salt/utils/oset.py | 4 ++-- salt/utils/process.py | 3 +++ salt/utils/profile.py | 3 +++ salt/utils/reactor.py | 4 ++++ salt/utils/stringutils.py | 3 +++ salt/utils/timed_subprocess.py | 4 +++- salt/utils/timeout.py | 12 +++++------- salt/utils/user.py | 4 ++++ salt/utils/yamlencoding.py | 3 +++ salt/utils/yamlloader.py | 4 ++++ salt/utils/zeromq.py | 4 +++- 29 files changed, 100 insertions(+), 21 deletions(-) diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 3b0bcd0af3..2d6191ba4c 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -22,6 +22,7 @@ from salt.ext import six # DEPRECATED FUNCTIONS # # These are not referenced anywhere in the codebase and are slated for removal. +# def option(value, default='', opts=None, pillar=None): ''' Pass in a generic option and receive the value that will be assigned @@ -110,7 +111,10 @@ def required_modules_error(name, docstring): # # MOVED FUNCTIONS # -# These are deprecated and will be removed in Neon. +# These functions have been moved to new locations. The functions below are +# convenience functions which will allow the old function locations to continue +# to work. The convenience functions will be removed in the Neon release. +# def get_accumulator_dir(cachedir): # Late import to avoid circular import. import salt.utils.versions @@ -118,7 +122,7 @@ def get_accumulator_dir(cachedir): salt.utils.versions.warn_until( 'Neon', 'Use of \'salt.utils.get_accumulator_dir\' detected. This function ' - 'has been moved to \'salt.state.accumulator_dir\' as of ' + 'has been moved to \'salt.state.get_accumulator_dir\' as of ' 'Salt Oxygen. This warning will be removed in Salt Neon.' ) return salt.state.get_accumulator_dir(cachedir) @@ -1648,8 +1652,9 @@ def get_values_of_matching_keys(pattern_dict, user_name): salt.utils.versions.warn_until( 'Neon', 'Use of \'salt.utils.get_values_of_matching_keys\' detected. ' - 'This function has been moved to \'salt.utils.master.get_master_key\' ' - 'as of Salt Oxygen. This warning will be removed in Salt Neon.' + 'This function has been moved to ' + '\'salt.utils.master.get_values_of_matching_keys\' as of Salt Oxygen. ' + 'This warning will be removed in Salt Neon.' ) return salt.utils.master.get_values_of_matching_keys(pattern_dict, user_name) diff --git a/salt/utils/cache.py b/salt/utils/cache.py index fad2eeb10e..32ccb23688 100644 --- a/salt/utils/cache.py +++ b/salt/utils/cache.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +In-memory caching used by Salt +''' # Import Python libs from __future__ import absolute_import, print_function import os diff --git a/salt/utils/configcomparer.py b/salt/utils/configcomparer.py index b7a0f279ff..60b7cf427a 100644 --- a/salt/utils/configcomparer.py +++ b/salt/utils/configcomparer.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -""" +''' Utilities for comparing and updating configurations while keeping track of changes in a way that can be easily reported in a state. -""" +''' # Import Python libs from __future__ import absolute_import diff --git a/salt/utils/configparser.py b/salt/utils/configparser.py index d060f9ddd2..6a0023073f 100644 --- a/salt/utils/configparser.py +++ b/salt/utils/configparser.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Custom configparser classes +''' # Import Python libs from __future__ import absolute_import import re diff --git a/salt/utils/crypt.py b/salt/utils/crypt.py index 1f643c8127..86a74e6e98 100644 --- a/salt/utils/crypt.py +++ b/salt/utils/crypt.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Functions dealing with encryption +''' from __future__ import absolute_import diff --git a/salt/utils/data.py b/salt/utils/data.py index 4dac969e2a..b3b8aa2ff9 100644 --- a/salt/utils/data.py +++ b/salt/utils/data.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +''' +Functions for manipulating, inspecting, or otherwise working with data types +and data structures. +''' from __future__ import absolute_import diff --git a/salt/utils/dateutils.py b/salt/utils/dateutils.py index f5fd9f44cf..941086432d 100644 --- a/salt/utils/dateutils.py +++ b/salt/utils/dateutils.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Convenience functions for dealing with datetime classes +''' from __future__ import absolute_import, division diff --git a/salt/utils/decorators/signature.py b/salt/utils/decorators/signature.py index a9e7662618..96b7f993d1 100644 --- a/salt/utils/decorators/signature.py +++ b/salt/utils/decorators/signature.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- - +''' +A decorator which returns a function with the same signature of the function +which is being wrapped. +''' # Import Python libs from __future__ import absolute_import import inspect diff --git a/salt/utils/doc.py b/salt/utils/doc.py index f28b2f3c7f..53a162383e 100644 --- a/salt/utils/doc.py +++ b/salt/utils/doc.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +''' +Functions for analyzing/parsing docstrings +''' + from __future__ import absolute_import import re from salt.ext import six diff --git a/salt/utils/files.py b/salt/utils/files.py index d9e76218cf..64c7a55878 100644 --- a/salt/utils/files.py +++ b/salt/utils/files.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Functions for working with files +''' from __future__ import absolute_import diff --git a/salt/utils/fsutils.py b/salt/utils/fsutils.py index 049ab6c0a2..df96020b47 100644 --- a/salt/utils/fsutils.py +++ b/salt/utils/fsutils.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# -# Copyright (C) 2014 SUSE LLC - ''' Run-time utilities ''' +# +# Copyright (C) 2014 SUSE LLC + # Import Python libs from __future__ import absolute_import diff --git a/salt/utils/functools.py b/salt/utils/functools.py index 4dbada06f2..c41a6d5710 100644 --- a/salt/utils/functools.py +++ b/salt/utils/functools.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Utility functions to modify other functions +''' from __future__ import absolute_import diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py index ad1154d1e5..fe20fce89a 100644 --- a/salt/utils/gitfs.py +++ b/salt/utils/gitfs.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Classes which provide the shared base for GitFS, git_pillar, and winrepo +''' # Import python libs from __future__ import absolute_import diff --git a/salt/utils/jid.py b/salt/utils/jid.py index f8a0664e22..c8dd40902f 100644 --- a/salt/utils/jid.py +++ b/salt/utils/jid.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +''' +Functions for creating and working with job IDs +''' + from __future__ import absolute_import from __future__ import print_function diff --git a/salt/utils/job.py b/salt/utils/job.py index a10098019a..d0f61e06f7 100644 --- a/salt/utils/job.py +++ b/salt/utils/job.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Functions for interacting with the job cache +''' # Import Python libs from __future__ import absolute_import diff --git a/salt/utils/json.py b/salt/utils/json.py index ab05bc6a86..e9f5e0969a 100644 --- a/salt/utils/json.py +++ b/salt/utils/json.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Functions to work with JSON +''' from __future__ import absolute_import diff --git a/salt/utils/lazy.py b/salt/utils/lazy.py index 78b6310ced..a5f96953a4 100644 --- a/salt/utils/lazy.py +++ b/salt/utils/lazy.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Lazily-evaluated data structures, primarily used by Salt's loader +''' # Import Python Libs from __future__ import absolute_import diff --git a/salt/utils/mako.py b/salt/utils/mako.py index 11edffe0cc..6266112f0f 100644 --- a/salt/utils/mako.py +++ b/salt/utils/mako.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Functions for working with Mako templates +''' from __future__ import absolute_import # Import python libs diff --git a/salt/utils/oset.py b/salt/utils/oset.py index 677181f307..4dbe08c7f3 100644 --- a/salt/utils/oset.py +++ b/salt/utils/oset.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -""" +''' Available at repository https://github.com/LuminosoInsight/ordered-set @@ -20,7 +20,7 @@ Rob Speer's changes are as follows: - index() just returns the index of an item - added a __getstate__ and __setstate__ so it can be pickled - added __getitem__ -""" +''' from __future__ import absolute_import import collections diff --git a/salt/utils/process.py b/salt/utils/process.py index f156145a4c..eccf859f72 100644 --- a/salt/utils/process.py +++ b/salt/utils/process.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Functions for daemonizing and otherwise modifying running processes +''' # Import python libs from __future__ import absolute_import, with_statement diff --git a/salt/utils/profile.py b/salt/utils/profile.py index ce8267ca37..6edcd0fbd3 100644 --- a/salt/utils/profile.py +++ b/salt/utils/profile.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Decorator and functions to profile Salt using cProfile +''' from __future__ import absolute_import diff --git a/salt/utils/reactor.py b/salt/utils/reactor.py index e5021ec6fe..debb23598e 100644 --- a/salt/utils/reactor.py +++ b/salt/utils/reactor.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +''' +Functions which implement running reactor jobs +''' + # Import python libs from __future__ import absolute_import diff --git a/salt/utils/stringutils.py b/salt/utils/stringutils.py index d30cfb1b0a..694734c8d8 100644 --- a/salt/utils/stringutils.py +++ b/salt/utils/stringutils.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Functions for manipulating or otherwise processing strings +''' # Import Python libs from __future__ import absolute_import, print_function diff --git a/salt/utils/timed_subprocess.py b/salt/utils/timed_subprocess.py index d77f3ca0cc..cf7613898e 100644 --- a/salt/utils/timed_subprocess.py +++ b/salt/utils/timed_subprocess.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -'''For running command line executables with a timeout''' +''' +For running command line executables with a timeout +''' from __future__ import absolute_import import shlex diff --git a/salt/utils/timeout.py b/salt/utils/timeout.py index 6e3b01ca15..9837b3a47f 100644 --- a/salt/utils/timeout.py +++ b/salt/utils/timeout.py @@ -5,16 +5,14 @@ import time log = logging.getLogger(__name__) +# To give us some leeway when making time-calculations BLUR_FACTOR = 0.95 -""" -To give us some leeway when making time-calculations -""" def wait_for(func, timeout=10, step=1, default=None, func_args=(), func_kwargs=None): - """ - Call `func` at regular intervals and Waits until the given function returns a truthy value - within the given timeout and returns that value. + ''' + Call `func` at regular intervals and Waits until the given function returns + a truthy value within the given timeout and returns that value. @param func: @type func: function @@ -29,7 +27,7 @@ def wait_for(func, timeout=10, step=1, default=None, func_args=(), func_kwargs=N @param func_kwargs: **kwargs for `func` @type func_kwargs: dict @return: `default` or result of `func` - """ + ''' if func_kwargs is None: func_kwargs = dict() max_time = time.time() + timeout diff --git a/salt/utils/user.py b/salt/utils/user.py index 134a5658ec..737f944c85 100644 --- a/salt/utils/user.py +++ b/salt/utils/user.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +''' +Functions for querying and modifying a user account and the groups to which it +belongs. +''' from __future__ import absolute_import diff --git a/salt/utils/yamlencoding.py b/salt/utils/yamlencoding.py index 15e35b83da..2f8ac8fb32 100644 --- a/salt/utils/yamlencoding.py +++ b/salt/utils/yamlencoding.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +''' +Functions for adding yaml encoding to the jinja context +''' # Import Python libs from __future__ import absolute_import diff --git a/salt/utils/yamlloader.py b/salt/utils/yamlloader.py index 6fe3a3fa1b..b439302b77 100644 --- a/salt/utils/yamlloader.py +++ b/salt/utils/yamlloader.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +''' +Custom YAML loading in Salt +''' + # Import python libs from __future__ import absolute_import import warnings diff --git a/salt/utils/zeromq.py b/salt/utils/zeromq.py index 892b23bbaf..251c05bee2 100644 --- a/salt/utils/zeromq.py +++ b/salt/utils/zeromq.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- - +''' +ZMQ-specific functions +''' # Import Python libs from __future__ import absolute_import From a685f458ed7d5f5f3edf7a8c263b3c94ef41c6da Mon Sep 17 00:00:00 2001 From: Pratik Bandarkar Date: Sat, 14 Oct 2017 00:58:58 +0000 Subject: [PATCH 621/633] add "auto_delete" option to "attach_disk" and "create_attach_volumes" for GCP. fixes #44101 - This fix will allow user to specify "auto_delete" option with attach_disk or "create_attach_volumes" function --- salt/cloud/clouds/gce.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/salt/cloud/clouds/gce.py b/salt/cloud/clouds/gce.py index 6e26cf8b95..8f4325c41b 100644 --- a/salt/cloud/clouds/gce.py +++ b/salt/cloud/clouds/gce.py @@ -2080,6 +2080,7 @@ def attach_disk(name=None, kwargs=None, call=None): disk_name = kwargs['disk_name'] mode = kwargs.get('mode', 'READ_WRITE').upper() boot = kwargs.get('boot', False) + auto_delete = kwargs.get('auto_delete', False) if boot and boot.lower() in ['true', 'yes', 'enabled']: boot = True else: @@ -2109,7 +2110,8 @@ def attach_disk(name=None, kwargs=None, call=None): transport=__opts__['transport'] ) - result = conn.attach_volume(node, disk, ex_mode=mode, ex_boot=boot) + result = conn.attach_volume(node, disk, ex_mode=mode, ex_boot=boot, + ex_auto_delete=auto_delete) __utils__['cloud.fire_event']( 'event', @@ -2389,6 +2391,8 @@ def create_attach_volumes(name, kwargs, call=None): 'type': The disk type, either pd-standard or pd-ssd. Optional, defaults to pd-standard. 'image': An image to use for this new disk. Optional. 'snapshot': A snapshot to use for this new disk. Optional. + 'auto_delete': An option(bool) to keep or remove the disk upon + instance deletion. Optional, defaults to False. Volumes are attached in the order in which they are given, thus on a new node the first volume will be /dev/sdb, the second /dev/sdc, and so on. @@ -2415,7 +2419,8 @@ def create_attach_volumes(name, kwargs, call=None): 'size': volume['size'], 'type': volume['type'], 'image': volume['image'], - 'snapshot': volume['snapshot'] + 'snapshot': volume['snapshot'], + 'auto_delete': volume.get('auto_delete', False) } create_disk(volume_dict, 'function') From 54c4be35ae93ebdb707fce69d5dab4552435f5c9 Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 16 Oct 2017 15:47:32 -0400 Subject: [PATCH 622/633] Update any moved utils references from the merge-forward --- salt/modules/kmod.py | 2 +- salt/utils/jinja.py | 3 ++- tests/unit/utils/test_parsers.py | 10 +++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/salt/modules/kmod.py b/salt/modules/kmod.py index 4bfdb920ac..75efc9e001 100644 --- a/salt/modules/kmod.py +++ b/salt/modules/kmod.py @@ -128,7 +128,7 @@ def available(): built_in_file = os.path.join(mod_dir, 'modules.builtin') if os.path.exists(built_in_file): - with salt.utils.fopen(built_in_file, 'r') as f: + with salt.utils.files.fopen(built_in_file, 'r') as f: for line in f: # Strip .ko from the basename ret.append(os.path.basename(line)[:-4]) diff --git a/salt/utils/jinja.py b/salt/utils/jinja.py index ec24821e25..d4192ab4c8 100644 --- a/salt/utils/jinja.py +++ b/salt/utils/jinja.py @@ -28,6 +28,7 @@ from jinja2.ext import Extension # Import salt libs import salt.fileclient +import salt.utils.data import salt.utils.files import salt.utils.url from salt.utils.decorators.jinja import jinja_filter, jinja_test, jinja_global @@ -582,7 +583,7 @@ def symmetric_difference(lst1, lst2): @jinja2.contextfunction def show_full_context(ctx): - return salt.utils.simple_types_filter({key: value for key, value in ctx.items()}) + return salt.utils.data.simple_types_filter({key: value for key, value in ctx.items()}) class SerializerExtension(Extension, object): diff --git a/tests/unit/utils/test_parsers.py b/tests/unit/utils/test_parsers.py index af4e594f55..767f67ed95 100644 --- a/tests/unit/utils/test_parsers.py +++ b/tests/unit/utils/test_parsers.py @@ -17,11 +17,11 @@ from tests.support.mock import ( ) # Import Salt Libs -import salt.utils.parsers import salt.log.setup as log import salt.config import salt.syspaths -import salt.utils +import salt.utils.parsers +import salt.utils.platform class ErrorMock(object): # pylint: disable=too-few-public-methods @@ -490,7 +490,7 @@ class LogSettingsParserTests(TestCase): @skipIf(NO_MOCK, NO_MOCK_REASON) -@skipIf(salt.utils.is_windows(), 'Windows uses a logging listener') +@skipIf(salt.utils.platform.is_windows(), 'Windows uses a logging listener') class MasterOptionParserTestCase(LogSettingsParserTests): ''' Tests parsing Salt Master options @@ -517,7 +517,7 @@ class MasterOptionParserTestCase(LogSettingsParserTests): @skipIf(NO_MOCK, NO_MOCK_REASON) -@skipIf(salt.utils.is_windows(), 'Windows uses a logging listener') +@skipIf(salt.utils.platform.is_windows(), 'Windows uses a logging listener') class MinionOptionParserTestCase(LogSettingsParserTests): ''' Tests parsing Salt Minion options @@ -571,7 +571,7 @@ class ProxyMinionOptionParserTestCase(LogSettingsParserTests): @skipIf(NO_MOCK, NO_MOCK_REASON) -@skipIf(salt.utils.is_windows(), 'Windows uses a logging listener') +@skipIf(salt.utils.platform.is_windows(), 'Windows uses a logging listener') class SyndicOptionParserTestCase(LogSettingsParserTests): ''' Tests parsing Salt Syndic options From 3e962252108c86b760f8204c1d58642055a3eaca Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Sun, 15 Oct 2017 14:59:17 -0500 Subject: [PATCH 623/633] Use one salt.utils.gitfs.GitFS instance per thread This reduces some of the overhead caused by many concurrent fileclient requests from minions. Additionally, initializing remotes has been folded into the GitBase class' dunder init. Instantiating an instance and initializing its remotes were initially split so that an object could simply be created with the master opts so that the configuration was loaded and nothing else, allowing for certain cases (like clearing the cache files) where we didn't need to actually initialize the remotes. But this both A) presents a problem when the instance being used is a singleton, as you don't want to be re-initializing the remotes all the time, and B) suppressing initialization can be (and is now being) done via a new argument to the dunder init. --- salt/daemons/masterapi.py | 9 ++- salt/fileserver/gitfs.py | 68 ++++++------------- salt/master.py | 8 +-- salt/pillar/__init__.py | 8 +-- salt/pillar/git_pillar.py | 15 ++-- salt/runners/cache.py | 30 ++++---- salt/runners/git_pillar.py | 9 +-- salt/runners/winrepo.py | 11 +-- salt/utils/gitfs.py | 102 +++++++++++++++++++++++++--- tests/unit/fileserver/test_gitfs.py | 87 +++++++++++++++--------- tests/unit/utils/test_gitfs.py | 24 ++++--- 11 files changed, 227 insertions(+), 144 deletions(-) diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index 4d5a8a1c06..12efc0c935 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -69,12 +69,11 @@ def init_git_pillar(opts): for opts_dict in [x for x in opts.get('ext_pillar', [])]: if 'git' in opts_dict: try: - pillar = salt.utils.gitfs.GitPillar(opts) - pillar.init_remotes( + pillar = salt.utils.gitfs.GitPillar( + opts, opts_dict['git'], - git_pillar.PER_REMOTE_OVERRIDES, - git_pillar.PER_REMOTE_ONLY - ) + per_remote_overrides=git_pillar.PER_REMOTE_OVERRIDES, + per_remote_only=git_pillar.PER_REMOTE_ONLY) ret.append(pillar) except FileserverConfigError: if opts.get('git_pillar_verify_config', True): diff --git a/salt/fileserver/gitfs.py b/salt/fileserver/gitfs.py index 1182ec69be..477e6c40b2 100644 --- a/salt/fileserver/gitfs.py +++ b/salt/fileserver/gitfs.py @@ -71,6 +71,15 @@ log = logging.getLogger(__name__) __virtualname__ = 'git' +def _gitfs(init_remotes=True): + return salt.utils.gitfs.GitFS( + __opts__, + __opts__['gitfs_remotes'], + per_remote_overrides=PER_REMOTE_OVERRIDES, + per_remote_only=PER_REMOTE_ONLY, + init_remotes=init_remotes) + + def __virtual__(): ''' Only load if the desired provider module is present and gitfs is enabled @@ -79,7 +88,7 @@ def __virtual__(): if __virtualname__ not in __opts__['fileserver_backend']: return False try: - salt.utils.gitfs.GitFS(__opts__) + _gitfs(init_remotes=False) # Initialization of the GitFS object did not fail, so we know we have # valid configuration syntax and that a valid provider was detected. return __virtualname__ @@ -92,18 +101,14 @@ def clear_cache(): ''' Completely clear gitfs cache ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - return gitfs.clear_cache() + return _gitfs(init_remotes=False).clear_cache() def clear_lock(remote=None, lock_type='update'): ''' Clear update.lk ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - return gitfs.clear_lock(remote=remote, lock_type=lock_type) + return _gitfs().clear_lock(remote=remote, lock_type=lock_type) def lock(remote=None): @@ -114,30 +119,21 @@ def lock(remote=None): information, or a pattern. If the latter, then remotes for which the URL matches the pattern will be locked. ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - return gitfs.lock(remote=remote) + return _gitfs().lock(remote=remote) def update(): ''' Execute a git fetch on all of the repos ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - gitfs.update() + _gitfs().update() def envs(ignore_cache=False): ''' Return a list of refs that can be used as environments ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - return gitfs.envs(ignore_cache=ignore_cache) + return _gitfs().envs(ignore_cache=ignore_cache) def find_file(path, tgt_env='base', **kwargs): # pylint: disable=W0613 @@ -145,10 +141,7 @@ def find_file(path, tgt_env='base', **kwargs): # pylint: disable=W0613 Find the first file to match the path and ref, read the file out of git and send the path to the newly cached file ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - return gitfs.find_file(path, tgt_env=tgt_env, **kwargs) + return _gitfs().find_file(path, tgt_env=tgt_env, **kwargs) def init(): @@ -156,29 +149,21 @@ def init(): Initialize remotes. This is only used by the master's pre-flight checks, and is not invoked by GitFS. ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) + _gitfs() def serve_file(load, fnd): ''' Return a chunk from a file based on the data received ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - return gitfs.serve_file(load, fnd) + return _gitfs().serve_file(load, fnd) def file_hash(load, fnd): ''' Return a file hash, the hash type is set in the master config file ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - return gitfs.file_hash(load, fnd) + return _gitfs().file_hash(load, fnd) def file_list(load): @@ -186,10 +171,7 @@ def file_list(load): Return a list of all files on the file server in a specified environment (specified as a key within the load dict). ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - return gitfs.file_list(load) + return _gitfs().file_list(load) def file_list_emptydirs(load): # pylint: disable=W0613 @@ -204,17 +186,11 @@ def dir_list(load): ''' Return a list of all directories on the master ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - return gitfs.dir_list(load) + return _gitfs().dir_list(load) def symlink_list(load): ''' Return a dict of all symlinks based on a given path in the repo ''' - gitfs = salt.utils.gitfs.GitFS(__opts__) - gitfs.init_remotes(__opts__['gitfs_remotes'], - PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) - return gitfs.symlink_list(load) + return _gitfs().symlink_list(load) diff --git a/salt/master.py b/salt/master.py index 05c0cc704b..1ab22c3334 100644 --- a/salt/master.py +++ b/salt/master.py @@ -487,11 +487,11 @@ class Master(SMaster): for repo in git_pillars: new_opts[u'ext_pillar'] = [repo] try: - git_pillar = salt.utils.gitfs.GitPillar(new_opts) - git_pillar.init_remotes( + git_pillar = salt.utils.gitfs.GitPillar( + new_opts, repo[u'git'], - salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, - salt.pillar.git_pillar.PER_REMOTE_ONLY) + per_remote_overrides=salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, + per_remote_only=salt.pillar.git_pillar.PER_REMOTE_ONLY) except FileserverConfigError as exc: critical_errors.append(exc.strerror) finally: diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py index 0848d76309..37cb1bd98b 100644 --- a/salt/pillar/__init__.py +++ b/salt/pillar/__init__.py @@ -891,11 +891,11 @@ class Pillar(object): # Avoid circular import import salt.utils.gitfs import salt.pillar.git_pillar - git_pillar = salt.utils.gitfs.GitPillar(self.opts) - git_pillar.init_remotes( + git_pillar = salt.utils.gitfs.GitPillar( + self.opts, self.ext['git'], - salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, - salt.pillar.git_pillar.PER_REMOTE_ONLY) + per_remote_overrides=salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, + per_remote_only=salt.pillar.git_pillar.PER_REMOTE_ONLY) git_pillar.fetch_remotes() except TypeError: # Handle malformed ext_pillar diff --git a/salt/pillar/git_pillar.py b/salt/pillar/git_pillar.py index fec485263d..732183a089 100644 --- a/salt/pillar/git_pillar.py +++ b/salt/pillar/git_pillar.py @@ -348,12 +348,6 @@ from salt.ext import six PER_REMOTE_OVERRIDES = ('env', 'root', 'ssl_verify', 'refspecs') PER_REMOTE_ONLY = ('name', 'mountpoint') -# Fall back to default per-remote-only. This isn't technically needed since -# salt.utils.gitfs.GitBase.init_remotes() will default to -# salt.utils.gitfs.PER_REMOTE_ONLY for this value, so this is mainly for -# runners and other modules that import salt.pillar.git_pillar. -PER_REMOTE_ONLY = salt.utils.gitfs.PER_REMOTE_ONLY - # Set up logging log = logging.getLogger(__name__) @@ -371,7 +365,7 @@ def __virtual__(): return False try: - salt.utils.gitfs.GitPillar(__opts__) + salt.utils.gitfs.GitPillar(__opts__, init_remotes=False) # Initialization of the GitPillar object did not fail, so we # know we have valid configuration syntax and that a valid # provider was detected. @@ -387,8 +381,11 @@ def ext_pillar(minion_id, pillar, *repos): # pylint: disable=unused-argument opts = copy.deepcopy(__opts__) opts['pillar_roots'] = {} opts['__git_pillar'] = True - git_pillar = salt.utils.gitfs.GitPillar(opts) - git_pillar.init_remotes(repos, PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) + git_pillar = salt.utils.gitfs.GitPillar( + opts, + repos, + per_remote_overrides=PER_REMOTE_OVERRIDES, + per_remote_only=PER_REMOTE_ONLY) if __opts__.get('__role') == 'minion': # If masterless, fetch the remotes. We'll need to remove this once # we make the minion daemon able to run standalone. diff --git a/salt/runners/cache.py b/salt/runners/cache.py index 459ff325ea..cbd7475853 100644 --- a/salt/runners/cache.py +++ b/salt/runners/cache.py @@ -328,11 +328,14 @@ def clear_git_lock(role, remote=None, **kwargs): salt.utils.args.invalid_kwargs(kwargs) if role == 'gitfs': - git_objects = [salt.utils.gitfs.GitFS(__opts__)] - git_objects[0].init_remotes( - __opts__['gitfs_remotes'], - salt.fileserver.gitfs.PER_REMOTE_OVERRIDES, - salt.fileserver.gitfs.PER_REMOTE_ONLY) + git_objects = [ + salt.utils.gitfs.GitFS( + __opts__, + __opts__['gitfs_remotes'], + per_remote_overrides=salt.fileserver.gitfs.PER_REMOTE_OVERRIDES, + per_remote_only=salt.fileserver.gitfs.PER_REMOTE_ONLY + ) + ] elif role == 'git_pillar': git_objects = [] for ext_pillar in __opts__['ext_pillar']: @@ -340,11 +343,11 @@ def clear_git_lock(role, remote=None, **kwargs): if key == 'git': if not isinstance(ext_pillar['git'], list): continue - obj = salt.utils.gitfs.GitPillar(__opts__) - obj.init_remotes( + obj = salt.utils.gitfs.GitPillar( + __opts__, ext_pillar['git'], - salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, - salt.pillar.git_pillar.PER_REMOTE_ONLY) + per_remote_overrides=salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, + per_remote_only=salt.pillar.git_pillar.PER_REMOTE_ONLY) git_objects.append(obj) elif role == 'winrepo': winrepo_dir = __opts__['winrepo_dir'] @@ -355,11 +358,12 @@ def clear_git_lock(role, remote=None, **kwargs): (winrepo_remotes, winrepo_dir), (__opts__['winrepo_remotes_ng'], __opts__['winrepo_dir_ng']) ): - obj = salt.utils.gitfs.WinRepo(__opts__, base_dir) - obj.init_remotes( + obj = salt.utils.gitfs.WinRepo( + __opts__, remotes, - salt.runners.winrepo.PER_REMOTE_OVERRIDES, - salt.runners.winrepo.PER_REMOTE_ONLY) + per_remote_overrides=salt.runners.winrepo.PER_REMOTE_OVERRIDES, + per_remote_only=salt.runners.winrepo.PER_REMOTE_ONLY, + cache_root=base_dir) git_objects.append(obj) else: raise SaltInvocationError('Invalid role \'{0}\''.format(role)) diff --git a/salt/runners/git_pillar.py b/salt/runners/git_pillar.py index 6826268076..984c7da8cc 100644 --- a/salt/runners/git_pillar.py +++ b/salt/runners/git_pillar.py @@ -66,10 +66,11 @@ def update(branch=None, repo=None): if pillar_type != 'git': continue pillar_conf = ext_pillar[pillar_type] - pillar = salt.utils.gitfs.GitPillar(__opts__) - pillar.init_remotes(pillar_conf, - salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, - salt.pillar.git_pillar.PER_REMOTE_ONLY) + pillar = salt.utils.gitfs.GitPillar( + __opts__, + pillar_conf, + per_remote_overrides=salt.pillar.git_pillar.PER_REMOTE_OVERRIDES, + per_remote_only=salt.pillar.git_pillar.PER_REMOTE_ONLY) for remote in pillar.remotes: # Skip this remote if it doesn't match the search criteria if branch is not None: diff --git a/salt/runners/winrepo.py b/salt/runners/winrepo.py index 1e73974c4e..4aa20d2b35 100644 --- a/salt/runners/winrepo.py +++ b/salt/runners/winrepo.py @@ -32,7 +32,7 @@ log = logging.getLogger(__name__) PER_REMOTE_OVERRIDES = ('ssl_verify', 'refspecs') # Fall back to default per-remote-only. This isn't technically needed since -# salt.utils.gitfs.GitBase.init_remotes() will default to +# salt.utils.gitfs.GitBase.__init__ will default to # salt.utils.gitfs.PER_REMOTE_ONLY for this value, so this is mainly for # runners and other modules that import salt.runners.winrepo. PER_REMOTE_ONLY = salt.utils.gitfs.PER_REMOTE_ONLY @@ -216,9 +216,12 @@ def update_git_repos(opts=None, clean=False, masterless=False): else: # New winrepo code utilizing salt.utils.gitfs try: - winrepo = salt.utils.gitfs.WinRepo(opts, base_dir) - winrepo.init_remotes( - remotes, PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY) + winrepo = salt.utils.gitfs.WinRepo( + opts, + remotes, + per_remote_overrides=PER_REMOTE_OVERRIDES, + per_remote_only=PER_REMOTE_ONLY, + cache_root=base_dir) winrepo.fetch_remotes() # Since we're not running update(), we need to manually call # clear_old_remotes() to remove directories from remotes that diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py index ae028463a0..2f4b119837 100644 --- a/salt/utils/gitfs.py +++ b/salt/utils/gitfs.py @@ -17,6 +17,8 @@ import shutil import stat import subprocess import time +import tornado.ioloop +import weakref from datetime import datetime # Import salt libs @@ -1923,12 +1925,47 @@ class GitBase(object): ''' Base class for gitfs/git_pillar ''' - def __init__(self, opts, git_providers=None, cache_root=None): + def __init__(self, opts, remotes=None, per_remote_overrides=(), + per_remote_only=PER_REMOTE_ONLY, git_providers=None, + cache_root=None, init_remotes=True): ''' IMPORTANT: If specifying a cache_root, understand that this is also where the remotes will be cloned. A non-default cache_root is only really designed right now for winrepo, as its repos need to be checked out into the winrepo locations and not within the cachedir. + + As of the Oxygen release cycle, the classes used to interface with + Pygit2 and GitPython can be overridden by passing the git_providers + argument when spawning a class instance. This allows for one to write + classes which inherit from salt.utils.gitfs.Pygit2 or + salt.utils.gitfs.GitPython, and then direct one of the GitBase + subclasses (GitFS, GitPillar, WinRepo) to use the custom class. For + example: + + .. code-block:: Python + + import salt.utils.gitfs + from salt.fileserver.gitfs import PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY + + class CustomPygit2(salt.utils.gitfs.Pygit2): + def fetch_remotes(self): + ... + Alternate fetch behavior here + ... + + git_providers = { + 'pygit2': CustomPygit2, + 'gitpython': salt.utils.gitfs.GitPython, + } + + gitfs = salt.utils.gitfs.GitFS( + __opts__, + __opts__['gitfs_remotes'], + per_remote_overrides=PER_REMOTE_OVERRIDES, + per_remote_only=PER_REMOTE_ONLY, + git_providers=git_providers) + + gitfs.fetch_remotes() ''' self.opts = opts self.git_providers = git_providers if git_providers is not None \ @@ -1944,8 +1981,13 @@ class GitBase(object): self.hash_cachedir = salt.utils.path.join(self.cache_root, 'hash') self.file_list_cachedir = salt.utils.path.join( self.opts['cachedir'], 'file_lists', self.role) + if init_remotes: + self.init_remotes( + remotes if remotes is not None else [], + per_remote_overrides, + per_remote_only) - def init_remotes(self, remotes, per_remote_overrides, + def init_remotes(self, remotes, per_remote_overrides=(), per_remote_only=PER_REMOTE_ONLY): ''' Initialize remotes @@ -2469,9 +2511,51 @@ class GitFS(GitBase): ''' Functionality specific to the git fileserver backend ''' - def __init__(self, opts): - self.role = 'gitfs' - super(GitFS, self).__init__(opts) + role = 'gitfs' + instance_map = weakref.WeakKeyDictionary() + + def __new__(cls, opts, remotes=None, per_remote_overrides=(), + per_remote_only=PER_REMOTE_ONLY, git_providers=None, + cache_root=None, init_remotes=True): + ''' + If we are not initializing remotes (such as in cases where we just want + to load the config so that we can run clear_cache), then just return a + new __init__'ed object. Otherwise, check the instance map and re-use an + instance if one exists for the current process. Weak references are + used to ensure that we garbage collect instances for threads which have + exited. + ''' + # No need to get the ioloop reference if we're not initializing remotes + io_loop = tornado.ioloop.IOLoop.current() if init_remotes else None + if not init_remotes or io_loop not in cls.instance_map: + # We only evaluate the second condition in this if statement if + # we're initializing remotes, so we won't get here unless io_loop + # is something other than None. + obj = object.__new__(cls) + super(GitFS, obj).__init__( + opts, + remotes if remotes is not None else [], + per_remote_overrides=per_remote_overrides, + per_remote_only=per_remote_only, + git_providers=git_providers if git_providers is not None + else GIT_PROVIDERS, + cache_root=cache_root, + init_remotes=init_remotes) + if not init_remotes: + log.debug('Created gitfs object with uninitialized remotes') + else: + log.debug('Created gitfs object for process %s', os.getpid()) + # Add to the instance map so we can re-use later + cls.instance_map[io_loop] = obj + return obj + log.debug('Re-using gitfs object for process %s', os.getpid()) + return cls.instance_map[io_loop] + + def __init__(self, opts, remotes, per_remote_overrides=(), # pylint: disable=super-init-not-called + per_remote_only=PER_REMOTE_ONLY, git_providers=None, + cache_root=None, init_remotes=True): + # Initialization happens above in __new__(), so don't do anything here + pass def dir_list(self, load): ''' @@ -2753,9 +2837,7 @@ class GitPillar(GitBase): ''' Functionality specific to the git external pillar ''' - def __init__(self, opts): - self.role = 'git_pillar' - super(GitPillar, self).__init__(opts) + role = 'git_pillar' def checkout(self): ''' @@ -2843,9 +2925,7 @@ class WinRepo(GitBase): ''' Functionality specific to the winrepo runner ''' - def __init__(self, opts, winrepo_dir): - self.role = 'winrepo' - super(WinRepo, self).__init__(opts, cache_root=winrepo_dir) + role = 'winrepo' def checkout(self): ''' diff --git a/tests/unit/fileserver/test_gitfs.py b/tests/unit/fileserver/test_gitfs.py index 64d8ca5284..bda0182eec 100644 --- a/tests/unit/fileserver/test_gitfs.py +++ b/tests/unit/fileserver/test_gitfs.py @@ -5,10 +5,12 @@ # Import Python libs from __future__ import absolute_import +import errno import os import shutil import tempfile import textwrap +import tornado.ioloop import logging import stat try: @@ -40,18 +42,26 @@ import salt.utils.win_functions log = logging.getLogger(__name__) +TMP_SOCK_DIR = tempfile.mkdtemp(dir=TMP) +TMP_REPO_DIR = os.path.join(TMP, 'gitfs_root') +INTEGRATION_BASE_FILES = os.path.join(FILES, 'file', 'base') + + +def _rmtree_error(func, path, excinfo): + os.chmod(path, stat.S_IWRITE) + func(path) + @skipIf(not HAS_GITPYTHON, 'GitPython is not installed') class GitfsConfigTestCase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): self.tmp_cachedir = tempfile.mkdtemp(dir=TMP) - self.tmp_sock_dir = tempfile.mkdtemp(dir=TMP) return { gitfs: { '__opts__': { 'cachedir': self.tmp_cachedir, - 'sock_dir': self.tmp_sock_dir, + 'sock_dir': TMP_SOCK_DIR, 'gitfs_root': 'salt', 'fileserver_backend': ['git'], 'gitfs_base': 'master', @@ -81,9 +91,17 @@ class GitfsConfigTestCase(TestCase, LoaderModuleMockMixin): } } + @classmethod + def setUpClass(cls): + # Clear the instance map so that we make sure to create a new instance + # for this test class. + try: + del salt.utils.gitfs.GitFS.instance_map[tornado.ioloop.IOLoop.current()] + except KeyError: + pass + def tearDown(self): shutil.rmtree(self.tmp_cachedir) - shutil.rmtree(self.tmp_sock_dir) def test_per_saltenv_config(self): opts_override = textwrap.dedent(''' @@ -109,10 +127,11 @@ class GitfsConfigTestCase(TestCase, LoaderModuleMockMixin): - mountpoint: abc ''') with patch.dict(gitfs.__opts__, yaml.safe_load(opts_override)): - git_fs = salt.utils.gitfs.GitFS(gitfs.__opts__) - git_fs.init_remotes( + git_fs = salt.utils.gitfs.GitFS( + gitfs.__opts__, gitfs.__opts__['gitfs_remotes'], - gitfs.PER_REMOTE_OVERRIDES, gitfs.PER_REMOTE_ONLY) + per_remote_overrides=gitfs.PER_REMOTE_OVERRIDES, + per_remote_only=gitfs.PER_REMOTE_ONLY) # repo1 (branch: foo) # The mountpoint should take the default (from gitfs_mountpoint), while @@ -169,14 +188,12 @@ class GitFSTest(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): self.tmp_cachedir = tempfile.mkdtemp(dir=TMP) - self.tmp_sock_dir = tempfile.mkdtemp(dir=TMP) - self.tmp_repo_dir = os.path.join(TMP, 'gitfs_root') return { gitfs: { '__opts__': { 'cachedir': self.tmp_cachedir, - 'sock_dir': self.tmp_sock_dir, - 'gitfs_remotes': ['file://' + self.tmp_repo_dir], + 'sock_dir': TMP_SOCK_DIR, + 'gitfs_remotes': ['file://' + TMP_REPO_DIR], 'gitfs_root': '', 'fileserver_backend': ['git'], 'gitfs_base': 'master', @@ -206,26 +223,26 @@ class GitFSTest(TestCase, LoaderModuleMockMixin): } } - def setUp(self): - ''' - We don't want to check in another .git dir into GH because that just gets messy. - Instead, we'll create a temporary repo on the fly for the tests to examine. - ''' - if not gitfs.__virtual__(): - self.skipTest("GitFS could not be loaded. Skipping GitFS tests!") - self.integration_base_files = os.path.join(FILES, 'file', 'base') + @classmethod + def setUpClass(cls): + # Clear the instance map so that we make sure to create a new instance + # for this test class. + try: + del salt.utils.gitfs.GitFS.instance_map[tornado.ioloop.IOLoop.current()] + except KeyError: + pass # Create the dir if it doesn't already exist try: - shutil.copytree(self.integration_base_files, self.tmp_repo_dir + '/') + shutil.copytree(INTEGRATION_BASE_FILES, TMP_REPO_DIR + '/') except OSError: # We probably caught an error because files already exist. Ignore pass try: - repo = git.Repo(self.tmp_repo_dir) + repo = git.Repo(TMP_REPO_DIR) except git.exc.InvalidGitRepositoryError: - repo = git.Repo.init(self.tmp_repo_dir) + repo = git.Repo.init(TMP_REPO_DIR) if 'USERNAME' not in os.environ: try: @@ -238,9 +255,19 @@ class GitFSTest(TestCase, LoaderModuleMockMixin): '\'root\'.') os.environ['USERNAME'] = 'root' - repo.index.add([x for x in os.listdir(self.tmp_repo_dir) + repo.index.add([x for x in os.listdir(TMP_REPO_DIR) if x != '.git']) repo.index.commit('Test') + + def setUp(self): + ''' + We don't want to check in another .git dir into GH because that just + gets messy. Instead, we'll create a temporary repo on the fly for the + tests to examine. + ''' + if not gitfs.__virtual__(): + self.skipTest("GitFS could not be loaded. Skipping GitFS tests!") + self.tmp_cachedir = tempfile.mkdtemp(dir=TMP) gitfs.update() def tearDown(self): @@ -248,17 +275,11 @@ class GitFSTest(TestCase, LoaderModuleMockMixin): Remove the temporary git repository and gitfs cache directory to ensure a clean environment for each test. ''' - shutil.rmtree(self.tmp_repo_dir, onerror=self._rmtree_error) - shutil.rmtree(self.tmp_cachedir, onerror=self._rmtree_error) - shutil.rmtree(self.tmp_sock_dir, onerror=self._rmtree_error) - del self.tmp_repo_dir - del self.tmp_cachedir - del self.tmp_sock_dir - del self.integration_base_files - - def _rmtree_error(self, func, path, excinfo): - os.chmod(path, stat.S_IWRITE) - func(path) + try: + shutil.rmtree(self.tmp_cachedir, onerror=_rmtree_error) + except OSError as exc: + if exc.errno != errno.EEXIST: + raise def test_file_list(self): ret = gitfs.file_list(LOAD) diff --git a/tests/unit/utils/test_gitfs.py b/tests/unit/utils/test_gitfs.py index 070a46fe75..89a8b4fd59 100644 --- a/tests/unit/utils/test_gitfs.py +++ b/tests/unit/utils/test_gitfs.py @@ -37,18 +37,19 @@ class TestGitFSProvider(TestCase): MagicMock(return_value=True)): with patch.object(role_class, 'verify_pygit2', MagicMock(return_value=False)): - args = [OPTS] + args = [OPTS, {}] + kwargs = {'init_remotes': False} if role_name == 'winrepo': - args.append('/tmp/winrepo-dir') + kwargs['cache_root'] = '/tmp/winrepo-dir' with patch.dict(OPTS, {key: provider}): # Try to create an instance with uppercase letters in # provider name. If it fails then a # FileserverConfigError will be raised, so no assert is # necessary. - role_class(*args) - # Now try to instantiate an instance with all lowercase - # letters. Again, no need for an assert here. - role_class(*args) + role_class(*args, **kwargs) + # Now try to instantiate an instance with all lowercase + # letters. Again, no need for an assert here. + role_class(*args, **kwargs) def test_valid_provider(self): ''' @@ -73,12 +74,13 @@ class TestGitFSProvider(TestCase): verify = 'verify_pygit2' mock2 = _get_mock(verify, provider) with patch.object(role_class, verify, mock2): - args = [OPTS] + args = [OPTS, {}] + kwargs = {'init_remotes': False} if role_name == 'winrepo': - args.append('/tmp/winrepo-dir') + kwargs['cache_root'] = '/tmp/winrepo-dir' with patch.dict(OPTS, {key: provider}): - role_class(*args) + role_class(*args, **kwargs) with patch.dict(OPTS, {key: 'foo'}): # Set the provider name to a known invalid provider @@ -86,5 +88,5 @@ class TestGitFSProvider(TestCase): self.assertRaises( FileserverConfigError, role_class, - *args - ) + *args, + **kwargs) From 8d1c1e21f060b9c1a0d859321449c5d1842db5a7 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Mon, 16 Oct 2017 16:43:10 -0600 Subject: [PATCH 624/633] Fix typos in paralell states docs --- doc/ref/states/parallel.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/ref/states/parallel.rst b/doc/ref/states/parallel.rst index 8a69eba2df..9edf1750e4 100644 --- a/doc/ref/states/parallel.rst +++ b/doc/ref/states/parallel.rst @@ -6,7 +6,7 @@ Introduced in Salt version ``2017.7.0`` it is now possible to run select states in parallel. This is accomplished very easily by adding the ``parallel: True`` option to your state declaration: -.. code_block:: yaml +.. code-block:: yaml nginx: service.running: @@ -24,7 +24,7 @@ state to finish. Given this example: -.. code_block:: yaml +.. code-block:: yaml sleep 10: cmd.run: @@ -74,16 +74,16 @@ also complete. Things to be Careful of ======================= -Parallel States does not prevent you from creating parallel conflicts on your +Parallel States do not prevent you from creating parallel conflicts on your system. This means that if you start multiple package installs using Salt then the package manager will block or fail. If you attempt to manage the same file with multiple states in parallel then the result can produce an unexpected file. Make sure that the states you choose to run in parallel do not conflict, or -else, like in and parallel programming environment, the outcome may not be +else, like in any parallel programming environment, the outcome may not be what you expect. Doing things like just making all states run in parallel -will almost certinly result in unexpected behavior. +will almost certainly result in unexpected behavior. With that said, running states in parallel should be safe the vast majority of the time and the most likely culprit for unexpected behavior is running From 9557504b758406bd1ac7182e92dc50866c65ad02 Mon Sep 17 00:00:00 2001 From: Tim Freund Date: Mon, 16 Oct 2017 19:26:43 -0400 Subject: [PATCH 625/633] Insert missing verb in gitfs walkthrough --- doc/topics/tutorials/gitfs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/topics/tutorials/gitfs.rst b/doc/topics/tutorials/gitfs.rst index 86dfae879d..02a7f0ecad 100644 --- a/doc/topics/tutorials/gitfs.rst +++ b/doc/topics/tutorials/gitfs.rst @@ -27,7 +27,7 @@ Installing Dependencies ======================= Both pygit2_ and GitPython_ are supported Python interfaces to git. If -compatible versions of both are installed, pygit2_ will preferred. In these +compatible versions of both are installed, pygit2_ will be preferred. In these cases, GitPython_ can be forced using the :conf_master:`gitfs_provider` parameter in the master config file. From c4d9684a904711b802f75f417dcbd6764fbaf496 Mon Sep 17 00:00:00 2001 From: Marc Koderer Date: Thu, 12 Oct 2017 10:32:45 +0200 Subject: [PATCH 626/633] Use correct mac prefix for kvm/qemu Using the private mac range for vms makes it impossible to distinguish between hypervisors. Issues #44056 Signed-off-by: Marc Koderer --- salt/modules/virt.py | 10 +++++++--- tests/unit/modules/test_virt.py | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 2d36b5b2d2..65a89a68e2 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -656,7 +656,7 @@ def _nic_profile(profile_name, hypervisor, **kwargs): if key not in attributes or not attributes[key]: attributes[key] = value - def _assign_mac(attributes): + def _assign_mac(attributes, hypervisor): dmac = kwargs.get('dmac', None) if dmac is not None: log.debug('DMAC address is {0}'.format(dmac)) @@ -666,11 +666,15 @@ def _nic_profile(profile_name, hypervisor, **kwargs): msg = 'Malformed MAC address: {0}'.format(dmac) raise CommandExecutionError(msg) else: - attributes['mac'] = salt.utils.network.gen_mac() + if hypervisor in ['qemu', 'kvm']: + attributes['mac'] = salt.utils.network.gen_mac( + prefix='52:54:00') + else: + attributes['mac'] = salt.utils.network.gen_mac() for interface in interfaces: _normalize_net_types(interface) - _assign_mac(interface) + _assign_mac(interface, hypervisor) if hypervisor in overlays: _apply_default_overlay(interface) diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index d083b7b1e5..029bff098e 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -428,6 +428,8 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): controllers = root.findall('.//devices/controller') # There should be no controller self.assertTrue(len(controllers) == 0) + # kvm mac address shoud start with 52:54:00 + self.assertTrue("mac address='52:54:00" in xml_data) def test_mixed_dict_and_list_as_profile_objects(self): From 4c02f6ccd88eefe934cffb414b520d65e369aa83 Mon Sep 17 00:00:00 2001 From: Wido den Hollander Date: Tue, 17 Oct 2017 17:03:47 +0200 Subject: [PATCH 627/633] parted: Cast boundary to string when checking unit type It could be that just a number (integer) is passed to as either start or end to the module which is then already a int. The endswith() method will then fail because that is a String function. By casting it to a String while checking we make sure that the function works. Signed-off-by: Wido den Hollander --- salt/modules/parted.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/parted.py b/salt/modules/parted.py index 72a6d3ce65..d2351e6683 100644 --- a/salt/modules/parted.py +++ b/salt/modules/parted.py @@ -93,7 +93,7 @@ def _validate_partition_boundary(boundary): ''' try: for unit in VALID_UNITS: - if boundary.endswith(unit): + if str(boundary).endswith(unit): return int(boundary) except Exception: From 6823cdcbfaa3e391ef227d1533507e3cf53ce863 Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Tue, 17 Oct 2017 19:21:29 +0000 Subject: [PATCH 628/633] add documentation for venv_bin in virtualenv_mod state --- salt/states/virtualenv_mod.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/salt/states/virtualenv_mod.py b/salt/states/virtualenv_mod.py index bd394bfd0b..2df058a7ce 100644 --- a/salt/states/virtualenv_mod.py +++ b/salt/states/virtualenv_mod.py @@ -67,6 +67,10 @@ def managed(name, name Path to the virtualenv. + venv_bin: virtualenv + The name (and optionally path) of the virtualenv command. This can also + be set globally in the minion config file as ``virtualenv.venv_bin``. + requirements: None Path to a pip requirements file. If the path begins with ``salt://`` the file will be transferred from the master file server. From 0311c2a4dee1c303641baa6953068383dba952d1 Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 17 Oct 2017 17:55:18 -0400 Subject: [PATCH 629/633] Replace salt utils import with correct salt.utils.files path This fixes a couple of test failures in the develop branch and cleans up a couple of formatting issues and import ordering. --- tests/integration/modules/test_groupadd.py | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/integration/modules/test_groupadd.py b/tests/integration/modules/test_groupadd.py index 9963793ca1..9936fc7411 100644 --- a/tests/integration/modules/test_groupadd.py +++ b/tests/integration/modules/test_groupadd.py @@ -2,18 +2,18 @@ # Import python libs from __future__ import absolute_import -import string +import grp +import os import random +import string # Import Salt Testing libs from tests.support.case import ModuleCase from tests.support.helpers import destructiveTest, skip_if_not_root -# Import 3rd-party libs +# Import Salt libs from salt.ext.six.moves import range -import os -import grp -from salt import utils +import salt.utils.files @skip_if_not_root @@ -66,7 +66,7 @@ class GroupModuleTest(ModuleCase): ''' defs_file = '/etc/login.defs' if os.path.exists(defs_file): - with utils.fopen(defs_file) as defs_fd: + with salt.utils.files.fopen(defs_file) as defs_fd: login_defs = dict([x.split() for x in defs_fd.readlines() if x.strip() @@ -102,12 +102,12 @@ class GroupModuleTest(ModuleCase): ''' Test the add group function ''' - #add a new group + # add a new group self.assertTrue(self.run_function('group.add', [self._group, self._gid])) group_info = self.run_function('group.info', [self._group]) self.assertEqual(group_info['name'], self._group) self.assertEqual(group_info['gid'], self._gid) - #try adding the group again + # try adding the group again self.assertFalse(self.run_function('group.add', [self._group, self._gid])) @destructiveTest @@ -124,7 +124,7 @@ class GroupModuleTest(ModuleCase): group_info = self.run_function('group.info', [self._group]) self.assertEqual(group_info['name'], self._group) self.assertTrue(gid_min <= group_info['gid'] <= gid_max) - #try adding the group again + # try adding the group again self.assertFalse(self.run_function('group.add', [self._group])) @@ -142,7 +142,7 @@ class GroupModuleTest(ModuleCase): group_info = self.run_function('group.info', [self._group]) self.assertEqual(group_info['name'], self._group) self.assertEqual(group_info['gid'], gid) - #try adding the group again + # try adding the group again self.assertFalse(self.run_function('group.add', [self._group, gid])) @@ -153,10 +153,10 @@ class GroupModuleTest(ModuleCase): ''' self.assertTrue(self.run_function('group.add', [self._group])) - #correct functionality + # correct functionality self.assertTrue(self.run_function('group.delete', [self._group])) - #group does not exist + # group does not exist self.assertFalse(self.run_function('group.delete', [self._no_group])) @destructiveTest @@ -193,11 +193,11 @@ class GroupModuleTest(ModuleCase): self.assertTrue(self.run_function('group.adduser', [self._group, self._user])) group_info = self.run_function('group.info', [self._group]) self.assertIn(self._user, group_info['members']) - #try add a non existing user + # try to add a non existing user self.assertFalse(self.run_function('group.adduser', [self._group, self._no_user])) - #try add a user to non existing group + # try to add a user to non existing group self.assertFalse(self.run_function('group.adduser', [self._no_group, self._user])) - #try add a non existing user to a non existing group + # try to add a non existing user to a non existing group self.assertFalse(self.run_function('group.adduser', [self._no_group, self._no_user])) @destructiveTest From d712031a435ea4376bcf4168c13dda53547314f4 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 18 Oct 2017 09:44:55 -0400 Subject: [PATCH 630/633] Update to_unicode util references to new stringutils path --- tests/unit/templates/test_jinja.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/templates/test_jinja.py b/tests/unit/templates/test_jinja.py index 08e7e2e5f1..e5ca422f92 100644 --- a/tests/unit/templates/test_jinja.py +++ b/tests/unit/templates/test_jinja.py @@ -33,6 +33,7 @@ from salt.utils.jinja import ( ) from salt.utils.templates import JINJA, render_jinja_tmpl from salt.utils.odict import OrderedDict +import salt.utils.stringutils # Import 3rd party libs import yaml @@ -296,7 +297,7 @@ class TestGetTemplate(TestCase): 'file_roots': self.local_opts['file_roots'], 'pillar_roots': self.local_opts['pillar_roots']}, a='Hi', b='Sàlt', saltenv='test', salt=self.local_salt)) - self.assertEqual(out, salt.utils.to_unicode('Hey world !Hi Sàlt !' + os.linesep)) + self.assertEqual(out, salt.utils.stringutils.to_unicode('Hey world !Hi Sàlt !' + os.linesep)) self.assertEqual(fc.requests[0]['path'], 'salt://macro') filename = os.path.join(TEMPLATES_DIR, 'files', 'test', 'non_ascii') @@ -370,8 +371,8 @@ class TestGetTemplate(TestCase): with salt.utils.files.fopen(out['data']) as fp: result = fp.read() if six.PY2: - result = salt.utils.to_unicode(result) - self.assertEqual(salt.utils.to_unicode('Assunção' + os.linesep), result) + result = salt.utils.stringutils.to_unicode(result) + self.assertEqual(salt.utils.stringutils.to_unicode('Assunção' + os.linesep), result) def test_get_context_has_enough_context(self): template = '1\n2\n3\n4\n5\n6\n7\n8\n9\na\nb\nc\nd\ne\nf' From 77b948b00ae5f073074f28d893dfae84c6b41968 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Wed, 18 Oct 2017 17:06:16 +0200 Subject: [PATCH 631/633] Skip shadow module unit test if not root or no shadow --- tests/unit/modules/test_shadow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/test_shadow.py b/tests/unit/modules/test_shadow.py index e152d59b9a..6040b83ab9 100644 --- a/tests/unit/modules/test_shadow.py +++ b/tests/unit/modules/test_shadow.py @@ -42,6 +42,7 @@ _HASHES = dict( @skipIf(not salt.utils.platform.is_linux(), 'minion is not Linux') +@skipIf(not HAS_SHADOW, 'shadow module is not available') class LinuxShadowTest(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): @@ -62,8 +63,7 @@ class LinuxShadowTest(TestCase, LoaderModuleMockMixin): hash_info['pw_hash'] ) - # 'list_users' function tests: 1 - + @skip_if_not_root def test_list_users(self): ''' Test if it returns a list of all users From 4bfeb7f1d144a8ce283c348459393a26abd4559f Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Thu, 19 Oct 2017 03:17:16 +0200 Subject: [PATCH 632/633] Fixed missing import --- tests/unit/modules/test_shadow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/test_shadow.py b/tests/unit/modules/test_shadow.py index 6040b83ab9..62781153c2 100644 --- a/tests/unit/modules/test_shadow.py +++ b/tests/unit/modules/test_shadow.py @@ -10,6 +10,7 @@ from __future__ import absolute_import import salt.utils.platform from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import TestCase, skipIf +from tests.support.helpers import skip_if_not_root # Import salt libs try: From 44e37bf6e578cda0930fbcfcde8ae00c3f0b2bdb Mon Sep 17 00:00:00 2001 From: Senthilkumar Eswaran Date: Wed, 18 Oct 2017 22:08:26 -0700 Subject: [PATCH 633/633] Fixing default redis.host in documentation --- salt/modules/redismod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/redismod.py b/salt/modules/redismod.py index a95e1b9f3f..40ebbdc3a1 100644 --- a/salt/modules/redismod.py +++ b/salt/modules/redismod.py @@ -9,7 +9,7 @@ Module to provide redis functionality to Salt .. code-block:: yaml - redis.host: 'localhost' + redis.host: 'salt' redis.port: 6379 redis.db: 0 redis.password: None