File: //usr/lib/python2.7/site-packages/salt/modules/xbpspkg.py
# -*- coding: utf-8 -*-
'''
Package support for XBPS package manager (used by VoidLinux)
.. versionadded:: 2016.11.0
'''
# TODO: what about the initial acceptance of repo's fingerprint when adding a
# new repo?
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import os
import re
import logging
import glob
# Import salt libs
import salt.utils.data
import salt.utils.files
import salt.utils.path
import salt.utils.pkg
import salt.utils.stringutils
import salt.utils.decorators as decorators
from salt.exceptions import CommandExecutionError, MinionError
log = logging.getLogger(__name__)
# Define the module's virtual name
__virtualname__ = 'pkg'
def __virtual__():
'''
Set the virtual pkg module if the os is Void and xbps-install found
'''
if __grains__['os'] in ('Void') and _check_xbps():
return __virtualname__
return False
@decorators.memoize
def _check_xbps():
'''
Looks to see if xbps-install is present on the system, return full path
'''
return salt.utils.path.which('xbps-install')
@decorators.memoize
def _get_version():
'''
Get the xbps version
'''
version_string = __salt__['cmd.run'](
[_check_xbps(), '--version'],
output_loglevel='trace')
if version_string is None:
# Dunno why it would, but...
return False
VERSION_MATCH = re.compile(r'(?:XBPS:[\s]+)([\d.]+)(?:[\s]+.*)')
version_match = VERSION_MATCH.search(version_string)
if not version_match:
return False
return version_match.group(1).split('.')
def _rehash():
'''
Recomputes internal hash table for the PATH variable.
Used whenever a new command is created during the current
session.
'''
shell = __salt__['environ.get']('SHELL')
if shell.split('/')[-1] in ('csh', 'tcsh'):
__salt__['cmd.run']('rehash', output_loglevel='trace')
def list_pkgs(versions_as_list=False, **kwargs):
'''
List the packages currently installed as a dict::
{'<package_name>': '<version>'}
CLI Example:
.. code-block:: bash
salt '*' pkg.list_pkgs
'''
versions_as_list = salt.utils.data.is_true(versions_as_list)
# not yet implemented or not applicable
if any([salt.utils.data.is_true(kwargs.get(x)) for x in ('removed', 'purge_desired')]):
return {}
cmd = 'xbps-query -l'
ret = {}
out = __salt__['cmd.run'](cmd, output_loglevel='trace')
for line in out.splitlines():
if not line:
continue
try:
# xbps-query -l output sample:
# ii desktop-file-utils-0.22_4 Utilities to ...
#
# XXX handle package status (like 'ii') ?
pkg, ver = line.split(None)[1].rsplit('-', 1)
except ValueError:
log.error(
'xbps-query: Unexpected formatting in line: "%s"',
line
)
__salt__['pkg_resource.add_pkg'](ret, pkg, ver)
__salt__['pkg_resource.sort_pkglist'](ret)
if not versions_as_list:
__salt__['pkg_resource.stringify'](ret)
return ret
def list_upgrades(refresh=True):
'''
Check whether or not an upgrade is available for all packages
CLI Example:
.. code-block:: bash
salt '*' pkg.list_upgrades
'''
# sample output of 'xbps-install -un':
# fuse-2.9.4_4 update i686 http://repo.voidlinux.eu/current 298133 91688
# xtools-0.34_1 update noarch http://repo.voidlinux.eu/current 21424 10752
refresh = salt.utils.data.is_true(refresh)
# Refresh repo index before checking for latest version available
if refresh:
refresh_db()
ret = {}
# retrieve list of updatable packages
cmd = 'xbps-install -un'
out = __salt__['cmd.run'](cmd, output_loglevel='trace')
for line in out.splitlines():
if not line:
continue
pkg = "base-system"
ver = "NonNumericValueIsError"
try:
pkg, ver = line.split()[0].rsplit('-', 1)
except (ValueError, IndexError):
log.error(
'xbps-query: Unexpected formatting in line: "%s"',
line
)
continue
log.trace('pkg=%s version=%s', pkg, ver)
ret[pkg] = ver
return ret
def latest_version(*names, **kwargs):
'''
Return the latest version of the named package available for upgrade or
installation. If more than one package name is specified, a dict of
name/version pairs is returned.
If the latest version of a given package is already installed, an empty
string will be returned for that package.
CLI Example:
.. code-block:: bash
salt '*' pkg.latest_version <package name>
salt '*' pkg.latest_version <package1> <package2> <package3> ...
'''
# Why using 'xbps-install -un' and not 'xbps-query -R':
# if several repos, xbps-query will produces this kind of output,
# that is difficult to handle correctly:
# [*] salt-2015.8.3_2 Remote execution system ...
# [-] salt-2015.8.3_1 Remote execution system ...
#
# XXX 'xbps-install -un pkg1 pkg2' won't produce any info on updatable pkg1
# if pkg2 is up-to-date. Bug of xbps 0.51, probably get fixed in 0.52.
# See related issue https://github.com/voidlinux/xbps/issues/145
#
# sample outputs of 'xbps-install -un':
# fuse-2.9.4_4 update i686 http://repo.voidlinux.eu/current 298133 91688
# xtools-0.34_1 update noarch http://repo.voidlinux.eu/current 21424 10752
# Package 'vim' is up to date.
refresh = salt.utils.data.is_true(kwargs.pop('refresh', True))
if len(names) == 0:
return ''
# Refresh repo index before checking for latest version available
if refresh:
refresh_db()
# Initialize the dict with empty strings
ret = {}
for name in names:
ret[name] = ''
# retrieve list of updatable packages
# ignore return code since 'is up to date' case produces retcode==17 (xbps 0.51)
cmd = ['xbps-install', '-un']
cmd.extend(names)
out = __salt__['cmd.run'](cmd, ignore_retcode=True, output_loglevel='trace')
for line in out.splitlines():
if not line:
continue
if line.find(' is up to date.') != -1:
continue
# retrieve tuple pkgname version
try:
pkg, ver = line.split()[0].rsplit('-', 1)
except (ValueError, IndexError):
log.error(
'xbps-query: Unexpected formatting in line: "%s"',
line
)
continue
log.trace('pkg=%s version=%s', pkg, ver)
if pkg in names:
ret[pkg] = ver
# Return a string if only one package name passed
if len(names) == 1:
return ret[names[0]]
return ret
# available_version is being deprecated
available_version = latest_version
def upgrade_available(name):
'''
Check whether or not an upgrade is available for a given package
CLI Example:
.. code-block:: bash
salt '*' pkg.upgrade_available <package name>
'''
return latest_version(name) != ''
def refresh_db():
'''
Update list of available packages from installed repos
CLI Example:
.. code-block:: bash
salt '*' pkg.refresh_db
'''
# Remove rtag file to keep multiple refreshes from happening in pkg states
salt.utils.pkg.clear_rtag(__opts__)
cmd = 'xbps-install -Sy'
call = __salt__['cmd.run_all'](cmd, output_loglevel='trace')
if call['retcode'] != 0:
comment = ''
if 'stderr' in call:
comment += call['stderr']
raise CommandExecutionError(comment)
return True
def version(*names, **kwargs):
'''
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.
CLI Example:
.. code-block:: bash
salt '*' pkg.version <package name>
salt '*' pkg.version <package1> <package2> <package3> ...
'''
return __salt__['pkg_resource.version'](*names, **kwargs)
def upgrade(refresh=True):
'''
Run a full system upgrade
refresh
Whether or not to refresh the package database before installing.
Default is `True`.
Returns a dictionary containing the changes:
.. code-block:: python
{'<package>': {'old': '<old-version>',
'new': '<new-version>'}}
CLI Example:
.. code-block:: bash
salt '*' pkg.upgrade
'''
# XXX if xbps has to be upgraded, 2 times is required to fully upgrade
# system: one for xbps, a subsequent one for all other packages. Not
# handled in this code.
old = list_pkgs()
cmd = ['xbps-install', '-{0}yu'.format('S' if refresh else '')]
result = __salt__['cmd.run_all'](cmd,
output_loglevel='trace',
python_shell=False)
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
ret = salt.utils.data.compare_dicts(old, new)
if result['retcode'] != 0:
raise CommandExecutionError(
'Problem encountered upgrading packages',
info={'changes': ret, 'result': result}
)
return ret
def install(name=None, refresh=False, fromrepo=None,
pkgs=None, sources=None, **kwargs):
'''
Install the passed package
name
The name of the package to be installed.
refresh
Whether or not to refresh the package database before installing.
fromrepo
Specify a package repository (url) to install from.
Multiple Package Installation Options:
pkgs
A list of packages to install from a software repository. Must be
passed as a python list.
CLI Example:
.. code-block:: bash
salt '*' pkg.install pkgs='["foo","bar"]'
sources
A list of packages to install. Must be passed as a list of dicts,
with the keys being package names, and the values being the source URI
or local path to the package.
CLI Example:
.. code-block:: bash
salt '*' pkg.install sources='[{"foo": "salt://foo.deb"},{"bar": "salt://bar.deb"}]'
Return a dict containing the new package names and versions::
{'<package>': {'old': '<old-version>',
'new': '<new-version>'}}
CLI Example:
.. code-block:: bash
salt '*' pkg.install <package name>
'''
# XXX sources is not yet used in this code
try:
pkg_params, pkg_type = __salt__['pkg_resource.parse_targets'](
name, pkgs, sources, **kwargs
)
except MinionError as exc:
raise CommandExecutionError(exc)
if pkg_params is None or len(pkg_params) == 0:
return {}
if pkg_type != 'repository':
log.error('xbps: pkg_type "%s" not supported.', pkg_type)
return {}
cmd = ['xbps-install']
if refresh:
cmd.append('-S') # update repo db
if fromrepo:
cmd.append('--repository={0}'.format(fromrepo))
cmd.append('-y') # assume yes when asked
cmd.extend(pkg_params)
old = list_pkgs()
__salt__['cmd.run'](cmd, output_loglevel='trace')
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
_rehash()
return salt.utils.data.compare_dicts(old, new)
def remove(name=None, pkgs=None, recursive=True, **kwargs):
'''
name
The name of the package to be deleted.
recursive
Also remove dependent packages (not required elsewhere).
Default mode: enabled.
Multiple Package Options:
pkgs
A list of packages to delete. Must be passed as a python list. The
``name`` parameter will be ignored if this option is passed.
Returns a list containing the removed packages.
CLI Example:
.. code-block:: bash
salt '*' pkg.remove <package name> [recursive=False]
salt '*' pkg.remove <package1>,<package2>,<package3> [recursive=False]
salt '*' pkg.remove pkgs='["foo", "bar"]' [recursive=False]
'''
try:
pkg_params, pkg_type = __salt__['pkg_resource.parse_targets'](
name, pkgs
)
except MinionError as exc:
raise CommandExecutionError(exc)
if not pkg_params:
return {}
old = list_pkgs()
# keep only installed packages
targets = [x for x in pkg_params if x in old]
if not targets:
return {}
cmd = ['xbps-remove', '-y']
if recursive:
cmd.append('-R')
cmd.extend(targets)
__salt__['cmd.run'](cmd, output_loglevel='trace')
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
return salt.utils.data.compare_dicts(old, new)
def list_repos():
'''
List all repos known by XBPS
CLI Example:
.. code-block:: bash
salt '*' pkg.list_repos
'''
repos = {}
out = __salt__['cmd.run']('xbps-query -L', output_loglevel='trace')
for line in out.splitlines():
repo = {}
if not line:
continue
try:
nb, url, rsa = line.strip().split(' ', 2)
except ValueError:
log.error(
'Problem parsing xbps-query: '
'Unexpected formatting in line: "%s"', line
)
repo['nbpkg'] = int(nb) if nb.isdigit() else 0
repo['url'] = url
repo['rsasigned'] = True if rsa == '(RSA signed)' else False
repos[repo['url']] = repo
return repos
def get_repo(repo, **kwargs):
'''
Display information about the repo.
CLI Examples:
.. code-block:: bash
salt '*' pkg.get_repo 'repo-url'
'''
repos = list_repos()
if repo in repos:
return repos[repo]
return {}
def _locate_repo_files(repo, rewrite=False):
'''
Find what file a repo is called in.
Helper function for add_repo() and del_repo()
repo
url of the repo to locate (persistent).
rewrite
Whether to remove matching repository settings during this process.
Returns a list of absolute paths.
'''
ret_val = []
files = []
conf_dirs = ['/etc/xbps.d/', '/usr/share/xbps.d/']
name_glob = '*.conf'
# Matches a line where first printing is "repository" and there is an equals
# sign before the repo, an optional forwardslash at the end of the repo name,
# and it's possible for there to be a comment after repository=repo
regex = re.compile(r'\s*repository\s*=\s*'+repo+r'/?\s*(#.*)?$')
for cur_dir in conf_dirs:
files.extend(glob.glob(cur_dir+name_glob))
for filename in files:
write_buff = []
with salt.utils.files.fopen(filename, 'r') as cur_file:
for line in cur_file:
if regex.match(salt.utils.stringutils.to_unicode(line)):
ret_val.append(filename)
else:
write_buff.append(line)
if rewrite and filename in ret_val:
if len(write_buff) > 0:
with salt.utils.files.fopen(filename, 'w') as rewrite_file:
rewrite_file.writelines(write_buff)
else: # Prune empty files
os.remove(filename)
return ret_val
def add_repo(repo, conffile='/usr/share/xbps.d/15-saltstack.conf'):
'''
Add an XBPS repository to the system.
repo
url of repo to add (persistent).
conffile
path to xbps conf file to add this repo
default: /usr/share/xbps.d/15-saltstack.conf
CLI Examples:
.. code-block:: bash
salt '*' pkg.add_repo <repo url> [conffile=/path/to/xbps/repo.conf]
'''
if len(_locate_repo_files(repo)) == 0:
try:
with salt.utils.files.fopen(conffile, 'a+') as conf_file:
conf_file.write(
salt.utils.stringutils.to_str(
'repository={0}\n'.format(repo)
)
)
except IOError:
return False
return True
def del_repo(repo):
'''
Remove an XBPS repository from the system.
repo
url of repo to remove (persistent).
CLI Examples:
.. code-block:: bash
salt '*' pkg.del_repo <repo url>
'''
try:
_locate_repo_files(repo, rewrite=True)
except IOError:
return False
else:
return True