Mini Shell
"""
Package support for pkgin based systems, inspired from freebsdpkg module
.. important::
If you feel that Salt should be using this module to manage packages on a
minion, and it is using a different module (or gives an error similar to
*'pkg.install' is not available*), see :ref:`here
<module-provider-override>`.
"""
import copy
import logging
import os
import re
import salt.utils.data
import salt.utils.decorators as decorators
import salt.utils.functools
import salt.utils.path
import salt.utils.pkg
from salt.exceptions import CommandExecutionError, MinionError
VERSION_MATCH = re.compile(r"pkgin(?:[\s]+)([\d.]+)(?:[\s]+)(?:.*)")
log = logging.getLogger(__name__)
# Define the module's virtual name
__virtualname__ = "pkg"
@decorators.memoize
def _check_pkgin():
"""
Looks to see if pkgin is present on the system, return full path
"""
ppath = salt.utils.path.which("pkgin")
if ppath is None:
# pkgin was not found in $PATH, try to find it via LOCALBASE
try:
localbase = __salt__["cmd.run"](
"pkg_info -Q LOCALBASE pkgin", output_loglevel="trace"
)
if localbase is not None:
ppath = f"{localbase}/bin/pkgin"
if not os.path.exists(ppath):
return None
except CommandExecutionError:
return None
return ppath
@decorators.memoize
def _get_version():
"""
Get the pkgin version
"""
version_string = __salt__["cmd.run"](
[_check_pkgin(), "-v"], output_loglevel="trace"
)
if version_string is None:
# Dunno why it would, but...
return False
version_match = VERSION_MATCH.search(version_string)
if not version_match:
return False
return version_match.group(1).split(".")
@decorators.memoize
def _supports_regex():
"""
Check support of regexp
"""
return tuple(int(i) for i in _get_version()) > (0, 5)
@decorators.memoize
def _supports_parsing():
"""
Check support of parsing
"""
return tuple(int(i) for i in _get_version()) > (0, 6)
def __virtual__():
"""
Set the virtual pkg module if the os is supported by pkgin
"""
supported = ["NetBSD", "SunOS", "DragonFly", "Minix", "Darwin", "SmartOS"]
if __grains__["os"] in supported and _check_pkgin():
return __virtualname__
return (
False,
"The pkgin execution module cannot be loaded: only "
"available on {} systems.".format(", ".join(supported)),
)
def _splitpkg(name):
"""
Split package name from versioned string
"""
# name is in the format foobar-1.0nb1, already space-splitted
if name[0].isalnum() and name != "No": # avoid < > = and 'No result'
return name.split(";", 1)[0].rsplit("-", 1)
def search(pkg_name, **kwargs):
"""
Searches for an exact match using pkgin ^package$
CLI Example:
.. code-block:: bash
salt '*' pkg.search 'mysql-server'
"""
pkglist = {}
pkgin = _check_pkgin()
if not pkgin:
return pkglist
if _supports_regex():
pkg_name = f"^{pkg_name}$"
out = __salt__["cmd.run"]([pkgin, "se", pkg_name], output_loglevel="trace")
for line in out.splitlines():
if line:
match = _splitpkg(line.split()[0])
if match:
pkglist[match[0]] = match[1]
return pkglist
def latest_version(*names, **kwargs):
"""
.. versionchanged:: 2016.3.0
Return the latest version of the named package available for upgrade or
installation.
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> ...
"""
refresh = salt.utils.data.is_true(kwargs.pop("refresh", True))
pkglist = {}
pkgin = _check_pkgin()
if not pkgin:
return pkglist
# Refresh before looking for the latest version available
if refresh:
refresh_db()
cmd_prefix = [pkgin, "se"]
if _supports_parsing():
cmd_prefix.insert(1, "-p")
for name in names:
cmd = copy.deepcopy(cmd_prefix)
cmd.append(f"^{name}$" if _supports_regex() else name)
out = __salt__["cmd.run"](cmd, output_loglevel="trace")
for line in out.splitlines():
if line.startswith("No results found for"):
return pkglist
p = line.split(";" if _supports_parsing() else None)
if p and p[0] in ("=:", "<:", ">:", ""):
# These are explanation comments
continue
elif p:
s = _splitpkg(p[0])
if s:
if not s[0] in pkglist:
if len(p) > 1 and p[1] in ("<", "", "="):
pkglist[s[0]] = s[1]
else:
pkglist[s[0]] = ""
if pkglist and len(names) == 1:
if names[0] in pkglist:
return pkglist[names[0]]
else:
return pkglist
# available_version is being deprecated
available_version = salt.utils.functools.alias_function(
latest_version, "available_version"
)
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 refresh_db(force=False, **kwargs):
"""
Use pkg update to get latest pkg_summary
force
Pass -f so that the cache is always refreshed.
.. versionadded:: 2018.3.0
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__)
pkgin = _check_pkgin()
if pkgin:
cmd = [pkgin, "up"]
if force:
cmd.insert(1, "-f")
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 _list_pkgs_from_context(versions_as_list):
"""
Use pkg list from __context__
"""
if versions_as_list:
return __context__["pkg.list_pkgs"]
else:
ret = copy.deepcopy(__context__["pkg.list_pkgs"])
__salt__["pkg_resource.stringify"](ret)
return ret
def list_pkgs(versions_as_list=False, **kwargs):
"""
.. versionchanged:: 2016.3.0
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 {}
if "pkg.list_pkgs" in __context__ and kwargs.get("use_context", True):
return _list_pkgs_from_context(versions_as_list)
pkgin = _check_pkgin()
ret = {}
out = __salt__["cmd.run"](
[pkgin, "ls"] if pkgin else ["pkg_info"], output_loglevel="trace"
)
for line in out.splitlines():
try:
# Some versions of pkgin check isatty unfortunately
# this results in cases where a ' ' or ';' can be used
pkg, ver = re.split("[; ]", line, 1)[0].rsplit("-", 1)
except ValueError:
continue
__salt__["pkg_resource.add_pkg"](ret, pkg, ver)
__salt__["pkg_resource.sort_pkglist"](ret)
__context__["pkg.list_pkgs"] = copy.deepcopy(ret)
if not versions_as_list:
__salt__["pkg_resource.stringify"](ret)
return ret
def list_upgrades(refresh=True, **kwargs):
"""
List all available package upgrades.
.. versionadded:: 2018.3.0
refresh
Whether or not to refresh the package database before installing.
CLI Example:
.. code-block:: bash
salt '*' pkg.list_upgrades
"""
pkgs = {}
for pkg in sorted(list_pkgs(refresh=refresh).keys()):
# NOTE: we already optionally refreshed in de list_pkg call
pkg_upgrade = latest_version(pkg, refresh=False)
if pkg_upgrade:
pkgs[pkg] = pkg_upgrade
return pkgs
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 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>
"""
try:
pkg_params, pkg_type = __salt__["pkg_resource.parse_targets"](
name, pkgs, sources, **kwargs
)
except MinionError as exc:
raise CommandExecutionError(exc)
# Support old "repo" argument
repo = kwargs.get("repo", "")
if not fromrepo and repo:
fromrepo = repo
if not pkg_params:
return {}
env = []
args = []
pkgin = _check_pkgin()
if pkgin:
cmd = pkgin
if fromrepo:
log.info("Setting PKG_REPOS=%s", fromrepo)
env.append(("PKG_REPOS", fromrepo))
else:
cmd = "pkg_add"
if fromrepo:
log.info("Setting PKG_PATH=%s", fromrepo)
env.append(("PKG_PATH", fromrepo))
if pkg_type == "file":
cmd = "pkg_add"
elif pkg_type == "repository":
if pkgin:
if refresh:
args.append("-f") # update repo db
args.extend(("-y", "in")) # Assume yes when asked
args.insert(0, cmd)
args.extend(pkg_params)
old = list_pkgs()
out = __salt__["cmd.run_all"](args, env=env, output_loglevel="trace")
if out["retcode"] != 0 and out["stderr"]:
errors = [out["stderr"]]
else:
errors = []
__context__.pop("pkg.list_pkgs", None)
new = list_pkgs()
ret = salt.utils.data.compare_dicts(old, new)
if errors:
raise CommandExecutionError(
"Problem encountered installing package(s)",
info={"errors": errors, "changes": ret},
)
_rehash()
return ret
def upgrade(refresh=True, pkgs=None, **kwargs):
"""
Run pkg upgrade, if pkgin used. Otherwise do nothing
refresh
Whether or not to refresh the package database before installing.
Multiple Package Upgrade Options:
pkgs
A list of packages to upgrade from a software repository. Must be
passed as a python list.
CLI Example:
.. code-block:: bash
salt '*' pkg.upgrade pkgs='["foo","bar"]'
Returns a dictionary containing the changes:
.. code-block:: python
{'<package>': {'old': '<old-version>',
'new': '<new-version>'}}
CLI Example:
.. code-block:: bash
salt '*' pkg.upgrade
"""
pkgin = _check_pkgin()
if not pkgin:
# There is not easy way to upgrade packages with old package system
return {}
if salt.utils.data.is_true(refresh):
refresh_db()
old = list_pkgs()
cmds = []
if not pkgs:
cmds.append([pkgin, "-y", "full-upgrade"])
elif salt.utils.data.is_list(pkgs):
for pkg in pkgs:
cmds.append([pkgin, "-y", "install", pkg])
else:
result = {
"retcode": 1,
"reason": "Ignoring the parameter `pkgs` because it is not a list!",
}
log.error(result["reason"])
for cmd in cmds:
result = __salt__["cmd.run_all"](
cmd, output_loglevel="trace", python_shell=False
)
if result["retcode"] != 0:
break
__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 remove(name=None, pkgs=None, **kwargs):
"""
name
The name of the package to be deleted.
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.
.. versionadded:: 0.16.0
Returns a list containing the removed packages.
CLI Example:
.. code-block:: bash
salt '*' pkg.remove <package name>
salt '*' pkg.remove <package1>,<package2>,<package3>
salt '*' pkg.remove pkgs='["foo", "bar"]'
"""
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()
args = []
for param in pkg_params:
ver = old.get(param, [])
if not ver:
continue
if isinstance(ver, list):
args.extend([f"{param}-{v}" for v in ver])
else:
args.append(f"{param}-{ver}")
if not args:
return {}
pkgin = _check_pkgin()
cmd = [pkgin, "-y", "remove"] if pkgin else ["pkg_remove"]
cmd.extend(args)
out = __salt__["cmd.run_all"](cmd, output_loglevel="trace")
if out["retcode"] != 0 and out["stderr"]:
errors = [out["stderr"]]
else:
errors = []
__context__.pop("pkg.list_pkgs", None)
new = list_pkgs()
ret = salt.utils.data.compare_dicts(old, new)
if errors:
raise CommandExecutionError(
"Problem encountered removing package(s)",
info={"errors": errors, "changes": ret},
)
return ret
def purge(name=None, pkgs=None, **kwargs):
"""
Package purges are not supported, this function is identical to
``remove()``.
name
The name of the package to be deleted.
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.
.. versionadded:: 0.16.0
Returns a dict containing the changes.
CLI Example:
.. code-block:: bash
salt '*' pkg.purge <package name>
salt '*' pkg.purge <package1>,<package2>,<package3>
salt '*' pkg.purge pkgs='["foo", "bar"]'
"""
return remove(name=name, pkgs=pkgs)
def _rehash():
"""
Recomputes internal hash table for the PATH variable.
Use 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 file_list(package, **kwargs):
"""
List the files that belong to a package.
CLI Examples:
.. code-block:: bash
salt '*' pkg.file_list nginx
"""
ret = file_dict(package)
files = []
for pkg_files in ret["files"].values():
files.extend(pkg_files)
ret["files"] = files
return ret
def file_dict(*packages, **kwargs):
"""
.. versionchanged:: 2016.3.0
List the files that belong to a package.
CLI Examples:
.. code-block:: bash
salt '*' pkg.file_dict nginx
salt '*' pkg.file_dict nginx varnish
"""
errors = []
files = {}
for package in packages:
cmd = ["pkg_info", "-qL", package]
ret = __salt__["cmd.run_all"](cmd, output_loglevel="trace")
files[package] = []
for line in ret["stderr"].splitlines():
errors.append(line)
for line in ret["stdout"].splitlines():
if line.startswith("/"):
files[package].append(line)
else:
continue # unexpected string
ret = {"errors": errors, "files": files}
for field in list(ret):
if not ret[field] or ret[field] == "":
del ret[field]
return ret
def normalize_name(pkgs, **kwargs):
"""
Normalize package names
.. note::
Nothing special to do to normalize, just return
the original. (We do need it to be compatible
with the pkg_resource provider.)
"""
return pkgs
Zerion Mini Shell 1.0