pkg.installed: Check metadata for "sources"

This uses the newly-added lowpkg.bin_pkg_info to get the binary
package's metadata and determine if it matches what is already
installed. If not, then it will be added to the list of install targets.
This commit is contained in:
Erik Johnson 2015-05-19 09:00:41 -05:00
parent be1193042a
commit 359ef9eaba

View File

@ -115,6 +115,27 @@ def __gen_rtag():
return os.path.join(__opts__['cachedir'], 'pkg_refresh')
def _get_comparison_spec(pkgver):
'''
Return a tuple containing the comparison operator and the version. If no
comparison operator was passed, the comparison is assumed to be an "equals"
comparison, and "==" will be the operator returned.
'''
match = re.match('^([<>])?(=)?([^<>=]+)$', pkgver)
if not match:
raise CommandExecutionError(
'Invalid version specification \'{0}\'.'.format(pkgver)
)
gt_lt, eq, verstr = match.groups()
oper = gt_lt or ''
oper += eq or ''
# A comparison operator of "=" is redundant, but possible.
# Change it to "==" so that the version comparison works
if oper in ('=', ''):
oper = '=='
return oper, verstr
def _fulfills_version_spec(versions, oper, desired_version):
'''
Returns True if any of the installed versions match the specified version,
@ -145,6 +166,7 @@ def _find_unpurge_targets(desired):
def _find_remove_targets(name=None,
version=None,
pkgs=None,
normalize=True,
**kwargs):
'''
Inspect the arguments to pkg.removed and discover what packages need to
@ -152,7 +174,7 @@ def _find_remove_targets(name=None,
'''
cur_pkgs = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs)
if pkgs:
to_remove = _repack_pkgs(pkgs)
to_remove = _repack_pkgs(pkgs, normalize=normalize)
if not to_remove:
# Badly-formatted SLS
@ -183,27 +205,19 @@ def _find_remove_targets(name=None,
targets.append(pkgname)
continue
version_spec = True
match = re.match('^([<>])?(=)?([^<>=]+)$', pkgver)
if not match:
msg = 'Invalid version specification {0!r} for package ' \
'{1!r}.'.format(pkgver, pkgname)
problems.append(msg)
try:
oper, verstr = _get_comparison_spec(pkgver)
except CommandExecutionError as exc:
problems.append(exc.strerror)
continue
if not _fulfills_version_spec(cver, oper, verstr):
log.debug(
'Current version ({0}) did not match desired version '
'specification ({1}), will not remove'
.format(cver, verstr)
)
else:
gt_lt, eq, verstr = match.groups()
comparison = gt_lt or ''
comparison += eq or ''
# A comparison operator of "=" is redundant, but possible.
# Change it to "==" so that the version comparison works
if comparison in ['=', '']:
comparison = '=='
if not _fulfills_version_spec(cver, comparison, verstr):
log.debug(
'Current version ({0}) did not match ({1}) specified '
'({2}), skipping remove {3}'
.format(cver, comparison, verstr, pkgname)
)
else:
targets.append(pkgname)
targets.append(pkgname)
if problems:
return {'name': name,
@ -214,7 +228,7 @@ def _find_remove_targets(name=None,
if not targets:
# All specified packages are already absent
msg = (
'All specified packages{0} are already absent.'
'All specified packages{0} are already absent'
.format(' (matching specified versions)' if version_spec else '')
)
return {'name': name,
@ -262,14 +276,17 @@ def _find_install_targets(name=None,
if pkgs:
desired = _repack_pkgs(pkgs)
elif sources:
desired = __salt__['pkg_resource.pack_sources'](sources)
desired = __salt__['pkg_resource.pack_sources'](
sources,
normalize=normalize,
)
if not desired:
# Badly-formatted SLS
return {'name': name,
'changes': {},
'result': False,
'comment': 'Invalidly formatted {0!r} parameter. See '
'comment': 'Invalidly formatted \'{0}\' parameter. See '
'minion log.'.format('pkgs' if pkgs
else 'sources')}
to_unpurge = _find_unpurge_targets(desired)
@ -301,8 +318,8 @@ def _find_install_targets(name=None,
return {'name': name,
'changes': {},
'result': True,
'comment': 'Version {0} of package {1!r} is already '
'installed.'.format(version, name)}
'comment': 'Version {0} of package \'{1}\' is already '
'installed'.format(version, name)}
# if cver is not an empty string, the package is already installed
elif cver and version is None and not pkg_verify:
@ -311,25 +328,12 @@ def _find_install_targets(name=None,
'changes': {},
'result': True,
'comment': 'Package {0} is already '
'installed.'.format(name)}
'installed'.format(name)}
version_spec = False
# Find out which packages will be targeted in the call to pkg.install
if sources:
targets = []
to_reinstall = []
for x in desired:
if x not in cur_pkgs:
targets.append(x)
elif pkg_verify:
retval = __salt__['pkg.verify'](x, ignore_types=ignore_types)
if retval:
to_reinstall.append(x)
altered_files[x] = retval
else:
if not sources:
# Check for alternate package names if strict processing is not
# enforced.
# Takes extra time. Disable for improved performance
# enforced. Takes extra time. Disable for improved performance
if not skip_suggestions:
# Perform platform-specific pre-flight checks
problems = _preflight_check(desired, **kwargs)
@ -343,7 +347,7 @@ def _find_install_targets(name=None,
if problems.get('suggest'):
for pkgname, suggestions in six.iteritems(problems['suggest']):
comments.append(
'Package {0!r} not found (possible matches: {1})'
'Package \'{0}\' not found (possible matches: {1})'
.format(pkgname, ', '.join(suggestions))
)
if comments:
@ -354,78 +358,103 @@ def _find_install_targets(name=None,
'result': False,
'comment': '. '.join(comments).rstrip()}
# Check current versions against desired versions
targets = {}
to_reinstall = {}
problems = []
for pkgname, pkgver in six.iteritems(desired):
cver = cur_pkgs.get(pkgname, [])
# Package not yet installed, so add to targets
if not cver:
targets[pkgname] = pkgver
# Find out which packages will be targeted in the call to pkg.install
targets = {}
to_reinstall = {}
problems = []
warnings = []
for key, val in six.iteritems(desired):
cver = cur_pkgs.get(key, [])
# Package not yet installed, so add to targets
if not cver:
targets[key] = val
continue
if sources:
if to_reinstall:
to_reinstall[key] = val
continue
elif not __salt__['pkg_resource.check_extra_requirements'](pkgname,
pkgver):
targets[pkgname] = pkgver
elif 'lowpkg.bin_pkg_info' not in __salt__:
continue
# Metadata parser is available, cache the file and derive the
# package's name and version
err = 'Unable to cache {0}: {1}'
try:
cached_path = __salt__['cp.cache_file'](val)
except CommandExecutionError as exc:
problems.append(err.format(val, exc))
continue
if not cached_path:
problems.append(err.format(val, 'file not found'))
continue
elif not os.path.exists(cached_path):
problems.append('{0} does not exist on minion'.format(val))
continue
source_info = __salt__['lowpkg.bin_pkg_info'](cached_path)
if source_info is None:
warnings.append('Failed to parse metadata for {0}'.format(val))
continue
else:
oper = '=='
verstr = source_info['version']
else:
if not __salt__['pkg_resource.check_extra_requirements'](key, val):
targets[key] = val
continue
# No version specified and pkg is installed
elif __salt__['pkg_resource.version_clean'](pkgver) is None:
elif __salt__['pkg_resource.version_clean'](val) is None:
if pkg_verify:
retval = __salt__['pkg.verify'](pkgname,
ignore_types=ignore_types)
if retval:
to_reinstall[pkgname] = pkgver
altered_files[pkgname] = retval
continue
version_spec = True
match = re.match('^([<>])?(=)?([^<>=]+)$', pkgver)
if not match:
msg = 'Invalid version specification {0!r} for package ' \
'{1!r}.'.format(pkgver, pkgname)
problems.append(msg)
else:
gt_lt, eq, verstr = match.groups()
comparison = gt_lt or ''
comparison += eq or ''
# A comparison operator of "=" is redundant, but possible.
# Change it to "==" so that the version comparison works
if comparison in ['=', '']:
comparison = '=='
if 'allow_updates' in kwargs:
if kwargs['allow_updates']:
comparison = '>='
if not _fulfills_version_spec(cver, comparison, verstr):
log.debug(
'Current version ({0}) did not match ({1}) desired '
'({2}), add to targets'
.format(cver, comparison, verstr)
verify_result = __salt__['pkg.verify'](
key,
ignore_types=ignore_types,
)
targets[pkgname] = pkgver
elif pkg_verify and comparison == '==':
retval = __salt__['pkg.verify'](pkgname,
ignore_types=ignore_types)
if retval:
to_reinstall[pkgname] = pkgver
altered_files[pkgname] = retval
if verify_result:
to_reinstall[key] = val
altered_files[key] = verify_result
continue
try:
oper, verstr = _get_comparison_spec(val)
except CommandExecutionError as exc:
problems.append(exc.strerror)
continue
if problems:
return {'name': name,
'changes': {},
'result': False,
'comment': ' '.join(problems)}
# Compare desired version against installed version.
version_spec = True
if not sources and 'allow_updates' in kwargs:
if kwargs['allow_updates']:
oper = '>='
if not _fulfills_version_spec(cver, oper, verstr):
log.debug(
'Current version ({0}) did not match desired version '
'specification ({1}), adding to installation targets'
.format(cver, val)
)
targets[key] = val
elif pkg_verify and oper == '==':
verify_result = __salt__['pkg.verify'](key,
ignore_types=ignore_types)
if verify_result:
to_reinstall[key] = val
altered_files[key] = verify_result
if problems:
return {'name': name,
'changes': {},
'result': False,
'comment': ' '.join(problems)}
if not any((targets, to_unpurge, to_reinstall)):
# All specified packages are installed
msg = (
'All specified packages are already installed{0}.'
.format(' and are at the desired version' if version_spec else '')
msg = 'All specified packages are already installed{0}'
msg = msg.format(
' and are at the desired version' if version_spec and not sources
else ''
)
return {'name': name,
'changes': {},
'result': True,
'comment': msg}
return desired, targets, to_unpurge, to_reinstall, altered_files
return desired, targets, to_unpurge, to_reinstall, altered_files, warnings
def _verify_install(desired, new_pkgs):
@ -446,16 +475,8 @@ def _verify_install(desired, new_pkgs):
elif pkgver.endswith("*") and cver[0].startswith(pkgver[:-1]):
ok.append(pkgname)
continue
match = re.match('^([<>])?(=)?([^<>=]+)$', pkgver)
gt_lt, eq, verstr = match.groups()
comparison = gt_lt or ''
comparison += eq or ''
# A comparison operator of "=" is redundant, but possible.
# Change it to "==" so that the version comparison works.
if comparison in ('=', ''):
comparison = '=='
if _fulfills_version_spec(cver, comparison, verstr):
oper, verstr = _get_comparison_spec(pkgver)
if _fulfills_version_spec(cver, oper, verstr):
ok.append(pkgname)
else:
failed.append(pkgname)
@ -695,11 +716,13 @@ def installed(
- pkg_verify:
- ignore_types: [config,doc]
normalize
Normalize the package name by removing the architecture. Default is
``True``. This is useful for poorly created packages which might
include the architecture as an actual part of the name such as kernel
modules which match a specific kernel version.
normalize : True
Normalize the package name by removing the architecture, if the
architecture of the package is different from the architecture of the
operating system. The ability to disable this behavior is useful for
poorly-created packages which include the architecture as an actual
part of the name, such as kernel modules which match a specific kernel
version.
.. versionadded:: 2014.7.0
@ -868,7 +891,8 @@ def installed(
**kwargs)
try:
desired, targets, to_unpurge, to_reinstall, altered_files = result
(desired, targets, to_unpurge,
to_reinstall, altered_files, warnings) = result
except ValueError:
# _find_install_targets() found no targets or encountered an error
@ -924,10 +948,13 @@ def installed(
return result
if to_unpurge and 'lowpkg.unpurge' not in __salt__:
return {'name': name,
'changes': {},
'result': False,
'comment': 'lowpkg.unpurge not implemented'}
ret = {'name': name,
'changes': {},
'result': False,
'comment': 'lowpkg.unpurge not implemented'}
if warnings:
ret['comment'] += '.' + '. '.join(warnings) + '.'
return ret
# Remove any targets not returned by _find_install_targets
if pkgs:
@ -959,20 +986,25 @@ def installed(
if to_reinstall:
# Add a comment for each package in to_reinstall with its
# pkg.verify output
for x in to_reinstall:
for reinstall_pkg in to_reinstall:
if sources:
pkgstr = x
pkgstr = reinstall_pkg
else:
pkgstr = _get_desired_pkg(x, to_reinstall)
pkgstr = _get_desired_pkg(reinstall_pkg, to_reinstall)
comment.append(
'\nPackage {0} is set to be reinstalled because the '
'following files have been altered:'.format(pkgstr)
)
comment.append('\n' + _nested_output(altered_files[x]))
return {'name': name,
'changes': {},
'result': None,
'comment': ' '.join(comment)}
comment.append(
'\n' + _nested_output(altered_files[reinstall_pkg])
)
ret = {'name': name,
'changes': {},
'result': None,
'comment': ' '.join(comment)}
if warnings:
ret['comment'] += '\n' + '. '.join(warnings) + '.'
return ret
changes = {'installed': {}}
modified_hold = None
@ -995,11 +1027,14 @@ def installed(
if os.path.isfile(rtag) and refresh:
os.remove(rtag)
except CommandExecutionError as exc:
return {'name': name,
'changes': {},
'result': False,
'comment': 'An error was encountered while installing '
'package(s): {0}'.format(exc)}
ret = {'name': name,
'changes': {},
'result': False,
'comment': 'An error was encountered while installing '
'package(s): {0}'.format(exc)}
if warnings:
ret['comment'] += '.' + '. '.join(warnings) + '.'
return ret
if isinstance(pkg_ret, dict):
changes['installed'].update(pkg_ret)
@ -1019,18 +1054,24 @@ def installed(
)
except (CommandExecutionError, SaltInvocationError) as exc:
comment.append(str(exc))
return {'name': name,
'changes': changes,
'result': False,
'comment': ' '.join(comment)}
ret = {'name': name,
'changes': changes,
'result': False,
'comment': ' '.join(comment)}
if warnings:
ret['comment'] += '.' + '. '.join(warnings) + '.'
return ret
else:
if 'result' in hold_ret and not hold_ret['result']:
return {'name': name,
'changes': {},
'result': False,
'comment': 'An error was encountered while '
'holding/unholding package(s): {0}'
.format(hold_ret['comment'])}
ret = {'name': name,
'changes': {},
'result': False,
'comment': 'An error was encountered while '
'holding/unholding package(s): {0}'
.format(hold_ret['comment'])}
if warnings:
ret['comment'] += '.' + '. '.join(warnings) + '.'
return ret
else:
modified_hold = [hold_ret[x] for x in hold_ret
if hold_ret[x]['changes']]
@ -1109,7 +1150,7 @@ def installed(
'{0}'.format(summary))
else:
comment.append(
'{0} targeted package{1} {2} already installed.'.format(
'{0} targeted package{1} {2} already installed'.format(
len(not_modified),
's' if len(not_modified) > 1 else '',
'were' if len(not_modified) > 1 else 'was'
@ -1152,45 +1193,49 @@ def installed(
# Rerun pkg.verify for packages in to_reinstall to determine failed
modified = []
failed = []
for x in to_reinstall:
retval = __salt__['pkg.verify'](x, ignore_types=ignore_types)
if retval:
failed.append(x)
altered_files[x] = retval
for reinstall_pkg in to_reinstall:
verify_result = __salt__['pkg.verify'](reinstall_pkg,
ignore_types=ignore_types)
if verify_result:
failed.append(reinstall_pkg)
altered_files[reinstall_pkg] = verify_result
else:
modified.append(x)
modified.append(reinstall_pkg)
if modified:
# Add a comment for each package in modified with its pkg.verify output
for x in modified:
for modified_pkg in modified:
if sources:
pkgstr = x
pkgstr = modified_pkg
else:
pkgstr = _get_desired_pkg(x, desired)
pkgstr = _get_desired_pkg(modified_pkg, desired)
comment.append(
'\nPackage {0} was reinstalled. The following files were '
'remediated:'.format(pkgstr)
)
comment.append(_nested_output(altered_files[x]))
comment.append(_nested_output(altered_files[modified_pkg]))
if failed:
# Add a comment for each package in failed with its pkg.verify output
for x in failed:
for failed_pkg in failed:
if sources:
pkgstr = x
pkgstr = failed_pkg
else:
pkgstr = _get_desired_pkg(x, desired)
pkgstr = _get_desired_pkg(failed_pkg, desired)
comment.append(
'\nReinstall was not successful for package {0}. The '
'following files could not be remediated:'.format(pkgstr)
)
comment.append(_nested_output(altered_files[x]))
comment.append(_nested_output(altered_files[failed_pkg]))
result = False
return {'name': name,
'changes': changes,
'result': result,
'comment': ' '.join(comment)}
ret = {'name': name,
'changes': changes,
'result': result,
'comment': ' '.join(comment)}
if warnings:
ret['comment'] += '\n' + '. '.join(warnings) + '.'
return ret
def latest(
@ -1319,7 +1364,7 @@ def latest(
for pkg in desired_pkgs:
if not avail[pkg]:
if not cur[pkg]:
msg = 'No information found for {0!r}.'.format(pkg)
msg = 'No information found for \'{0}\'.'.format(pkg)
log.error(msg)
problems.append(msg)
elif not cur[pkg] \
@ -1363,7 +1408,7 @@ def latest(
'up-to-date: {0}'
).format(up_to_date_details)
else:
comment += ' {0} packages are already up-to-date.'.format(
comment += ' {0} packages are already up-to-date'.format(
up_to_date_nb
)
@ -1412,7 +1457,7 @@ def latest(
msg = 'The following packages were already up-to-date: ' \
'{0}'.format(', '.join(sorted(up_to_date)))
else:
msg = '{0} packages were already up-to-date. '.format(
msg = '{0} packages were already up-to-date '.format(
len(up_to_date))
comments.append(msg)
@ -1438,7 +1483,7 @@ def latest(
'{0}'.format(', '.join(sorted(up_to_date)))
else:
comment += '{0} packages were already ' \
'up-to-date.'.format(len(up_to_date))
'up-to-date'.format(len(up_to_date))
return {'name': name,
'changes': changes,
@ -1453,7 +1498,7 @@ def latest(
'({0}).'.format(', '.join(sorted(desired_pkgs)))
else:
comment = 'Package {0} is already ' \
'up-to-date.'.format(desired_pkgs[0])
'up-to-date'.format(desired_pkgs[0])
return {'name': name,
'changes': {},
@ -1461,7 +1506,13 @@ def latest(
'comment': comment}
def _uninstall(action='remove', name=None, version=None, pkgs=None, **kwargs):
def _uninstall(
action='remove',
name=None,
version=None,
pkgs=None,
normalize=True,
**kwargs):
'''
Common function for package removal
'''
@ -1469,18 +1520,21 @@ def _uninstall(action='remove', name=None, version=None, pkgs=None, **kwargs):
return {'name': name,
'changes': {},
'result': False,
'comment': 'Invalid action {0!r}. '
'comment': 'Invalid action \'{0}\'. '
'This is probably a bug.'.format(action)}
try:
pkg_params = __salt__['pkg_resource.parse_targets'](name, pkgs)[0]
pkg_params = __salt__['pkg_resource.parse_targets'](
name,
pkgs,
normalize=normalize)[0]
except MinionError as exc:
return {'name': name,
'changes': {},
'result': False,
'comment': 'An error was encountered while parsing targets: '
'{0}'.format(exc)}
targets = _find_remove_targets(name, version, pkgs, **kwargs)
targets = _find_remove_targets(name, version, pkgs, normalize, **kwargs)
if isinstance(targets, dict) and 'result' in targets:
return targets
elif not isinstance(targets, list):
@ -1544,7 +1598,7 @@ def _uninstall(action='remove', name=None, version=None, pkgs=None, **kwargs):
'comment': ' '.join(comments)}
def removed(name, version=None, pkgs=None, **kwargs):
def removed(name, version=None, pkgs=None, normalize=True, **kwargs):
'''
Verify that a package is not installed, calling ``pkg.remove`` if necessary
to remove the package.
@ -1556,6 +1610,15 @@ def removed(name, version=None, pkgs=None, **kwargs):
The version of the package that should be removed. Don't do anything if
the package is installed with an unmatching version.
normalize : True
Normalize the package name by removing the architecture, if the
architecture of the package is different from the architecture of the
operating system. The ability to disable this behavior is useful for
poorly-created packages which include the architecture as an actual
part of the name, such as kernel modules which match a specific kernel
version.
.. versionadded:: Beryllium
Multiple Package Options:
@ -1568,7 +1631,7 @@ def removed(name, version=None, pkgs=None, **kwargs):
'''
try:
return _uninstall(action='remove', name=name, version=version,
pkgs=pkgs, **kwargs)
pkgs=pkgs, normalize=normalize, **kwargs)
except CommandExecutionError as exc:
return {'name': name,
'changes': {},
@ -1576,7 +1639,7 @@ def removed(name, version=None, pkgs=None, **kwargs):
'comment': str(exc)}
def purged(name, version=None, pkgs=None, **kwargs):
def purged(name, version=None, pkgs=None, normalize=True, **kwargs):
'''
Verify that a package is not installed, calling ``pkg.purge`` if necessary
to purge the package. All configuration files are also removed.
@ -1588,6 +1651,15 @@ def purged(name, version=None, pkgs=None, **kwargs):
The version of the package that should be removed. Don't do anything if
the package is installed with an unmatching version.
normalize : True
Normalize the package name by removing the architecture, if the
architecture of the package is different from the architecture of the
operating system. The ability to disable this behavior is useful for
poorly-created packages which include the architecture as an actual
part of the name, such as kernel modules which match a specific kernel
version.
.. versionadded:: Beryllium
Multiple Package Options:
@ -1600,7 +1672,7 @@ def purged(name, version=None, pkgs=None, **kwargs):
'''
try:
return _uninstall(action='purge', name=name, version=version,
pkgs=pkgs, **kwargs)
pkgs=pkgs, normalize=normalize **kwargs)
except CommandExecutionError as exc:
return {'name': name,
'changes': {},
@ -1642,11 +1714,11 @@ def uptodate(name, refresh=False, **kwargs):
ret['comment'] = str(exc)
return ret
else:
ret['comment'] = 'refresh must be a boolean.'
ret['comment'] = 'refresh must be a boolean'
return ret
if not packages:
ret['comment'] = 'System is already up-to-date.'
ret['comment'] = 'System is already up-to-date'
ret['result'] = True
return ret
elif __opts__['test']: