diff --git a/doc/ref/configuration/logging/index.rst b/doc/ref/configuration/logging/index.rst index 9d469778d2..a266480be4 100644 --- a/doc/ref/configuration/logging/index.rst +++ b/doc/ref/configuration/logging/index.rst @@ -225,15 +225,16 @@ enclosing brackets ``[`` and ``]``: Default: ``{}`` -This can be used to control logging levels more specifically. The example sets -the main salt library at the 'warning' level, but sets ``salt.modules`` to log -at the ``debug`` level: +This can be used to control logging levels more specifically, based on log call name. The example sets +the main salt library at the 'warning' level, sets ``salt.modules`` to log +at the ``debug`` level, and sets a custom module to the ``all`` level: .. code-block:: yaml log_granular_levels: 'salt': 'warning' 'salt.modules': 'debug' + 'salt.loader.saltmaster.ext.module.custom_module': 'all' External Logging Handlers ------------------------- diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index 39a6c9f5c1..4c58ae2474 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -303,6 +303,20 @@ option on the Salt master. master_port: 4506 +.. conf_minion:: publish_port + +``publish_port`` +--------------- + +Default: ``4505`` + +The port of the master publish server, this needs to coincide with the publish_port +option on the Salt master. + +.. code-block:: yaml + + publish_port: 4505 + .. conf_minion:: user ``user`` diff --git a/doc/topics/grains/index.rst b/doc/topics/grains/index.rst index aed9c1a912..fe9c284595 100644 --- a/doc/topics/grains/index.rst +++ b/doc/topics/grains/index.rst @@ -314,3 +314,9 @@ Syncing grains can be done a number of ways, they are automatically synced when above) the grains can be manually synced and reloaded by calling the :mod:`saltutil.sync_grains ` or :mod:`saltutil.sync_all ` functions. + +.. note:: + + When the :conf_minion:`grains_cache` is set to False, the grains dictionary is built + and stored in memory on the minion. Every time the minion restarts or + ``saltutil.refresh_grains`` is run, the grain dictionary is rebuilt from scratch. diff --git a/salt/beacons/__init__.py b/salt/beacons/__init__.py index 994f2c02af..99913a3b96 100644 --- a/salt/beacons/__init__.py +++ b/salt/beacons/__init__.py @@ -186,19 +186,60 @@ class Beacon(object): else: self.opts['beacons'][name].append({'enabled': enabled_value}) - def list_beacons(self): + def _get_beacons(self, + include_opts=True, + include_pillar=True): + ''' + Return the beacons data structure + ''' + beacons = {} + if include_pillar: + pillar_beacons = self.opts.get('pillar', {}).get('beacons', {}) + if not isinstance(pillar_beacons, dict): + raise ValueError('Beacons must be of type dict.') + beacons.update(pillar_beacons) + if include_opts: + opts_beacons = self.opts.get('beacons', {}) + if not isinstance(opts_beacons, dict): + raise ValueError('Beacons must be of type dict.') + beacons.update(opts_beacons) + return beacons + + def list_beacons(self, + include_pillar=True, + include_opts=True): ''' List the beacon items + + include_pillar: Whether to include beacons that are + configured in pillar, default is True. + + include_opts: Whether to include beacons that are + configured in opts, default is True. ''' + beacons = self._get_beacons(include_pillar, include_opts) + # 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') - self.opts['beacons'].update(b_conf) - evt.fire_event({'complete': True, 'beacons': self.opts['beacons']}, + evt.fire_event({'complete': True, 'beacons': 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 add_beacon(self, name, beacon_data): ''' Add a beacon item @@ -207,16 +248,23 @@ class Beacon(object): data = {} data[name] = beacon_data - if name in self.opts['beacons']: - log.info('Updating settings for beacon ' - 'item: {0}'.format(name)) + if name in self._get_beacons(include_opts=False): + comment = 'Cannot update beacon item {0}, ' \ + 'because it is configured in pillar.'.format(name) + complete = False else: - log.info('Added new beacon item {0}'.format(name)) - self.opts['beacons'].update(data) + if name in self.opts['beacons']: + comment = 'Updating settings for beacon ' \ + 'item: {0}'.format(name) + else: + comment = 'Added new beacon item: {0}'.format(name) + complete = True + self.opts['beacons'].update(data) # Fire the complete event back along with updated list of beacons evt = salt.utils.event.get_event('minion', opts=self.opts) - evt.fire_event({'complete': True, 'beacons': self.opts['beacons']}, + evt.fire_event({'complete': complete, 'comment': comment, + 'beacons': self.opts['beacons']}, tag='/salt/minion/minion_beacon_add_complete') return True @@ -229,15 +277,21 @@ class Beacon(object): data = {} data[name] = beacon_data - log.info('Updating settings for beacon ' - 'item: {0}'.format(name)) - self.opts['beacons'].update(data) + if name in self._get_beacons(include_opts=False): + comment = 'Cannot modify beacon item {0}, ' \ + 'it is configured in pillar.'.format(name) + complete = False + else: + comment = 'Updating settings for beacon ' \ + 'item: {0}'.format(name) + complete = True + self.opts['beacons'].update(data) # Fire the complete event back along with updated list of beacons evt = salt.utils.event.get_event('minion', opts=self.opts) - evt.fire_event({'complete': True, 'beacons': self.opts['beacons']}, + evt.fire_event({'complete': complete, 'comment': comment, + 'beacons': self.opts['beacons']}, tag='/salt/minion/minion_beacon_modify_complete') - return True def delete_beacon(self, name): @@ -245,13 +299,22 @@ class Beacon(object): Delete a beacon item ''' - if name in self.opts['beacons']: - log.info('Deleting beacon item {0}'.format(name)) - del self.opts['beacons'][name] + if name in self._get_beacons(include_opts=False): + comment = 'Cannot delete beacon item {0}, ' \ + 'it is configured in pillar.'.format(name) + complete = False + else: + if name in self.opts['beacons']: + del self.opts['beacons'][name] + comment = 'Deleting beacon item: {0}'.format(name) + else: + comment = 'Beacon item {0} not found.'.format(name) + complete = True # Fire the complete event back along with updated list of beacons evt = salt.utils.event.get_event('minion', opts=self.opts) - evt.fire_event({'complete': True, 'beacons': self.opts['beacons']}, + evt.fire_event({'complete': complete, 'comment': comment, + 'beacons': self.opts['beacons']}, tag='/salt/minion/minion_beacon_delete_complete') return True @@ -289,11 +352,19 @@ class Beacon(object): Enable a beacon ''' - self._update_enabled(name, True) + if name in self._get_beacons(include_opts=False): + comment = 'Cannot enable beacon item {0}, ' \ + 'it is configured in pillar.'.format(name) + complete = False + else: + self._update_enabled(name, True) + comment = 'Enabling beacon item {0}'.format(name) + complete = True # Fire the complete event back along with updated list of beacons evt = salt.utils.event.get_event('minion', opts=self.opts) - evt.fire_event({'complete': True, 'beacons': self.opts['beacons']}, + evt.fire_event({'complete': complete, 'comment': comment, + 'beacons': self.opts['beacons']}, tag='/salt/minion/minion_beacon_enabled_complete') return True @@ -303,11 +374,19 @@ class Beacon(object): Disable a beacon ''' - self._update_enabled(name, False) + if name in self._get_beacons(include_opts=False): + comment = 'Cannot disable beacon item {0}, ' \ + 'it is configured in pillar.'.format(name) + complete = False + else: + self._update_enabled(name, False) + comment = 'Disabling beacon item {0}'.format(name) + complete = True # Fire the complete event back along with updated list of beacons evt = salt.utils.event.get_event('minion', opts=self.opts) - evt.fire_event({'complete': True, 'beacons': self.opts['beacons']}, + evt.fire_event({'complete': complete, 'comment': comment, + 'beacons': self.opts['beacons']}, tag='/salt/minion/minion_beacon_disabled_complete') return True diff --git a/salt/minion.py b/salt/minion.py index 2cafd9ded6..5bcf1e33b9 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -1896,6 +1896,8 @@ class Minion(MinionBase): func = data.get('func', None) name = data.get('name', None) beacon_data = data.get('beacon_data', None) + include_pillar = data.get(u'include_pillar', None) + include_opts = data.get(u'include_opts', None) if func == 'add': self.beacons.add_beacon(name, beacon_data) @@ -1912,7 +1914,9 @@ class Minion(MinionBase): elif func == 'disable_beacon': self.beacons.disable_beacon(name) elif func == 'list': - self.beacons.list_beacons() + self.beacons.list_beacons(include_opts, include_pillar) + elif func == u'list_available': + self.beacons.list_available_beacons() def environ_setenv(self, tag, data): ''' diff --git a/salt/modules/beacons.py b/salt/modules/beacons.py index 61e2042000..35f7a3317e 100644 --- a/salt/modules/beacons.py +++ b/salt/modules/beacons.py @@ -27,12 +27,22 @@ __func_alias__ = { } -def list_(return_yaml=True): +def list_(return_yaml=True, + include_pillar=True, + include_opts=True): ''' List the beacons currently configured on the minion - :param return_yaml: Whether to return YAML formatted output, default True - :return: List of currently configured Beacons. + :param return_yaml: Whether to return YAML formatted output, + default True + + :param include_pillar: Whether to include beacons that are + configured in pillar, default is True. + + :param include_opts: Whether to include beacons that are + configured in opts, default is True. + + :return: List of currently configured Beacons. CLI Example: @@ -45,7 +55,10 @@ def list_(return_yaml=True): try: eventer = salt.utils.event.get_event('minion', opts=__opts__) - res = __salt__['event.fire']({'func': 'list'}, 'manage_beacons') + res = __salt__['event.fire']({'func': 'list', + 'include_pillar': include_pillar, + 'include_opts': include_opts}, + 'manage_beacons') if res: event_ret = eventer.get_event(tag='/salt/minion/minion_beacons_list_complete', wait=30) log.debug('event_ret {0}'.format(event_ret)) @@ -69,6 +82,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 @@ -91,6 +145,10 @@ def add(name, beacon_data, **kwargs): ret['comment'] = 'Beacon {0} is already configured.'.format(name) return ret + if name not in list_available(return_yaml=False): + ret['comment'] = 'Beacon "{0}" is not available.'.format(name) + return ret + if 'test' in kwargs and kwargs['test']: ret['result'] = True ret['comment'] = 'Beacon: {0} would be added.'.format(name) @@ -130,7 +188,10 @@ def add(name, beacon_data, **kwargs): if name in beacons and beacons[name] == beacon_data: ret['result'] = True ret['comment'] = 'Added beacon: {0}.'.format(name) - return ret + else: + ret['result'] = False + ret['comment'] = event_ret['comment'] + 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.' @@ -215,7 +276,10 @@ def modify(name, beacon_data, **kwargs): if name in beacons and beacons[name] == beacon_data: ret['result'] = True ret['comment'] = 'Modified beacon: {0}.'.format(name) - return ret + else: + ret['result'] = False + ret['comment'] = event_ret['comment'] + 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.' @@ -257,6 +321,9 @@ def delete(name, **kwargs): ret['result'] = True ret['comment'] = 'Deleted beacon: {0}.'.format(name) return ret + else: + ret['result'] = False + ret['comment'] = event_ret['comment'] 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.' @@ -279,7 +346,7 @@ def save(): ret = {'comment': [], 'result': True} - beacons = list_(return_yaml=False) + beacons = list_(return_yaml=False, include_pillar=False) # move this file into an configurable opt sfn = '{0}/{1}/beacons.conf'.format(__opts__['config_dir'], @@ -332,7 +399,7 @@ def enable(**kwargs): else: ret['result'] = False ret['comment'] = 'Failed to enable beacons on minion.' - return ret + return ret except KeyError: # Effectively a no-op, since we can't really return without an event system ret['comment'] = 'Event module not available. Beacons enable job failed.' @@ -372,7 +439,7 @@ def disable(**kwargs): else: ret['result'] = False ret['comment'] = 'Failed to disable beacons on minion.' - return ret + return ret except KeyError: # Effectively a no-op, since we can't really return without an event system ret['comment'] = 'Event module not available. Beacons enable job failed.' @@ -435,7 +502,10 @@ def enable_beacon(name, **kwargs): else: ret['result'] = False ret['comment'] = 'Failed to enable beacon {0} on minion.'.format(name) - return ret + else: + ret['result'] = False + ret['comment'] = event_ret['comment'] + 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 enable job failed.' @@ -488,7 +558,10 @@ def disable_beacon(name, **kwargs): else: ret['result'] = False ret['comment'] = 'Failed to disable beacon on minion.' - return ret + else: + ret['result'] = False + ret['comment'] = event_ret['comment'] + 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 disable job failed.' diff --git a/salt/modules/file.py b/salt/modules/file.py index 6e903a5669..1f94d8f242 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -1861,14 +1861,14 @@ def line(path, content=None, match=None, mode=None, location=None, if changed: if show_changes: with salt.utils.fopen(path, 'r') as fp_: - path_content = _splitlines_preserving_trailing_newline( - fp_.read()) - changes_diff = ''.join(difflib.unified_diff( - path_content, _splitlines_preserving_trailing_newline(body))) + path_content = fp_.read().splitlines(True) + changes_diff = ''.join(difflib.unified_diff(path_content, body.splitlines(True))) if __opts__['test'] is False: fh_ = None try: - fh_ = salt.utils.atomicfile.atomic_open(path, 'w') + # Make sure we match the file mode from salt.utils.fopen + mode = 'wb' if six.PY2 and salt.utils.is_windows() else 'w' + fh_ = salt.utils.atomicfile.atomic_open(path, mode) fh_.write(body) finally: if fh_: diff --git a/salt/modules/reg.py b/salt/modules/reg.py index 644859f141..0ca1f14772 100644 --- a/salt/modules/reg.py +++ b/salt/modules/reg.py @@ -24,7 +24,7 @@ Values or Entries Values/Entries are name/data pairs. There can be many values in a key. The (Default) value corresponds to the Key, the rest are their own value pairs. -:depends: - winreg Python module +:depends: - PyWin32 ''' # When production windows installer is using Python 3, Python 2 code can be removed @@ -35,14 +35,13 @@ from __future__ import unicode_literals import sys import logging from salt.ext.six.moves import range # pylint: disable=W0622,import-error -from salt.ext import six # Import third party libs try: - from salt.ext.six.moves import winreg as _winreg # pylint: disable=import-error,no-name-in-module - from win32con import HWND_BROADCAST, WM_SETTINGCHANGE - from win32api import RegCreateKeyEx, RegSetValueEx, RegFlushKey, \ - RegCloseKey, error as win32apiError, SendMessage + import win32gui + import win32api + import win32con + import pywintypes HAS_WINDOWS_MODULES = True except ImportError: HAS_WINDOWS_MODULES = False @@ -60,7 +59,7 @@ __virtualname__ = 'reg' def __virtual__(): ''' - Only works on Windows systems with the _winreg python module + Only works on Windows systems with the PyWin32 ''' if not salt.utils.is_windows(): return (False, 'reg execution module failed to load: ' @@ -69,106 +68,76 @@ def __virtual__(): if not HAS_WINDOWS_MODULES: return (False, 'reg execution module failed to load: ' 'One of the following libraries did not load: ' - + '_winreg, win32gui, win32con, win32api') + + 'win32gui, win32con, win32api') return __virtualname__ -# winreg in python 2 is hard coded to use codex 'mbcs', which uses -# encoding that the user has assign. The function _unicode_to_mbcs -# and _unicode_to_mbcs help with this. +def _to_mbcs(vdata): + ''' + Converts unicode to to current users character encoding. Use this for values + returned by reg functions + ''' + return salt.utils.to_unicode(vdata, 'mbcs') -def _unicode_to_mbcs(instr): +def _to_unicode(vdata): ''' - Converts unicode to to current users character encoding. + Converts from current users character encoding to unicode. Use this for + parameters being pass to reg functions ''' - if isinstance(instr, six.text_type): - # unicode to windows utf8 - return instr.encode('mbcs') - else: - # Assume its byte str or not a str/unicode - return instr - - -def _mbcs_to_unicode(instr): - ''' - Converts from current users character encoding to unicode. - When instr has a value of None, the return value of the function - will also be None. - ''' - if instr is None or isinstance(instr, six.text_type): - return instr - else: - return six.text_type(instr, 'mbcs') - - -def _mbcs_to_unicode_wrap(obj, vtype): - ''' - Wraps _mbcs_to_unicode for use with registry vdata - ''' - if vtype == 'REG_BINARY': - # We should be able to leave it alone if the user has passed binary data in yaml with - # binary !! - # In python < 3 this should have type str and in python 3+ this should be a byte array - return obj - if isinstance(obj, list): - return [_mbcs_to_unicode(x) for x in obj] - elif isinstance(obj, six.integer_types): - return obj - else: - return _mbcs_to_unicode(obj) + return salt.utils.to_unicode(vdata, 'utf-8') class Registry(object): # pylint: disable=R0903 ''' - Delay '_winreg' usage until this module is used + Delay usage until this module is used ''' def __init__(self): self.hkeys = { - 'HKEY_CURRENT_USER': _winreg.HKEY_CURRENT_USER, - 'HKEY_LOCAL_MACHINE': _winreg.HKEY_LOCAL_MACHINE, - 'HKEY_USERS': _winreg.HKEY_USERS, - 'HKCU': _winreg.HKEY_CURRENT_USER, - 'HKLM': _winreg.HKEY_LOCAL_MACHINE, - 'HKU': _winreg.HKEY_USERS, + 'HKEY_CURRENT_USER': win32con.HKEY_CURRENT_USER, + 'HKEY_LOCAL_MACHINE': win32con.HKEY_LOCAL_MACHINE, + 'HKEY_USERS': win32con.HKEY_USERS, + 'HKCU': win32con.HKEY_CURRENT_USER, + 'HKLM': win32con.HKEY_LOCAL_MACHINE, + 'HKU': win32con.HKEY_USERS, } self.vtype = { - 'REG_BINARY': _winreg.REG_BINARY, - 'REG_DWORD': _winreg.REG_DWORD, - 'REG_EXPAND_SZ': _winreg.REG_EXPAND_SZ, - 'REG_MULTI_SZ': _winreg.REG_MULTI_SZ, - 'REG_SZ': _winreg.REG_SZ + 'REG_BINARY': win32con.REG_BINARY, + 'REG_DWORD': win32con.REG_DWORD, + 'REG_EXPAND_SZ': win32con.REG_EXPAND_SZ, + 'REG_MULTI_SZ': win32con.REG_MULTI_SZ, + 'REG_SZ': win32con.REG_SZ, + 'REG_QWORD': win32con.REG_QWORD } self.opttype = { - 'REG_OPTION_NON_VOLATILE': _winreg.REG_OPTION_NON_VOLATILE, - 'REG_OPTION_VOLATILE': _winreg.REG_OPTION_VOLATILE + 'REG_OPTION_NON_VOLATILE': 0, + 'REG_OPTION_VOLATILE': 1 } # Return Unicode due to from __future__ import unicode_literals self.vtype_reverse = { - _winreg.REG_BINARY: 'REG_BINARY', - _winreg.REG_DWORD: 'REG_DWORD', - _winreg.REG_EXPAND_SZ: 'REG_EXPAND_SZ', - _winreg.REG_MULTI_SZ: 'REG_MULTI_SZ', - _winreg.REG_SZ: 'REG_SZ', - # REG_QWORD isn't in the winreg library - 11: 'REG_QWORD' + win32con.REG_BINARY: 'REG_BINARY', + win32con.REG_DWORD: 'REG_DWORD', + win32con.REG_EXPAND_SZ: 'REG_EXPAND_SZ', + win32con.REG_MULTI_SZ: 'REG_MULTI_SZ', + win32con.REG_SZ: 'REG_SZ', + win32con.REG_QWORD: 'REG_QWORD' } self.opttype_reverse = { - _winreg.REG_OPTION_NON_VOLATILE: 'REG_OPTION_NON_VOLATILE', - _winreg.REG_OPTION_VOLATILE: 'REG_OPTION_VOLATILE' + 0: 'REG_OPTION_NON_VOLATILE', + 1: 'REG_OPTION_VOLATILE' } # delete_key_recursive uses this to check the subkey contains enough \ # as we do not want to remove all or most of the registry self.subkey_slash_check = { - _winreg.HKEY_CURRENT_USER: 0, - _winreg.HKEY_LOCAL_MACHINE: 1, - _winreg.HKEY_USERS: 1 + win32con.HKEY_CURRENT_USER: 0, + win32con.HKEY_LOCAL_MACHINE: 1, + win32con.HKEY_USERS: 1 } self.registry_32 = { - True: _winreg.KEY_READ | _winreg.KEY_WOW64_32KEY, - False: _winreg.KEY_READ, + True: win32con.KEY_READ | win32con.KEY_WOW64_32KEY, + False: win32con.KEY_READ, } def __getattr__(self, k): @@ -191,21 +160,16 @@ def _key_exists(hive, key, use_32bit_registry=False): :return: Returns True if found, False if not found :rtype: bool ''' - - if PY2: - local_hive = _mbcs_to_unicode(hive) - local_key = _unicode_to_mbcs(key) - else: - local_hive = hive - local_key = key + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) registry = Registry() hkey = registry.hkeys[local_hive] access_mask = registry.registry_32[use_32bit_registry] try: - handle = _winreg.OpenKey(hkey, local_key, 0, access_mask) - _winreg.CloseKey(handle) + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) + win32api.RegCloseKey(handle) return True except WindowsError: # pylint: disable=E0602 return False @@ -224,7 +188,10 @@ def broadcast_change(): salt '*' reg.broadcast_change ''' # https://msdn.microsoft.com/en-us/library/windows/desktop/ms644952(v=vs.85).aspx - return bool(SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0)) + _, res = win32gui.SendMessageTimeout( + win32con.HWND_BROADCAST, win32con.WM_SETTINGCHANGE, 0, 0, + win32con.SMTO_ABORTIFHUNG, 5000) + return not bool(res) def list_keys(hive, key=None, use_32bit_registry=False): @@ -253,12 +220,8 @@ def list_keys(hive, key=None, use_32bit_registry=False): salt '*' reg.list_keys HKLM 'SOFTWARE' ''' - if PY2: - local_hive = _mbcs_to_unicode(hive) - local_key = _unicode_to_mbcs(key) - else: - local_hive = hive - local_key = key + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) registry = Registry() hkey = registry.hkeys[local_hive] @@ -266,12 +229,12 @@ def list_keys(hive, key=None, use_32bit_registry=False): subkeys = [] try: - handle = _winreg.OpenKey(hkey, local_key, 0, access_mask) + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) - for i in range(_winreg.QueryInfoKey(handle)[0]): - subkey = _winreg.EnumKey(handle, i) + for i in range(win32api.RegQueryInfoKey(handle)[0]): + subkey = win32api.RegEnumKey(handle, i) if PY2: - subkeys.append(_mbcs_to_unicode(subkey)) + subkeys.append(_to_unicode(subkey)) else: subkeys.append(subkey) @@ -312,13 +275,8 @@ def list_values(hive, key=None, use_32bit_registry=False, include_default=True): salt '*' reg.list_values HKLM 'SYSTEM\\CurrentControlSet\\Services\\Tcpip' ''' - - if PY2: - local_hive = _mbcs_to_unicode(hive) - local_key = _unicode_to_mbcs(key) - else: - local_hive = hive - local_key = key + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) registry = Registry() hkey = registry.hkeys[local_hive] @@ -327,37 +285,21 @@ def list_values(hive, key=None, use_32bit_registry=False, include_default=True): values = list() try: - handle = _winreg.OpenKey(hkey, local_key, 0, access_mask) + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) - for i in range(_winreg.QueryInfoKey(handle)[1]): - vname, vdata, vtype = _winreg.EnumValue(handle, i) + for i in range(win32api.RegQueryInfoKey(handle)[1]): + vname, vdata, vtype = win32api.RegEnumValue(handle, i) + + if not vname: + vname = "(Default)" value = {'hive': local_hive, 'key': local_key, - 'vname': vname, - 'vdata': vdata, + 'vname': _to_mbcs(vname), + 'vdata': _to_mbcs(vdata), 'vtype': registry.vtype_reverse[vtype], 'success': True} values.append(value) - if include_default: - # Get the default value for the key - value = {'hive': local_hive, - 'key': local_key, - 'vname': '(Default)', - 'vdata': None, - 'success': True} - try: - # QueryValueEx returns unicode data - vdata, vtype = _winreg.QueryValueEx(handle, '(Default)') - if vdata or vdata in [0, '']: - value['vtype'] = registry.vtype_reverse[vtype] - value['vdata'] = vdata - else: - value['comment'] = 'Empty Value' - except WindowsError: # pylint: disable=E0602 - value['vdata'] = ('(value not set)') - value['vtype'] = 'REG_SZ' - values.append(value) except WindowsError as exc: # pylint: disable=E0602 log.debug(exc) log.debug(r'Cannot find key: {0}\{1}'.format(hive, key)) @@ -403,30 +345,19 @@ def read_value(hive, key, vname=None, use_32bit_registry=False): salt '*' reg.read_value HKEY_LOCAL_MACHINE 'SOFTWARE\Salt' 'version' ''' - # If no name is passed, the default value of the key will be returned # The value name is Default # Setup the return array - if PY2: - ret = {'hive': _mbcs_to_unicode(hive), - 'key': _mbcs_to_unicode(key), - 'vname': _mbcs_to_unicode(vname), - 'vdata': None, - 'success': True} - local_hive = _mbcs_to_unicode(hive) - local_key = _unicode_to_mbcs(key) - local_vname = _unicode_to_mbcs(vname) + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + local_vname = _to_unicode(vname) - else: - ret = {'hive': hive, - 'key': key, - 'vname': vname, - 'vdata': None, - 'success': True} - local_hive = hive - local_key = key - local_vname = vname + ret = {'hive': local_hive, + 'key': local_key, + 'vname': local_vname, + 'vdata': None, + 'success': True} if not vname: ret['vname'] = '(Default)' @@ -436,19 +367,22 @@ def read_value(hive, key, vname=None, use_32bit_registry=False): access_mask = registry.registry_32[use_32bit_registry] try: - handle = _winreg.OpenKey(hkey, local_key, 0, access_mask) + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) try: - # QueryValueEx returns unicode data - vdata, vtype = _winreg.QueryValueEx(handle, local_vname) + # RegQueryValueEx returns and accepts unicode data + vdata, vtype = win32api.RegQueryValueEx(handle, local_vname) if vdata or vdata in [0, '']: ret['vtype'] = registry.vtype_reverse[vtype] - ret['vdata'] = vdata + if vtype == 7: + ret['vdata'] = [_to_mbcs(i) for i in vdata] + else: + ret['vdata'] = _to_mbcs(vdata) else: ret['comment'] = 'Empty Value' except WindowsError: # pylint: disable=E0602 ret['vdata'] = ('(value not set)') ret['vtype'] = 'REG_SZ' - except WindowsError as exc: # pylint: disable=E0602 + except pywintypes.error as exc: # pylint: disable=E0602 log.debug(exc) log.debug('Cannot find key: {0}\\{1}'.format(local_hive, local_key)) ret['comment'] = 'Cannot find key: {0}\\{1}'.format(local_hive, local_key) @@ -555,42 +489,47 @@ def set_value(hive, salt '*' reg.set_value HKEY_LOCAL_MACHINE 'SOFTWARE\\Salt' 'version' '2015.5.2' \\ vtype=REG_LIST vdata='[a,b,c]' ''' - - if PY2: - try: - local_hive = _mbcs_to_unicode(hive) - local_key = _mbcs_to_unicode(key) - local_vname = _mbcs_to_unicode(vname) - local_vtype = _mbcs_to_unicode(vtype) - local_vdata = _mbcs_to_unicode_wrap(vdata, local_vtype) - except TypeError as exc: # pylint: disable=E0602 - log.error(exc, exc_info=True) - return False - else: - local_hive = hive - local_key = key - local_vname = vname - local_vdata = vdata - local_vtype = vtype + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + local_vname = _to_unicode(vname) + local_vtype = _to_unicode(vtype) registry = Registry() hkey = registry.hkeys[local_hive] vtype_value = registry.vtype[local_vtype] - access_mask = registry.registry_32[use_32bit_registry] | _winreg.KEY_ALL_ACCESS + access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS + + # Check data type and cast to expected type + # int will automatically become long on 64bit numbers + # https://www.python.org/dev/peps/pep-0237/ + + # String Types to Unicode + if vtype_value in [1, 2]: + local_vdata = _to_unicode(vdata) + # Don't touch binary... + elif vtype_value == 3: + local_vdata = vdata + # Make sure REG_MULTI_SZ is a list of strings + elif vtype_value == 7: + local_vdata = [_to_unicode(i) for i in vdata] + # Everything else is int + else: + local_vdata = int(vdata) + if volatile: create_options = registry.opttype['REG_OPTION_VOLATILE'] else: create_options = registry.opttype['REG_OPTION_NON_VOLATILE'] try: - handle, _ = RegCreateKeyEx(hkey, local_key, access_mask, + handle, _ = win32api.RegCreateKeyEx(hkey, local_key, access_mask, Options=create_options) - RegSetValueEx(handle, local_vname, 0, vtype_value, local_vdata) - RegFlushKey(handle) - RegCloseKey(handle) + win32api.RegSetValueEx(handle, local_vname, 0, vtype_value, local_vdata) + win32api.RegFlushKey(handle) + win32api.RegCloseKey(handle) broadcast_change() return True - except (win32apiError, SystemError, ValueError, TypeError) as exc: # pylint: disable=E0602 + except (win32api.error, SystemError, ValueError, TypeError) as exc: # pylint: disable=E0602 log.error(exc, exc_info=True) return False @@ -626,18 +565,14 @@ def delete_key_recursive(hive, key, use_32bit_registry=False): salt '*' reg.delete_key_recursive HKLM SOFTWARE\\salt ''' - if PY2: - local_hive = _mbcs_to_unicode(hive) - local_key = _unicode_to_mbcs(key) - else: - local_hive = hive - local_key = key + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) # Instantiate the registry object registry = Registry() hkey = registry.hkeys[local_hive] key_path = local_key - access_mask = registry.registry_32[use_32bit_registry] | _winreg.KEY_ALL_ACCESS + access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS if not _key_exists(local_hive, local_key, use_32bit_registry): return False @@ -654,17 +589,17 @@ def delete_key_recursive(hive, key, use_32bit_registry=False): i = 0 while True: try: - subkey = _winreg.EnumKey(_key, i) + subkey = win32api.RegEnumKey(_key, i) yield subkey i += 1 - except WindowsError: # pylint: disable=E0602 + except pywintypes.error: # pylint: disable=E0602 break def _traverse_registry_tree(_hkey, _keypath, _ret, _access_mask): ''' Traverse the registry tree i.e. dive into the tree ''' - _key = _winreg.OpenKey(_hkey, _keypath, 0, _access_mask) + _key = win32api.RegOpenKeyEx(_hkey, _keypath, 0, _access_mask) for subkeyname in _subkeys(_key): subkeypath = r'{0}\{1}'.format(_keypath, subkeyname) _ret = _traverse_registry_tree(_hkey, subkeypath, _ret, access_mask) @@ -683,8 +618,8 @@ def delete_key_recursive(hive, key, use_32bit_registry=False): # Delete all sub_keys for sub_key_path in key_list: try: - key_handle = _winreg.OpenKey(hkey, sub_key_path, 0, access_mask) - _winreg.DeleteKey(key_handle, '') + key_handle = win32api.RegOpenKeyEx(hkey, sub_key_path, 0, access_mask) + win32api.RegDeleteKey(key_handle, '') ret['Deleted'].append(r'{0}\{1}'.format(hive, sub_key_path)) except WindowsError as exc: # pylint: disable=E0602 log.error(exc, exc_info=True) @@ -723,23 +658,18 @@ def delete_value(hive, key, vname=None, use_32bit_registry=False): salt '*' reg.delete_value HKEY_CURRENT_USER 'SOFTWARE\\Salt' 'version' ''' - if PY2: - local_hive = _mbcs_to_unicode(hive) - local_key = _unicode_to_mbcs(key) - local_vname = _unicode_to_mbcs(vname) - else: - local_hive = hive - local_key = key - local_vname = vname + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + local_vname = _to_unicode(vname) registry = Registry() hkey = registry.hkeys[local_hive] - access_mask = registry.registry_32[use_32bit_registry] | _winreg.KEY_ALL_ACCESS + access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS try: - handle = _winreg.OpenKey(hkey, local_key, 0, access_mask) - _winreg.DeleteValue(handle, local_vname) - _winreg.CloseKey(handle) + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) + win32api.RegDeleteValue(handle, local_vname) + win32api.RegCloseKey(handle) broadcast_change() return True except WindowsError as exc: # pylint: disable=E0602 diff --git a/salt/modules/state.py b/salt/modules/state.py index 4679c6f6f3..b90d4b7f3e 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -493,6 +493,18 @@ def apply_(mods=None, Values passed this way will override Pillar values set via ``pillar_roots`` or an external Pillar source. + exclude + Exclude specific states from execution. Accepts a list of sls names, a + comma-separated string of sls names, or a list of dictionaries + containing ``sls`` or ``id`` keys. Glob-patterns may be used to match + multiple states. + + .. code-block:: bash + + salt '*' state.apply exclude=bar,baz + salt '*' state.apply exclude=foo* + salt '*' state.apply exclude="[{'id': 'id_to_exclude'}, {'sls': 'sls_to_exclude'}]" + queue : False Instead of failing immediately when another state run is in progress, queue the new state run to begin running once the other has finished. @@ -758,6 +770,18 @@ def highstate(test=None, queue=False, **kwargs): .. versionadded:: 2016.3.0 + exclude + Exclude specific states from execution. Accepts a list of sls names, a + comma-separated string of sls names, or a list of dictionaries + containing ``sls`` or ``id`` keys. Glob-patterns may be used to match + multiple states. + + .. code-block:: bash + + salt '*' state.higstate exclude=bar,baz + salt '*' state.higstate exclude=foo* + salt '*' state.highstate exclude="[{'id': 'id_to_exclude'}, {'sls': 'sls_to_exclude'}]" + saltenv Specify a salt fileserver environment to be used when applying states @@ -935,6 +959,18 @@ def sls(mods, test=None, exclude=None, queue=False, **kwargs): .. versionadded:: 2016.3.0 + exclude + Exclude specific states from execution. Accepts a list of sls names, a + comma-separated string of sls names, or a list of dictionaries + containing ``sls`` or ``id`` keys. Glob-patterns may be used to match + multiple states. + + .. code-block:: bash + + salt '*' state.sls foo,bar,baz exclude=bar,baz + salt '*' state.sls foo,bar,baz exclude=ba* + salt '*' state.sls foo,bar,baz exclude="[{'id': 'id_to_exclude'}, {'sls': 'sls_to_exclude'}]" + queue : False Instead of failing immediately when another state run is in progress, queue the new state run to begin running once the other has finished. diff --git a/salt/modules/win_file.py b/salt/modules/win_file.py index 461fb5310d..0894e1ef7d 100644 --- a/salt/modules/win_file.py +++ b/salt/modules/win_file.py @@ -58,7 +58,7 @@ from salt.modules.file import (check_hash, # pylint: disable=W0611 lstat, path_exists_glob, write, pardir, join, HASHES, HASHES_REVMAP, comment, uncomment, _add_flags, comment_line, _regex_to_static, _get_line_indent, apply_template_on_contents, dirname, basename, - list_backups_dir) + list_backups_dir, _assert_occurrence, _starts_till) from salt.modules.file import normpath as normpath_ from salt.utils import namespaced_function as _namespaced_function @@ -116,7 +116,7 @@ def __virtual__(): global write, pardir, join, _add_flags, apply_template_on_contents global path_exists_glob, comment, uncomment, _mkstemp_copy global _regex_to_static, _get_line_indent, dirname, basename - global list_backups_dir, normpath_ + global list_backups_dir, normpath_, _assert_occurrence, _starts_till replace = _namespaced_function(replace, globals()) search = _namespaced_function(search, globals()) @@ -179,6 +179,8 @@ def __virtual__(): basename = _namespaced_function(basename, globals()) list_backups_dir = _namespaced_function(list_backups_dir, globals()) normpath_ = _namespaced_function(normpath_, globals()) + _assert_occurrence = _namespaced_function(_assert_occurrence, globals()) + _starts_till = _namespaced_function(_starts_till, globals()) else: return False, 'Module win_file: Missing Win32 modules' diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index c8f699a7d6..ba6ea214c0 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -39,10 +39,11 @@ import logging import os import re import time +import sys from functools import cmp_to_key # Import third party libs -import salt.ext.six as six +from salt.ext import six # pylint: disable=import-error,no-name-in-module from salt.ext.six.moves.urllib.parse import urlparse as _urlparse @@ -50,9 +51,12 @@ from salt.ext.six.moves.urllib.parse import urlparse as _urlparse from salt.exceptions import (CommandExecutionError, SaltInvocationError, SaltRenderError) -import salt.utils -import salt.utils.pkg +import salt.utils # Can be removed once is_true, get_hash, compare_dicts are moved +import salt.utils.args +import salt.utils.files import salt.utils.path +import salt.utils.pkg +import salt.utils.versions import salt.syspaths import salt.payload from salt.exceptions import MinionError @@ -99,7 +103,7 @@ def latest_version(*names, **kwargs): salt '*' pkg.latest_version salt '*' pkg.latest_version ... ''' - if len(names) == 0: + if not names: return '' # Initialize the return dict with empty strings @@ -124,6 +128,8 @@ def latest_version(*names, **kwargs): if name in installed_pkgs: log.trace('Determining latest installed version of %s', name) try: + # installed_pkgs[name] Can be version number or 'Not Found' + # 'Not Found' occurs when version number is not found in the registry latest_installed = sorted( installed_pkgs[name], key=cmp_to_key(_reverse_cmp_pkg_versions) @@ -140,6 +146,8 @@ def latest_version(*names, **kwargs): # get latest available (from winrepo_dir) version of package pkg_info = _get_package_info(name, saltenv=saltenv) log.trace('Raw winrepo pkg_info for {0} is {1}'.format(name, pkg_info)) + + # latest_available can be version number or 'latest' or even 'Not Found' latest_available = _get_latest_pkg_version(pkg_info) if latest_available: log.debug('Latest available version ' @@ -147,9 +155,9 @@ def latest_version(*names, **kwargs): # check, whether latest available version # is newer than latest installed version - if salt.utils.compare_versions(ver1=str(latest_available), - oper='>', - ver2=str(latest_installed)): + if compare_versions(ver1=str(latest_available), + oper='>', + ver2=str(latest_installed)): log.debug('Upgrade of {0} from {1} to {2} ' 'is available'.format(name, latest_installed, @@ -188,10 +196,9 @@ def upgrade_available(name, **kwargs): # same default as latest_version refresh = salt.utils.is_true(kwargs.get('refresh', True)) - current = version(name, saltenv=saltenv, refresh=refresh).get(name) - latest = latest_version(name, saltenv=saltenv, refresh=False) - - return compare_versions(latest, '>', current) + # if latest_version returns blank, the latest version is already installed or + # their is no package definition. This is a salt standard which could be improved. + return latest_version(name, saltenv=saltenv, refresh=refresh) != '' def list_upgrades(refresh=True, **kwargs): @@ -222,9 +229,13 @@ def list_upgrades(refresh=True, **kwargs): pkgs = {} for pkg in installed_pkgs: if pkg in available_pkgs: + # latest_version() will be blank if the latest version is installed. + # or the package name is wrong. Given we check available_pkgs, this + # should not be the case of wrong package name. + # Note: latest_version() is an expensive way to do this as it + # calls list_pkgs each time. latest_ver = latest_version(pkg, refresh=False, saltenv=saltenv) - install_ver = installed_pkgs[pkg] - if compare_versions(latest_ver, '>', install_ver): + if latest_ver: pkgs[pkg] = latest_ver return pkgs @@ -241,7 +252,7 @@ def list_available(*names, **kwargs): saltenv (str): The salt environment to use. Default ``base``. - refresh (bool): Refresh package metadata. Default ``True``. + refresh (bool): Refresh package metadata. Default ``False``. return_dict_always (bool): Default ``False`` dict when a single package name is queried. @@ -264,7 +275,7 @@ def list_available(*names, **kwargs): return '' saltenv = kwargs.get('saltenv', 'base') - refresh = salt.utils.is_true(kwargs.get('refresh', True)) + refresh = salt.utils.is_true(kwargs.get('refresh', False)) return_dict_always = \ salt.utils.is_true(kwargs.get('return_dict_always', False)) @@ -293,7 +304,9 @@ def list_available(*names, **kwargs): def version(*names, **kwargs): ''' - Returns a version if the package is installed, else returns an empty string + Returns a string representing the package version or an empty string if not + installed. If more than one package name is specified, a dict of + name/version pairs is returned. Args: name (str): One or more package names @@ -303,10 +316,12 @@ def version(*names, **kwargs): refresh (bool): Refresh package metadata. Default ``False``. Returns: + str: version string when a single package is specified. dict: The package name(s) with the installed versions. - .. code-block:: cfg + .. code-block:: cfg + {['', '', ]} OR {'': ['', '', ]} CLI Example: @@ -315,19 +330,25 @@ def version(*names, **kwargs): salt '*' pkg.version salt '*' pkg.version - ''' - saltenv = kwargs.get('saltenv', 'base') - installed_pkgs = list_pkgs(refresh=kwargs.get('refresh', False)) - available_pkgs = get_repo_data(saltenv).get('repo') + ''' + # Standard is return empty string even if not a valid name + # TODO: Look at returning an error across all platforms with + # CommandExecutionError(msg,info={'errors': errors }) + # available_pkgs = get_repo_data(saltenv).get('repo') + # for name in names: + # if name in available_pkgs: + # ret[name] = installed_pkgs.get(name, '') + + saltenv = kwargs.get('saltenv', 'base') + installed_pkgs = list_pkgs(saltenv=saltenv, refresh=kwargs.get('refresh', False)) + + if len(names) == 1: + return installed_pkgs.get(names[0], '') ret = {} for name in names: - if name in available_pkgs: - ret[name] = installed_pkgs.get(name, '') - else: - ret[name] = 'not available' - + ret[name] = installed_pkgs.get(name, '') return ret @@ -336,7 +357,6 @@ def list_pkgs(versions_as_list=False, **kwargs): List the packages currently installed Args: - version_as_list (bool): Returns the versions as a list Kwargs: saltenv (str): The salt environment to use. Default ``base``. @@ -424,7 +444,7 @@ def _get_reg_software(): '(value not set)', '', None] - #encoding = locale.getpreferredencoding() + reg_software = {} hive = 'HKLM' @@ -462,7 +482,7 @@ def _get_reg_software(): def _refresh_db_conditional(saltenv, **kwargs): ''' Internal use only in this module, has a different set of defaults and - returns True or False. And supports check the age of the existing + returns True or False. And supports checking the age of the existing generated metadata db, as well as ensure metadata db exists to begin with Args: @@ -476,8 +496,7 @@ def _refresh_db_conditional(saltenv, **kwargs): failhard (bool): If ``True``, an error will be raised if any repo SLS files failed to - process. If ``False``, no error will be raised, and a dictionary - containing the full results will be returned. + process. Returns: bool: True Fetched or Cache uptodate, False to indicate an issue @@ -695,8 +714,8 @@ def genrepo(**kwargs): verbose (bool): Return verbose data structure which includes 'success_list', a list - of all sls files and the package names contained within. Default - 'False' + of all sls files and the package names contained within. + Default ``False``. failhard (bool): If ``True``, an error will be raised if any repo SLS files failed @@ -739,11 +758,13 @@ def genrepo(**kwargs): successful_verbose ) serial = salt.payload.Serial(__opts__) + # TODO: 2016.11 has PY2 mode as 'w+b' develop has 'w+' ? PY3 is 'wb+' + # also the reading of this is 'rb' in get_repo_data() mode = 'w+' if six.PY2 else 'wb+' + with salt.utils.fopen(repo_details.winrepo_file, mode) as repo_cache: repo_cache.write(serial.dumps(ret)) - # save reading it back again. ! this breaks due to utf8 issues - #__context__['winrepo.data'] = ret + # For some reason we can not save ret into __context__['winrepo.data'] as this breaks due to utf8 issues successful_count = len(successful_verbose) error_count = len(ret['errors']) if verbose: @@ -778,7 +799,7 @@ def genrepo(**kwargs): return results -def _repo_process_pkg_sls(file, short_path_name, ret, successful_verbose): +def _repo_process_pkg_sls(filename, short_path_name, ret, successful_verbose): renderers = salt.loader.render(__opts__, __salt__) def _failed_compile(msg): @@ -788,7 +809,7 @@ def _repo_process_pkg_sls(file, short_path_name, ret, successful_verbose): try: config = salt.template.compile_template( - file, + filename, renderers, __opts__['renderer'], __opts__.get('renderer_blacklist', ''), @@ -803,7 +824,6 @@ def _repo_process_pkg_sls(file, short_path_name, ret, successful_verbose): if config: revmap = {} errors = [] - pkgname_ok_list = [] for pkgname, versions in six.iteritems(config): if pkgname in ret['repo']: log.error( @@ -812,12 +832,12 @@ def _repo_process_pkg_sls(file, short_path_name, ret, successful_verbose): ) errors.append('package \'{0}\' already defined'.format(pkgname)) break - for version, repodata in six.iteritems(versions): + for version_str, repodata in six.iteritems(versions): # Ensure version is a string/unicode - if not isinstance(version, six.string_types): + if not isinstance(version_str, six.string_types): msg = ( 'package \'{0}\'{{0}}, version number {1} ' - 'is not a string'.format(pkgname, version) + 'is not a string'.format(pkgname, version_str) ) log.error( msg.format(' within \'{0}\''.format(short_path_name)) @@ -829,7 +849,7 @@ def _repo_process_pkg_sls(file, short_path_name, ret, successful_verbose): msg = ( 'package \'{0}\'{{0}}, repo data for ' 'version number {1} is not defined as a dictionary ' - .format(pkgname, version) + .format(pkgname, version_str) ) log.error( msg.format(' within \'{0}\''.format(short_path_name)) @@ -840,8 +860,6 @@ def _repo_process_pkg_sls(file, short_path_name, ret, successful_verbose): if errors: ret.setdefault('errors', {})[short_path_name] = errors else: - if pkgname not in pkgname_ok_list: - pkgname_ok_list.append(pkgname) ret.setdefault('repo', {}).update(config) ret.setdefault('name_map', {}).update(revmap) successful_verbose[short_path_name] = config.keys() @@ -916,7 +934,8 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): to install. (no spaces after the commas) refresh (bool): - Boolean value representing whether or not to refresh the winrepo db + Boolean value representing whether or not to refresh the winrepo db. + Default ``False``. pkgs (list): A list of packages to install from a software repository. All @@ -1072,7 +1091,7 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): for pkg in pkg_params: pkg_params[pkg] = {'version': pkg_params[pkg]} - if pkg_params is None or len(pkg_params) == 0: + if not pkg_params: log.error('No package definition found') return {} @@ -1114,11 +1133,12 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): version_num = str(version_num) if not version_num: + # following can be version number or latest version_num = _get_latest_pkg_version(pkginfo) # Check if the version is already installed if version_num in old.get(pkg_name, '').split(',') \ - or (old.get(pkg_name) == 'Not Found'): + or (old.get(pkg_name, '') == 'Not Found'): # Desired version number already installed ret[pkg_name] = {'current': version_num} continue @@ -1244,32 +1264,32 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): options.get('extra_install_flags', '') ) - #Compute msiexec string + # Compute msiexec string use_msiexec, msiexec = _get_msiexec(pkginfo[version_num].get('msiexec', False)) # Build cmd and arguments # cmd and arguments must be separated for use with the task scheduler + cmd_shell = os.getenv('ComSpec', '{0}\\system32\\cmd.exe'.format(os.getenv('WINDIR'))) if use_msiexec: - cmd = msiexec - arguments = ['/i', cached_pkg] + arguments = '"{0}" /I "{1}"'.format(msiexec, cached_pkg) if pkginfo[version_num].get('allusers', True): - arguments.append('ALLUSERS="1"') - arguments.extend(salt.utils.shlex_split(install_flags, posix=False)) + arguments = '{0} ALLUSERS=1'.format(arguments) else: - cmd = cached_pkg - arguments = salt.utils.shlex_split(install_flags, posix=False) + arguments = '"{0}"'.format(cached_pkg) + + if install_flags: + arguments = '{0} {1}'.format(arguments, install_flags) # Install the software # Check Use Scheduler Option if pkginfo[version_num].get('use_scheduler', False): - # Create Scheduled Task __salt__['task.create_task'](name='update-salt-software', user_name='System', force=True, action_type='Execute', - cmd=cmd, - arguments=' '.join(arguments), + cmd=cmd_shell, + arguments='/s /c "{0}"'.format(arguments), start_in=cache_path, trigger_type='Once', start_date='1975-01-01', @@ -1312,15 +1332,13 @@ def install(name=None, refresh=False, pkgs=None, **kwargs): ret[pkg_name] = {'install status': 'failed'} else: - # Combine cmd and arguments - cmd = [cmd] - cmd.extend(arguments) - # Launch the command - result = __salt__['cmd.run_all'](cmd, - cache_path, - python_shell=False, - redirect_stderr=True) + result = __salt__['cmd.run_all']( + '"{0}" /s /c "{1}"'.format(cmd_shell, arguments), + cache_path, + output_loglevel='trace', + python_shell=False, + redirect_stderr=True) if not result['retcode']: ret[pkg_name] = {'install status': 'success'} changed.append(pkg_name) @@ -1397,14 +1415,17 @@ def remove(name=None, pkgs=None, version=None, **kwargs): .. versionadded:: 0.16.0 Args: - name (str): The name(s) of the package(s) to be uninstalled. Can be a - single package or a comma delimted list of packages, no spaces. + name (str): + The name(s) of the package(s) to be uninstalled. Can be a + single package or a comma delimited list of packages, no spaces. + version (str): The version of the package to be uninstalled. If this option is used to to uninstall multiple packages, then this version will be applied to all targeted packages. Recommended using only when uninstalling a single package. If this parameter is omitted, the latest version will be uninstalled. + pkgs (list): A list of packages to delete. Must be passed as a python list. The ``name`` parameter will be ignored if this option is passed. @@ -1541,6 +1562,7 @@ def remove(name=None, pkgs=None, version=None, **kwargs): # Compare the hash of the cached installer to the source only if # the file is hosted on salt: + # TODO cp.cache_file does cache and hash checking? So why do it again? if uninstaller.startswith('salt:'): if __salt__['cp.hash_file'](uninstaller, saltenv) != \ __salt__['cp.hash_file'](cached_pkg): @@ -1566,6 +1588,7 @@ def remove(name=None, pkgs=None, version=None, **kwargs): # Get parameters for cmd expanded_cached_pkg = str(os.path.expandvars(cached_pkg)) + expanded_cache_path = str(os.path.expandvars(cache_path)) # Get uninstall flags uninstall_flags = pkginfo[target].get('uninstall_flags', '') @@ -1574,31 +1597,31 @@ def remove(name=None, pkgs=None, version=None, **kwargs): uninstall_flags = '{0} {1}'.format( uninstall_flags, kwargs.get('extra_uninstall_flags', '')) - #Compute msiexec string + # Compute msiexec string use_msiexec, msiexec = _get_msiexec(pkginfo[target].get('msiexec', False)) + cmd_shell = os.getenv('ComSpec', '{0}\\system32\\cmd.exe'.format(os.getenv('WINDIR'))) # 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)) + arguments = '"{0}" /X "{1}"'.format(msiexec, uninstaller if uninstaller else expanded_cached_pkg) else: - cmd = expanded_cached_pkg - arguments = salt.utils.shlex_split(uninstall_flags, posix=False) + arguments = '"{0}"'.format(expanded_cached_pkg) + + if uninstall_flags: + arguments = '{0} {1}'.format(arguments, uninstall_flags) # Uninstall the software # Check Use Scheduler Option if pkginfo[target].get('use_scheduler', False): - # Create Scheduled Task __salt__['task.create_task'](name='update-salt-software', user_name='System', force=True, action_type='Execute', - cmd=cmd, - arguments=' '.join(arguments), - start_in=cache_path, + cmd=cmd_shell, + arguments='/s /c "{0}"'.format(arguments), + start_in=expanded_cache_path, trigger_type='Once', start_date='1975-01-01', start_time='01:00', @@ -1610,13 +1633,11 @@ def remove(name=None, pkgs=None, version=None, **kwargs): log.error('Scheduled Task failed to run') ret[pkgname] = {'uninstall status': 'failed'} else: - # Build the install command - cmd = [cmd] - cmd.extend(arguments) - # Launch the command result = __salt__['cmd.run_all']( - cmd, + '"{0}" /s /c "{1}"'.format(cmd_shell, arguments), + expanded_cache_path, + output_loglevel='trace', python_shell=False, redirect_stderr=True) if not result['retcode']: @@ -1662,11 +1683,13 @@ def purge(name=None, pkgs=None, version=None, **kwargs): name (str): The name of the package to be deleted. - version (str): The version of the package to be deleted. If this option - is used in combination with the ``pkgs`` option below, then this + version (str): + The version of the package to be deleted. If this option is + used in combination with the ``pkgs`` option below, then this version will be applied to all targeted packages. - pkgs (list): A list of packages to delete. Must be passed as a python + pkgs (list): + A list of packages to delete. Must be passed as a python list. The ``name`` parameter will be ignored if this option is passed. @@ -1800,4 +1823,20 @@ def compare_versions(ver1='', oper='==', ver2=''): salt '*' pkg.compare_versions 1.2 >= 1.3 ''' - return salt.utils.compare_versions(ver1, oper, ver2) + if not ver1: + raise SaltInvocationError('compare_version, ver1 is blank') + if not ver2: + raise SaltInvocationError('compare_version, ver2 is blank') + + # Support version being the special meaning of 'latest' + if ver1 == 'latest': + ver1 = str(sys.maxsize) + if ver2 == 'latest': + ver2 = str(sys.maxsize) + # Support version being the special meaning of 'Not Found' + if ver1 == 'Not Found': + ver1 = '0.0.0.0.0' + if ver2 == 'Not Found': + ver2 = '0.0.0.0.0' + + return salt.utils.compare_versions(ver1, oper, ver2, ignore_epoch=True) diff --git a/salt/output/__init__.py b/salt/output/__init__.py index 8a1732c512..5137c278e8 100644 --- a/salt/output/__init__.py +++ b/salt/output/__init__.py @@ -143,6 +143,17 @@ def get_printout(out, opts=None, **kwargs): # See Issue #29796 for more information. out = opts['output'] + # Handle setting the output when --static is passed. + if not out and opts.get('static'): + if opts.get('output'): + out = opts['output'] + elif opts.get('fun', '').split('.')[0] == 'state': + # --static doesn't have an output set at this point, but if we're + # running a state function and "out" hasn't already been set, we + # should set the out variable to "highstate". Otherwise state runs + # are set to "nested" below. See Issue #44556 for more information. + out = 'highstate' + if out == 'text': out = 'txt' elif out is None or out == '': diff --git a/salt/returners/pgjsonb.py b/salt/returners/pgjsonb.py index 126b29abae..5029c5875d 100644 --- a/salt/returners/pgjsonb.py +++ b/salt/returners/pgjsonb.py @@ -254,14 +254,14 @@ def returner(ret): with _get_serv(ret, commit=True) as cur: sql = '''INSERT INTO salt_returns (fun, jid, return, id, success, full_ret, alter_time) - VALUES (%s, %s, %s, %s, %s, %s, %s)''' + VALUES (%s, %s, %s, %s, %s, %s, to_timestamp(%s))''' cur.execute(sql, (ret['fun'], ret['jid'], psycopg2.extras.Json(ret['return']), ret['id'], ret.get('success', False), psycopg2.extras.Json(ret), - time.strftime('%Y-%m-%d %H:%M:%S %z', time.localtime()))) + time.time())) except salt.exceptions.SaltMasterError: log.critical('Could not store return with pgjsonb returner. PostgreSQL server unavailable.') @@ -278,9 +278,9 @@ def event_return(events): tag = event.get('tag', '') data = event.get('data', '') sql = '''INSERT INTO salt_events (tag, data, master_id, alter_time) - VALUES (%s, %s, %s, %s)''' + VALUES (%s, %s, %s, to_timestamp(%s))''' cur.execute(sql, (tag, psycopg2.extras.Json(data), - __opts__['id'], time.strftime('%Y-%m-%d %H:%M:%S %z', time.localtime()))) + __opts__['id'], time.time())) def save_load(jid, load, minions=None): diff --git a/salt/state.py b/salt/state.py index ca694f3e6e..17a57a51a7 100644 --- a/salt/state.py +++ b/salt/state.py @@ -686,7 +686,7 @@ class State(object): except AttributeError: pillar_enc = str(pillar_enc).lower() self._pillar_enc = pillar_enc - if initial_pillar is not None: + if initial_pillar: self.opts['pillar'] = initial_pillar if self._pillar_override: self.opts['pillar'] = salt.utils.dictupdate.merge( diff --git a/salt/states/reg.py b/salt/states/reg.py index b752c8bac2..d9bc8a60e5 100644 --- a/salt/states/reg.py +++ b/salt/states/reg.py @@ -59,6 +59,7 @@ from __future__ import absolute_import # Import python libs import logging +import salt.utils log = logging.getLogger(__name__) @@ -186,13 +187,14 @@ def present(name, use_32bit_registry=use_32bit_registry) if vdata == reg_current['vdata'] and reg_current['success']: - ret['comment'] = '{0} in {1} is already configured'.\ - format(vname if vname else '(Default)', name) + ret['comment'] = u'{0} in {1} is already configured' \ + ''.format(salt.utils.to_unicode(vname, 'utf-8') if vname else u'(Default)', + salt.utils.to_unicode(name, 'utf-8')) return ret add_change = {'Key': r'{0}\{1}'.format(hive, key), - 'Entry': '{0}'.format(vname if vname else '(Default)'), - 'Value': '{0}'.format(vdata)} + 'Entry': u'{0}'.format(salt.utils.to_unicode(vname, 'utf-8') if vname else u'(Default)'), + 'Value': salt.utils.to_unicode(vdata, 'utf-8')} # Check for test option if __opts__['test']: diff --git a/salt/states/win_path.py b/salt/states/win_path.py index a0815825f6..c9537d3e77 100644 --- a/salt/states/win_path.py +++ b/salt/states/win_path.py @@ -65,7 +65,8 @@ def exists(name, index=None): ''' Add the directory to the system PATH at index location - index: where the directory should be placed in the PATH (default: None) + index: where the directory should be placed in the PATH (default: None). + This is 0-indexed, so 0 means to prepend at the very start of the PATH. [Note: Providing no index will append directory to PATH and will not enforce its location within the PATH.] @@ -96,7 +97,7 @@ def exists(name, index=None): try: currIndex = sysPath.index(path) - if index: + if index is not None: index = int(index) if index < 0: index = len(sysPath) + index + 1 @@ -115,7 +116,7 @@ def exists(name, index=None): except ValueError: pass - if not index: + if index is None: index = len(sysPath) # put it at the end ret['changes']['added'] = '{0} will be added at index {1}'.format(name, index) if __opts__['test']: diff --git a/salt/utils/atomicfile.py b/salt/utils/atomicfile.py index 2aab01c31a..f2b2781d18 100644 --- a/salt/utils/atomicfile.py +++ b/salt/utils/atomicfile.py @@ -120,6 +120,8 @@ class _AtomicWFile(object): self._fh.close() if os.path.isfile(self._filename): shutil.copymode(self._filename, self._tmp_filename) + st = os.stat(self._filename) + os.chown(self._tmp_filename, st.st_uid, st.st_gid) atomic_rename(self._tmp_filename, self._filename) def __exit__(self, exc_type, exc_value, traceback): diff --git a/salt/utils/files.py b/salt/utils/files.py index 900c2795bc..6f4ac70d55 100644 --- a/salt/utils/files.py +++ b/salt/utils/files.py @@ -39,6 +39,16 @@ HASHES = { HASHES_REVMAP = dict([(y, x) for x, y in six.iteritems(HASHES)]) +def __clean_tmp(tmp): + ''' + Remove temporary files + ''' + try: + salt.utils.rm_rf(tmp) + except Exception: + pass + + def guess_archive_type(name): ''' Guess an archive type (tar, zip, or rar) by its file extension @@ -116,7 +126,15 @@ def copyfile(source, dest, backup_mode='', cachedir=''): fstat = os.stat(dest) except OSError: pass - shutil.move(tgt, dest) + + # The move could fail if the dest has xattr protections, so delete the + # temp file in this case + try: + shutil.move(tgt, dest) + except Exception: + __clean_tmp(tgt) + raise + if fstat is not None: os.chown(dest, fstat.st_uid, fstat.st_gid) os.chmod(dest, fstat.st_mode) @@ -134,10 +152,7 @@ def copyfile(source, dest, backup_mode='', cachedir=''): subprocess.call(cmd, stdout=dev_null, stderr=dev_null) if os.path.isfile(tgt): # The temp file failed to move - try: - os.remove(tgt) - except Exception: - pass + __clean_tmp(tgt) def rename(src, dst): diff --git a/tests/integration/modules/test_file.py b/tests/integration/modules/test_file.py index b4ff4f31d8..acba2e7f50 100644 --- a/tests/integration/modules/test_file.py +++ b/tests/integration/modules/test_file.py @@ -3,12 +3,23 @@ # Import python libs from __future__ import absolute_import import getpass -import grp -import pwd import os import shutil import sys +# Posix only +try: + import grp + import pwd +except ImportError: + pass + +# Windows only +try: + import win32file +except ImportError: + pass + # Import Salt Testing libs from tests.support.case import ModuleCase from tests.support.unit import skipIf @@ -18,6 +29,16 @@ from tests.support.paths import FILES, TMP import salt.utils +def symlink(source, link_name): + ''' + Handle symlinks on Windows with Python < 3.2 + ''' + if salt.utils.is_windows(): + win32file.CreateSymbolicLink(link_name, source) + else: + os.symlink(source, link_name) + + class FileModuleTest(ModuleCase): ''' Validate the file module @@ -25,27 +46,27 @@ class FileModuleTest(ModuleCase): def setUp(self): self.myfile = os.path.join(TMP, 'myfile') with salt.utils.fopen(self.myfile, 'w+') as fp: - fp.write('Hello\n') + fp.write('Hello' + os.linesep) self.mydir = os.path.join(TMP, 'mydir/isawesome') if not os.path.isdir(self.mydir): # left behind... Don't fail because of this! os.makedirs(self.mydir) self.mysymlink = os.path.join(TMP, 'mysymlink') - if os.path.islink(self.mysymlink): + if os.path.islink(self.mysymlink) or os.path.isfile(self.mysymlink): os.remove(self.mysymlink) - os.symlink(self.myfile, self.mysymlink) + symlink(self.myfile, self.mysymlink) self.mybadsymlink = os.path.join(TMP, 'mybadsymlink') - if os.path.islink(self.mybadsymlink): + if os.path.islink(self.mybadsymlink) or os.path.isfile(self.mybadsymlink): os.remove(self.mybadsymlink) - os.symlink('/nonexistentpath', self.mybadsymlink) + symlink('/nonexistentpath', self.mybadsymlink) super(FileModuleTest, self).setUp() def tearDown(self): if os.path.isfile(self.myfile): os.remove(self.myfile) - if os.path.islink(self.mysymlink): + if os.path.islink(self.mysymlink) or os.path.isfile(self.mysymlink): os.remove(self.mysymlink) - if os.path.islink(self.mybadsymlink): + if os.path.islink(self.mybadsymlink) or os.path.isfile(self.mybadsymlink): os.remove(self.mybadsymlink) shutil.rmtree(self.mydir, ignore_errors=True) super(FileModuleTest, self).tearDown() @@ -173,3 +194,20 @@ class FileModuleTest(ModuleCase): ret = self.run_function('file.source_list', ['file://' + self.myfile, 'filehash', 'base']) self.assertEqual(list(ret), ['file://' + self.myfile, 'filehash']) + + def test_file_line_changes_format(self): + ''' + Test file.line changes output formatting. + + Issue #41474 + ''' + ret = self.minion_run('file.line', self.myfile, 'Goodbye', + mode='insert', after='Hello') + self.assertIn('Hello' + os.linesep + '+Goodbye', ret) + + def test_file_line_content(self): + self.minion_run('file.line', self.myfile, 'Goodbye', + mode='insert', after='Hello') + with salt.utils.fopen(self.myfile, 'r') as fp: + content = fp.read() + self.assertEqual(content, 'Hello' + os.linesep + 'Goodbye' + os.linesep) diff --git a/tests/integration/output/test_output.py b/tests/integration/output/test_output.py index 1073e99c0d..c741fb78d2 100644 --- a/tests/integration/output/test_output.py +++ b/tests/integration/output/test_output.py @@ -109,3 +109,59 @@ class OutputReturnTest(ShellCase): delattr(self, 'maxDiff') else: self.maxDiff = old_max_diff + + def test_output_highstate(self): + ''' + Regression tests for the highstate outputter. Calls a basic state with various + flags. Each comparison should be identical when successful. + ''' + # Test basic highstate output. No frills. + expected = ['minion:', ' ID: simple-ping', ' Function: module.run', + ' Name: test.ping', ' Result: True', + ' Comment: Module function test.ping executed', + ' Changes: ', ' ret:', ' True', + 'Summary for minion', 'Succeeded: 1 (changed=1)', 'Failed: 0', + 'Total states run: 1'] + state_run = self.run_salt('"minion" state.sls simple-ping') + + for expected_item in expected: + self.assertIn(expected_item, state_run) + + # Test highstate output while also passing --out=highstate. + # This is a regression test for Issue #29796 + state_run = self.run_salt('"minion" state.sls simple-ping --out=highstate') + + for expected_item in expected: + self.assertIn(expected_item, state_run) + + # Test highstate output when passing --static and running a state function. + # See Issue #44556. + state_run = self.run_salt('"minion" state.sls simple-ping --static') + + for expected_item in expected: + self.assertIn(expected_item, state_run) + + # Test highstate output when passing --static and --out=highstate. + # See Issue #44556. + state_run = self.run_salt('"minion" state.sls simple-ping --static --out=highstate') + + for expected_item in expected: + self.assertIn(expected_item, state_run) + + def test_output_highstate_falls_back_nested(self): + ''' + Tests outputter when passing --out=highstate with a non-state call. This should + fall back to "nested" output. + ''' + expected = ['minion:', ' True'] + ret = self.run_salt('"minion" test.ping --out=highstate') + self.assertEqual(ret, expected) + + def test_static_simple(self): + ''' + Tests passing the --static option with a basic test.ping command. This + should be the "nested" output. + ''' + expected = ['minion:', ' True'] + ret = self.run_salt('"minion" test.ping --static') + self.assertEqual(ret, expected) diff --git a/tests/unit/grains/test_core.py b/tests/unit/grains/test_core.py index 6ee4257863..33bcfedfd5 100644 --- a/tests/unit/grains/test_core.py +++ b/tests/unit/grains/test_core.py @@ -25,6 +25,16 @@ import salt.grains.core as core # Import 3rd-party libs import salt.ext.six as six +# Globals +IPv4Address = salt.ext.ipaddress.IPv4Address +IPv6Address = salt.ext.ipaddress.IPv6Address +IP4_LOCAL = '127.0.0.1' +IP4_ADD1 = '10.0.0.1' +IP4_ADD2 = '10.0.0.2' +IP6_LOCAL = '::1' +IP6_ADD1 = '2001:4860:4860::8844' +IP6_ADD2 = '2001:4860:4860::8888' + @skipIf(NO_MOCK, NO_MOCK_REASON) class CoreGrainsTestCase(TestCase, LoaderModuleMockMixin): @@ -462,3 +472,127 @@ PATCHLEVEL = 3 self.assertEqual(os_grains.get('osrelease'), os_release_map['osrelease']) self.assertListEqual(list(os_grains.get('osrelease_info')), os_release_map['osrelease_info']) self.assertEqual(os_grains.get('osmajorrelease'), os_release_map['osmajorrelease']) + + def _check_ipaddress(self, value, ip_v): + ''' + check if ip address in a list is valid + ''' + for val in value: + assert isinstance(val, six.string_types) + ip_method = 'is_ipv{0}'.format(ip_v) + self.assertTrue(getattr(salt.utils.network, ip_method)(val)) + + def _check_empty(self, key, value, empty): + ''' + if empty is False and value does not exist assert error + if empty is True and value exists assert error + ''' + if not empty and not value: + raise Exception("{0} is empty, expecting a value".format(key)) + elif empty and value: + raise Exception("{0} is suppose to be empty. value: {1} \ + exists".format(key, value)) + + @skipIf(not salt.utils.is_linux(), 'System is not Linux') + def test_fqdn_return(self): + ''' + test ip4 and ip6 return values + ''' + net_ip4_mock = [IP4_LOCAL, IP4_ADD1, IP4_ADD2] + net_ip6_mock = [IP6_LOCAL, IP6_ADD1, IP6_ADD2] + + self._run_fqdn_tests(net_ip4_mock, net_ip6_mock, + ip4_empty=False, ip6_empty=False) + + @skipIf(not salt.utils.is_linux(), 'System is not Linux') + def test_fqdn6_empty(self): + ''' + test when ip6 is empty + ''' + net_ip4_mock = [IP4_LOCAL, IP4_ADD1, IP4_ADD2] + net_ip6_mock = [] + + self._run_fqdn_tests(net_ip4_mock, net_ip6_mock, + ip4_empty=False) + + @skipIf(not salt.utils.is_linux(), 'System is not Linux') + def test_fqdn4_empty(self): + ''' + test when ip4 is empty + ''' + net_ip4_mock = [] + net_ip6_mock = [IP6_LOCAL, IP6_ADD1, IP6_ADD2] + + self._run_fqdn_tests(net_ip4_mock, net_ip6_mock, + ip6_empty=False) + + @skipIf(not salt.utils.is_linux(), 'System is not Linux') + def test_fqdn_all_empty(self): + ''' + test when both ip4 and ip6 are empty + ''' + net_ip4_mock = [] + net_ip6_mock = [] + + self._run_fqdn_tests(net_ip4_mock, net_ip6_mock) + + def _run_fqdn_tests(self, net_ip4_mock, net_ip6_mock, + ip6_empty=True, ip4_empty=True): + + def _check_type(key, value, ip4_empty, ip6_empty): + ''' + check type and other checks + ''' + assert isinstance(value, list) + + if '4' in key: + self._check_empty(key, value, ip4_empty) + self._check_ipaddress(value, ip_v='4') + elif '6' in key: + self._check_empty(key, value, ip6_empty) + self._check_ipaddress(value, ip_v='6') + + ip4_mock = [(2, 1, 6, '', (IP4_ADD1, 0)), + (2, 3, 0, '', (IP4_ADD2, 0))] + ip6_mock = [(10, 1, 6, '', (IP6_ADD1, 0, 0, 0)), + (10, 3, 0, '', (IP6_ADD2, 0, 0, 0))] + + with patch.dict(core.__opts__, {'ipv6': False}): + with patch.object(salt.utils.network, 'ip_addrs', + MagicMock(return_value=net_ip4_mock)): + with patch.object(salt.utils.network, 'ip_addrs6', + MagicMock(return_value=net_ip6_mock)): + with patch.object(core.socket, 'getaddrinfo', side_effect=[ip4_mock, ip6_mock]): + get_fqdn = core.ip_fqdn() + ret_keys = ['fqdn_ip4', 'fqdn_ip6', 'ipv4', 'ipv6'] + for key in ret_keys: + value = get_fqdn[key] + _check_type(key, value, ip4_empty, ip6_empty) + + @skipIf(not salt.utils.is_linux(), 'System is not Linux') + def test_dns_return(self): + ''' + test the return for a dns grain. test for issue: + https://github.com/saltstack/salt/issues/41230 + ''' + resolv_mock = {'domain': '', 'sortlist': [], 'nameservers': + [IPv4Address(IP4_ADD1), + IPv6Address(IP6_ADD1)], 'ip4_nameservers': + [IPv4Address(IP4_ADD1)], + 'search': ['test.saltstack.com'], 'ip6_nameservers': + [IPv6Address(IP6_ADD1)], 'options': []} + ret = {'dns': {'domain': '', 'sortlist': [], 'nameservers': + [IP4_ADD1, IP6_ADD1], 'ip4_nameservers': + [IP4_ADD1], 'search': ['test.saltstack.com'], + 'ip6_nameservers': [IP6_ADD1], 'options': + []}} + self._run_dns_test(resolv_mock, ret) + + def _run_dns_test(self, resolv_mock, ret): + with patch.object(salt.utils, 'is_windows', + MagicMock(return_value=False)): + with patch.dict(core.__opts__, {'ipv6': False}): + with patch.object(salt.utils.dns, 'parse_resolv', + MagicMock(return_value=resolv_mock)): + get_dns = core.dns() + self.assertEqual(get_dns, ret)