Merge pull request #45446 from rallytime/bp-45390

Back-port #45390 to 2016.11.9
This commit is contained in:
Nicole Thomas 2018-01-16 15:08:38 -05:00 committed by GitHub
commit 7322efba92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 176 additions and 113 deletions

View File

@ -9,10 +9,14 @@ Windows
=======
Execution module pkg
--------------------
Significate changes (PR #43708, damon-atkins) have been made to the pkg execution module. Users should test this release against their existing package sls definition files.
Significate changes (PR #43708 & #45390, damon-atkins) have been made to the pkg execution module. Users should test this release against their existing package sls definition files.
- ``pkg.list_available`` no longer defaults to refreshing the winrepo meta database.
- ``pkg.install`` without a ``version`` parameter no longer upgrades software if the software is already installed. Use ``pkg.install version=latest`` or in a state use ``pkg.latest`` to get the old behavior.
- ``pkg.list_pkgs`` now returns multiple versions if software installed more than once.
- ``pkg.list_pkgs`` now returns 'Not Found' when the version is not found instead of '(value not set)' which matches the contents of the sls definitions.
- ``pkg.remove()`` will wait upto 3 seconds (normally about a second) to detect changes in the registry after removing software, improving reporting of version changes.
- ``pkg.remove()`` can remove ``latest`` software, if ``latest`` is defined in sls definition.
- Documentation was update for the execution module to match the style in new versions, some corrections as well.
- All install/remove commands are prefix with cmd.exe shell and cmdmod is called with a command line string instead of a list. Some sls files in saltstack/salt-winrepo-ng expected the commands to be prefixed with cmd.exe (i.e. the use of ``&``).
- Some execution module functions results, now behavour more like their Unix/Linux versions.

View File

@ -27,6 +27,14 @@ As the creation of this metadata can take some time, the
:conf_minion:`winrepo_cache_expire_min` minion config option can be used to
suppress refreshes when the metadata is less than a given number of seconds
old.
.. note::
Version numbers can be `version number string`, `latest` and `Not Found`.
Where `Not Found` means this module was not able to determine the version of
the software installed, it can also be used as the version number in sls
definitions file in these cases. Versions numbers are sorted in order of
0,`Not Found`,`order version numbers`,...,`latest`.
'''
# Import python future libs
@ -42,24 +50,28 @@ import time
import sys
from functools import cmp_to_key
# Import third party libs
from salt.ext import six
# pylint: disable=import-error,no-name-in-module
from salt.ext.six.moves.urllib.parse import urlparse as _urlparse
# Import salt libs
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.args
import salt.utils.files
import salt.utils.pkg
import salt.utils.versions
import salt.syspaths
import salt.payload
from salt.exceptions import MinionError
from salt.utils.versions import LooseVersion
try:
# Import third party libs
from salt.ext import six
# pylint: disable=import-error,no-name-in-module
from salt.ext.six.moves.urllib.parse import urlparse as _urlparse
# Import salt libs
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.args
import salt.utils.files
import salt.utils.pkg
import salt.utils.versions
import salt.syspaths
import salt.payload
from salt.exceptions import MinionError
from salt.utils.versions import LooseVersion
except ImportError:
raise ImportError('Salt not installed correctly, salt python modules missing')
log = logging.getLogger(__name__)
@ -385,20 +397,23 @@ def list_pkgs(versions_as_list=False, **kwargs):
ret = {}
name_map = _get_name_map(saltenv)
for pkg_name, val in six.iteritems(_get_reg_software()):
for pkg_name, val_list in six.iteritems(_get_reg_software()):
if pkg_name in name_map:
key = name_map[pkg_name]
if val in ['(value not set)', 'Not Found', None, False]:
# Look up version from winrepo
pkg_info = _get_package_info(key, saltenv=saltenv)
if not pkg_info:
continue
for pkg_ver in pkg_info:
if pkg_info[pkg_ver]['full_name'] == pkg_name:
val = pkg_ver
for val in val_list:
if val == 'Not Found':
# Look up version from winrepo
pkg_info = _get_package_info(key, saltenv=saltenv)
if not pkg_info:
continue
for pkg_ver in pkg_info.keys():
if pkg_info[pkg_ver]['full_name'] == pkg_name:
val = pkg_ver
__salt__['pkg_resource.add_pkg'](ret, key, val)
else:
key = pkg_name
__salt__['pkg_resource.add_pkg'](ret, key, val)
for val in val_list:
__salt__['pkg_resource.add_pkg'](ret, key, val)
__salt__['pkg_resource.sort_pkglist'](ret)
if not versions_as_list:
@ -406,21 +421,6 @@ def list_pkgs(versions_as_list=False, **kwargs):
return ret
def _search_software(target):
'''
This searches the msi product databases for name matches of the list of
target products, it will return a dict with values added to the list passed
in
'''
search_results = {}
software = dict(_get_reg_software().items())
for key, value in six.iteritems(software):
if key is not None:
if target.lower() in key.lower():
search_results[key] = value
return search_results
def _get_reg_software():
'''
This searches the uninstall keys in the registry to find a match in the sub
@ -444,29 +444,62 @@ def _get_reg_software():
None]
reg_software = {}
hive = 'HKLM'
key = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
def update(hive, key, reg_key, use_32bit):
# 2018 this code has been update to reflect some of utils/pkg/win.py logic
# i.e. check_reg, and checking of DisplayName and DisplayVersion.
d_name = ''
d_vers = ''
d_name = __salt__['reg.read_value'](hive,
'{0}\\{1}'.format(key, reg_key),
'DisplayName',
use_32bit)['vdata']
d_name_regdata = __salt__['reg.read_value'](hive,
'{0}\\{1}'.format(key, reg_key),
'DisplayName',
use_32bit)
if (not d_name_regdata['success'] or
d_name_regdata['vtype'] not in ['REG_SZ', 'REG_EXPAND_SZ'] or
d_name_regdata['vdata'] in ['(value not set)', None, False]):
return
d_name = d_name_regdata['vdata']
d_vers = __salt__['reg.read_value'](hive,
'{0}\\{1}'.format(key, reg_key),
'DisplayVersion',
use_32bit)['vdata']
d_vers_regdata = __salt__['reg.read_value'](hive,
'{0}\\{1}'.format(key, reg_key),
'DisplayVersion',
use_32bit)
if (not d_vers_regdata['success'] or
d_vers_regdata['vtype'] not in ['REG_SZ', 'REG_EXPAND_SZ', 'REG_DWORD'] or
d_vers_regdata['vdata'] in [None, False]):
return
if isinstance(d_vers_regdata['vdata'], int):
d_vers = six.text_type(d_vers_regdata['vdata'])
else:
d_vers = d_vers_regdata['vdata']
if not d_vers or d_vers == '(value not set)':
d_vers = 'Not Found'
check_ok = False
for check_reg in ['UninstallString', 'QuietUninstallString', 'ModifyPath']:
check_regdata = __salt__['reg.read_value'](hive,
'{0}\\{1}'.format(key, reg_key),
check_reg,
use_32bit)
if (not check_regdata['success'] or
check_regdata['vtype'] not in ['REG_SZ', 'REG_EXPAND_SZ'] or
check_regdata['vdata'] in ['(value not set)', None, False]):
continue
else:
check_ok = True
if not check_ok:
return
if d_name not in ignore_list:
# some MS Office updates don't register a product name which means
# their information is useless
reg_software.update({d_name: six.text_type(d_vers)})
reg_software.setdefault(d_name, []).append(d_vers)
for reg_key in __salt__['reg.list_keys'](hive, key):
update(hive, key, reg_key, False)
@ -856,7 +889,7 @@ def _repo_process_pkg_sls(filename, short_path_name, ret, successful_verbose):
if config:
revmap = {}
errors = []
for pkgname, versions in six.iteritems(config):
for pkgname, version_list in six.iteritems(config):
if pkgname in ret['repo']:
log.error(
'package \'%s\' within \'%s\' already defined, skipping',
@ -864,7 +897,7 @@ def _repo_process_pkg_sls(filename, short_path_name, ret, successful_verbose):
)
errors.append('package \'{0}\' already defined'.format(pkgname))
break
for version_str, repodata in six.iteritems(versions):
for version_str, repodata in six.iteritems(version_list):
# Ensure version is a string/unicode
if not isinstance(version_str, six.string_types):
msg = (
@ -1137,7 +1170,7 @@ def install(name=None, refresh=False, pkgs=None, **kwargs):
}
# Get a list of currently installed software for comparison at the end
old = list_pkgs(saltenv=saltenv, refresh=refresh)
old = list_pkgs(saltenv=saltenv, refresh=refresh, versions_as_list=True)
# Loop through each package
changed = []
@ -1154,16 +1187,20 @@ def install(name=None, refresh=False, pkgs=None, **kwargs):
continue
# Get the version number passed or the latest available (must be a string)
version_num = ''
version_num = None
if options:
version_num = options.get('version', '')
version_num = options.get('version', None)
# Using the salt cmdline with version=5.3 might be interpreted
# as a float it must be converted to a string in order for
# string matching to work.
if not isinstance(version_num, six.string_types) and version_num is not None:
if not isinstance(version_num, six.text_type) and version_num is not None:
version_num = six.text_type(version_num)
if not version_num:
if pkg_name in old:
log.debug('A version ({0}) already installed for package '
'{1}'.format(version_num, pkg_name))
continue
# following can be version number or latest or Not Found
version_num = _get_latest_pkg_version(pkginfo)
@ -1171,8 +1208,7 @@ def install(name=None, refresh=False, pkgs=None, **kwargs):
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'):
if version_num in old.get(pkg_name, []):
# Desired version number already installed
ret[pkg_name] = {'current': version_num}
continue
@ -1344,9 +1380,9 @@ def install(name=None, refresh=False, pkgs=None, **kwargs):
else:
# Make sure the task is running, try for 5 secs
from time import time
t_end = time() + 5
while time() < t_end:
t_end = time.time() + 5
while time.time() < t_end:
time.sleep(0.25)
task_running = __salt__['task.status'](
'update-salt-software') == 'Running'
if task_running:
@ -1393,7 +1429,11 @@ def install(name=None, refresh=False, pkgs=None, **kwargs):
ret[pkg_name] = {'install status': 'failed'}
# Get a new list of installed software
new = list_pkgs(saltenv=saltenv)
new = list_pkgs(saltenv=saltenv, refresh=False)
# Take the "old" package list and convert the values to strings in
# preparation for the comparison below.
__salt__['pkg_resource.stringify'](old)
# For installers that have no specific version (ie: chrome)
# The software definition file will have a version of 'latest'
@ -1434,9 +1474,9 @@ def upgrade(**kwargs):
salt '*' pkg.upgrade
'''
log.warning('pkg.upgrade not implemented on Windows yet')
refresh = salt.utils.is_true(kwargs.get('refresh', True))
saltenv = kwargs.get('saltenv', 'base')
log.warning('pkg.upgrade not implemented on Windows yet refresh:%s saltenv:%s', refresh, saltenv)
# Uncomment the below once pkg.upgrade has been implemented
# if salt.utils.is_true(refresh):
@ -1444,7 +1484,7 @@ def upgrade(**kwargs):
return {}
def remove(name=None, pkgs=None, version=None, **kwargs):
def remove(name=None, pkgs=None, **kwargs):
'''
Remove the passed package(s) from the system using winrepo
@ -1455,6 +1495,12 @@ def remove(name=None, pkgs=None, version=None, **kwargs):
The name(s) of the package(s) to be uninstalled. Can be a
single package or a comma delimited list of packages, no spaces.
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.
Kwargs:
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
@ -1462,11 +1508,6 @@ def remove(name=None, pkgs=None, version=None, **kwargs):
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.
Kwargs:
saltenv (str): Salt environment. Default ``base``
refresh (bool): Refresh package metadata. Default ``False``
@ -1506,7 +1547,7 @@ def remove(name=None, pkgs=None, version=None, **kwargs):
old = list_pkgs(saltenv=saltenv, refresh=refresh, versions_as_list=True)
# Loop through each package
changed = []
changed = [] # list of changed package names
for pkgname, version_num in six.iteritems(pkg_params):
# Load package information for the package
@ -1519,44 +1560,57 @@ def remove(name=None, pkgs=None, version=None, **kwargs):
ret[pkgname] = msg
continue
# Check to see if package is installed on the system
if pkgname not in old:
log.debug('%s %s not installed', pkgname, version_num if version_num else '')
ret[pkgname] = {'current': 'not installed'}
continue
removal_targets = []
# Only support a single version number
if version_num is not None:
# Using the salt cmdline with version=5.3 might be interpreted
# as a float it must be converted to a string in order for
# string matching to work.
if not isinstance(version_num, six.string_types) and version_num is not None:
version_num = six.text_type(version_num)
if version_num not in pkginfo and 'latest' in pkginfo:
version_num = 'latest'
elif 'latest' in pkginfo:
version_num = 'latest'
version_num = six.text_type(version_num)
# Check to see if package is installed on the system
removal_targets = []
if pkgname not in old:
log.error('%s %s not installed', pkgname, version)
ret[pkgname] = {'current': 'not installed'}
continue
# At least one version of the software is installed.
if version_num is None:
for ver_install in old[pkgname]:
if ver_install not in pkginfo and 'latest' in pkginfo:
log.debug('%s %s using package latest entry to to remove', pkgname, version_num)
removal_targets.append('latest')
else:
removal_targets.append(ver_install)
else:
if version_num is None:
removal_targets.extend(old[pkgname])
elif version_num not in old[pkgname] \
and 'Not Found' not in old[pkgname] \
and version_num != 'latest':
log.error('%s %s not installed', pkgname, version)
ret[pkgname] = {
'current': '{0} not installed'.format(version_num)
if version_num in pkginfo:
# we known how to remove this version
if version_num in old[pkgname]:
removal_targets.append(version_num)
else:
log.debug('%s %s not installed', pkgname, version_num)
ret[pkgname] = {'current': '{0} not installed'.format(version_num)}
continue
elif 'latest' in pkginfo:
# we do not have version entry, assume software can self upgrade and use latest
log.debug('%s %s using package latest entry to to remove', pkgname, version_num)
removal_targets.append('latest')
if not removal_targets:
log.error('%s %s no definition to remove this version', pkgname, version_num)
ret[pkgname] = {
'current': '{0} no definition, cannot removed'.format(version_num)
}
continue
else:
removal_targets.append(version_num)
continue
for target in removal_targets:
# Get the uninstaller
uninstaller = pkginfo[target].get('uninstaller', '')
cache_dir = pkginfo[target].get('cache_dir', False)
uninstall_flags = pkginfo[target].get('uninstall_flags', '')
# If no uninstaller found, use the installer
if not uninstaller:
# If no uninstaller found, use the installer with uninstall flags
if not uninstaller and uninstall_flags:
uninstaller = pkginfo[target].get('installer', '')
# If still no uninstaller found, fail
@ -1565,7 +1619,7 @@ def remove(name=None, pkgs=None, version=None, **kwargs):
'No installer or uninstaller configured for package %s',
pkgname,
)
ret[pkgname] = {'no uninstaller': target}
ret[pkgname] = {'no uninstaller defined': target}
continue
# Where is the uninstaller
@ -1624,9 +1678,6 @@ def remove(name=None, pkgs=None, version=None, **kwargs):
# os.path.expandvars is not required as we run everything through cmd.exe /s /c
# Get uninstall flags
uninstall_flags = pkginfo[target].get('uninstall_flags', '')
if kwargs.get('extra_uninstall_flags'):
uninstall_flags = '{0} {1}'.format(
uninstall_flags, kwargs.get('extra_uninstall_flags', ''))
@ -1648,6 +1699,7 @@ def remove(name=None, pkgs=None, version=None, **kwargs):
arguments = '{0} {1}'.format(arguments, uninstall_flags)
# Uninstall the software
changed.append(pkgname)
# Check Use Scheduler Option
if pkginfo[target].get('use_scheduler', False):
# Create Scheduled Task
@ -1697,29 +1749,32 @@ def remove(name=None, pkgs=None, version=None, **kwargs):
ret[pkgname] = {'uninstall status': 'failed'}
# Get a new list of installed software
new = list_pkgs(saltenv=saltenv)
new = list_pkgs(saltenv=saltenv, refresh=False)
# Take the "old" package list and convert the values to strings in
# preparation for the comparison below.
__salt__['pkg_resource.stringify'](old)
# Check for changes in the registry
difference = salt.utils.compare_dicts(old, new)
tries = 0
while not all(name in difference for name in changed) and tries <= 1000:
new = list_pkgs(saltenv=saltenv)
found_chgs = all(name in difference for name in changed)
end_t = time.time() + 3 # give it 3 seconds to catch up.
while not found_chgs and time.time() < end_t:
time.sleep(0.5)
new = list_pkgs(saltenv=saltenv, refresh=False)
difference = salt.utils.compare_dicts(old, new)
tries += 1
if tries == 1000:
ret['_comment'] = 'Registry not updated.'
found_chgs = all(name in difference for name in changed)
if not found_chgs:
log.warning('Expected changes for package removal may not have occured')
# Compare the software list before and after
# Add the difference to ret
ret.update(difference)
return ret
def purge(name=None, pkgs=None, version=None, **kwargs):
def purge(name=None, pkgs=None, **kwargs):
'''
Package purges are not supported, this function is identical to
``remove()``.
@ -1757,7 +1812,6 @@ def purge(name=None, pkgs=None, version=None, **kwargs):
'''
return remove(name=name,
pkgs=pkgs,
version=version,
**kwargs)
@ -1841,6 +1895,11 @@ def _reverse_cmp_pkg_versions(pkg1, pkg2):
def _get_latest_pkg_version(pkginfo):
'''
Returns the latest version of the package.
Will return 'latest' or version number string, and
'Not Found' if 'Not Found' is the only entry.
'''
if len(pkginfo) == 1:
return next(six.iterkeys(pkginfo))
try: