1"""
2Support for Opkg
3
4.. important::
5    If you feel that Salt should be using this module to manage packages on a
6    minion, and it is using a different module (or gives an error similar to
7    *'pkg.install' is not available*), see :ref:`here
8    <module-provider-override>`.
9
10.. versionadded:: 2016.3.0
11
12.. note::
13
14    For version comparison support on opkg < 0.3.4, the ``opkg-utils`` package
15    must be installed.
16
17"""
18
19import copy
20import errno
21import logging
22import os
23import pathlib
24import re
25import shlex
26
27import salt.utils.args
28import salt.utils.data
29import salt.utils.files
30import salt.utils.itertools
31import salt.utils.path
32import salt.utils.pkg
33import salt.utils.stringutils
34import salt.utils.versions
35from salt.exceptions import CommandExecutionError, MinionError, SaltInvocationError
36
37REPO_REGEXP = r'^#?\s*(src|src/gz)\s+([^\s<>]+|"[^<>]+")\s+[^\s<>]+'
38OPKG_CONFDIR = "/etc/opkg"
39ATTR_MAP = {
40    "Architecture": "arch",
41    "Homepage": "url",
42    "Installed-Time": "install_date_time_t",
43    "Maintainer": "packager",
44    "Package": "name",
45    "Section": "group",
46}
47
48log = logging.getLogger(__name__)
49
50# Define the module's virtual name
51__virtualname__ = "pkg"
52
53NILRT_RESTARTCHECK_STATE_PATH = "/var/lib/salt/restartcheck_state"
54
55
56def _get_nisysapi_conf_d_path():
57    return "/usr/lib/{}/nisysapi/conf.d/experts/".format(
58        "arm-linux-gnueabi"
59        if "arm" in __grains__.get("cpuarch")
60        else "x86_64-linux-gnu"
61    )
62
63
64def _update_nilrt_restart_state():
65    """
66    NILRT systems determine whether to reboot after various package operations
67    including but not limited to kernel module installs/removals by checking
68    specific file md5sums & timestamps. These files are touched/modified by
69    the post-install/post-remove functions of their respective packages.
70
71    The opkg module uses this function to store/update those file timestamps
72    and checksums to be used later by the restartcheck module.
73
74    """
75    # TODO: This stat & md5sum should be replaced with _fingerprint_file call -W. Werner, 2020-08-18
76    uname = __salt__["cmd.run_stdout"]("uname -r")
77    __salt__["cmd.shell"](
78        "stat -c %Y /lib/modules/{}/modules.dep >{}/modules.dep.timestamp".format(
79            uname, NILRT_RESTARTCHECK_STATE_PATH
80        )
81    )
82    __salt__["cmd.shell"](
83        "md5sum /lib/modules/{}/modules.dep >{}/modules.dep.md5sum".format(
84            uname, NILRT_RESTARTCHECK_STATE_PATH
85        )
86    )
87
88    # We can't assume nisysapi.ini always exists like modules.dep
89    nisysapi_path = "/usr/local/natinst/share/nisysapi.ini"
90    if os.path.exists(nisysapi_path):
91        # TODO: This stat & md5sum should be replaced with _fingerprint_file call -W. Werner, 2020-08-18
92        __salt__["cmd.shell"](
93            "stat -c %Y {} >{}/nisysapi.ini.timestamp".format(
94                nisysapi_path, NILRT_RESTARTCHECK_STATE_PATH
95            )
96        )
97        __salt__["cmd.shell"](
98            "md5sum {} >{}/nisysapi.ini.md5sum".format(
99                nisysapi_path, NILRT_RESTARTCHECK_STATE_PATH
100            )
101        )
102
103    # Expert plugin files get added to a conf.d dir, so keep track of the total
104    # no. of files, their timestamps and content hashes
105    nisysapi_conf_d_path = _get_nisysapi_conf_d_path()
106
107    if os.path.exists(nisysapi_conf_d_path):
108        with salt.utils.files.fopen(
109            "{}/sysapi.conf.d.count".format(NILRT_RESTARTCHECK_STATE_PATH), "w"
110        ) as fcount:
111            fcount.write(str(len(os.listdir(nisysapi_conf_d_path))))
112
113        for fexpert in os.listdir(nisysapi_conf_d_path):
114            _fingerprint_file(
115                filename=pathlib.Path(nisysapi_conf_d_path, fexpert),
116                fingerprint_dir=pathlib.Path(NILRT_RESTARTCHECK_STATE_PATH),
117            )
118
119
120def _fingerprint_file(*, filename, fingerprint_dir):
121    """
122    Compute stat & md5sum hash of provided ``filename``. Store
123    the hash and timestamp in ``fingerprint_dir``.
124
125    filename
126        ``Path`` to the file to stat & hash.
127
128    fingerprint_dir
129        ``Path`` of the directory to store the stat and hash output files.
130    """
131    __salt__["cmd.shell"](
132        "stat -c %Y {} > {}/{}.timestamp".format(
133            filename, fingerprint_dir, filename.name
134        )
135    )
136    __salt__["cmd.shell"](
137        "md5sum {} > {}/{}.md5sum".format(filename, fingerprint_dir, filename.name)
138    )
139
140
141def _get_restartcheck_result(errors):
142    """
143    Return restartcheck result and append errors (if any) to ``errors``
144    """
145    rs_result = __salt__["restartcheck.restartcheck"](verbose=False)
146    if isinstance(rs_result, dict) and "comment" in rs_result:
147        errors.append(rs_result["comment"])
148    return rs_result
149
150
151def _process_restartcheck_result(rs_result):
152    """
153    Check restartcheck output to see if system/service restarts were requested
154    and take appropriate action.
155    """
156    if "No packages seem to need to be restarted" in rs_result:
157        return
158    for rstr in rs_result:
159        if "System restart required" in rstr:
160            _update_nilrt_restart_state()
161            __salt__["system.set_reboot_required_witnessed"]()
162        else:
163            service = os.path.join("/etc/init.d", rstr)
164            if os.path.exists(service):
165                __salt__["cmd.run"]([service, "restart"])
166
167
168def __virtual__():
169    """
170    Confirm this module is on a nilrt based system
171    """
172    if __grains__.get("os_family") == "NILinuxRT":
173        try:
174            os.makedirs(NILRT_RESTARTCHECK_STATE_PATH)
175        except OSError as exc:
176            if exc.errno != errno.EEXIST:
177                return (
178                    False,
179                    "Error creating {} (-{}): {}".format(
180                        NILRT_RESTARTCHECK_STATE_PATH, exc.errno, exc.strerror
181                    ),
182                )
183        # populate state dir if empty
184        if not os.listdir(NILRT_RESTARTCHECK_STATE_PATH):
185            _update_nilrt_restart_state()
186        return __virtualname__
187
188    if os.path.isdir(OPKG_CONFDIR):
189        return __virtualname__
190    return False, "Module opkg only works on OpenEmbedded based systems"
191
192
193def latest_version(*names, **kwargs):
194    """
195    Return the latest version of the named package available for upgrade or
196    installation. If more than one package name is specified, a dict of
197    name/version pairs is returned.
198
199    If the latest version of a given package is already installed, an empty
200    string will be returned for that package.
201
202    CLI Example:
203
204    .. code-block:: bash
205
206        salt '*' pkg.latest_version <package name>
207        salt '*' pkg.latest_version <package name>
208        salt '*' pkg.latest_version <package1> <package2> <package3> ...
209    """
210    refresh = salt.utils.data.is_true(kwargs.pop("refresh", True))
211
212    if len(names) == 0:
213        return ""
214
215    ret = {}
216    for name in names:
217        ret[name] = ""
218
219    # Refresh before looking for the latest version available
220    if refresh:
221        refresh_db()
222
223    cmd = ["opkg", "list-upgradable"]
224    out = __salt__["cmd.run_stdout"](cmd, output_loglevel="trace", python_shell=False)
225    for line in salt.utils.itertools.split(out, "\n"):
226        try:
227            name, _oldversion, newversion = line.split(" - ")
228            if name in names:
229                ret[name] = newversion
230        except ValueError:
231            pass
232
233    # Return a string if only one package name passed
234    if len(names) == 1:
235        return ret[names[0]]
236    return ret
237
238
239# available_version is being deprecated
240available_version = latest_version
241
242
243def version(*names, **kwargs):
244    """
245    Returns a string representing the package version or an empty string if not
246    installed. If more than one package name is specified, a dict of
247    name/version pairs is returned.
248
249    CLI Example:
250
251    .. code-block:: bash
252
253        salt '*' pkg.version <package name>
254        salt '*' pkg.version <package1> <package2> <package3> ...
255    """
256    return __salt__["pkg_resource.version"](*names, **kwargs)
257
258
259def refresh_db(failhard=False, **kwargs):  # pylint: disable=unused-argument
260    """
261    Updates the opkg database to latest packages based upon repositories
262
263    Returns a dict, with the keys being package databases and the values being
264    the result of the update attempt. Values can be one of the following:
265
266    - ``True``: Database updated successfully
267    - ``False``: Problem updating database
268
269    failhard
270        If False, return results of failed lines as ``False`` for the package
271        database that encountered the error.
272        If True, raise an error with a list of the package databases that
273        encountered errors.
274
275        .. versionadded:: 2018.3.0
276
277    CLI Example:
278
279    .. code-block:: bash
280
281        salt '*' pkg.refresh_db
282    """
283    # Remove rtag file to keep multiple refreshes from happening in pkg states
284    salt.utils.pkg.clear_rtag(__opts__)
285    ret = {}
286    error_repos = []
287    cmd = ["opkg", "update"]
288    # opkg returns a non-zero retcode when there is a failure to refresh
289    # from one or more repos. Due to this, ignore the retcode.
290    call = __salt__["cmd.run_all"](
291        cmd,
292        output_loglevel="trace",
293        python_shell=False,
294        ignore_retcode=True,
295        redirect_stderr=True,
296    )
297
298    out = call["stdout"]
299    prev_line = ""
300    for line in salt.utils.itertools.split(out, "\n"):
301        if "Inflating" in line:
302            key = line.strip().split()[1][:-1]
303            ret[key] = True
304        elif "Updated source" in line:
305            # Use the previous line.
306            key = prev_line.strip().split()[1][:-1]
307            ret[key] = True
308        elif "Failed to download" in line:
309            key = line.strip().split()[5].split(",")[0]
310            ret[key] = False
311            error_repos.append(key)
312        prev_line = line
313
314    if failhard and error_repos:
315        raise CommandExecutionError(
316            "Error getting repos: {}".format(", ".join(error_repos))
317        )
318
319    # On a non-zero exit code where no failed repos were found, raise an
320    # exception because this appears to be a different kind of error.
321    if call["retcode"] != 0 and not error_repos:
322        raise CommandExecutionError(out)
323
324    return ret
325
326
327def _is_testmode(**kwargs):
328    """
329    Returns whether a test mode (noaction) operation was requested.
330    """
331    return bool(kwargs.get("test") or __opts__.get("test"))
332
333
334def _append_noaction_if_testmode(cmd, **kwargs):
335    """
336    Adds the --noaction flag to the command if it's running in the test mode.
337    """
338    if _is_testmode(**kwargs):
339        cmd.append("--noaction")
340
341
342def _build_install_command_list(cmd_prefix, to_install, to_downgrade, to_reinstall):
343    """
344    Builds a list of install commands to be executed in sequence in order to process
345    each of the to_install, to_downgrade, and to_reinstall lists.
346    """
347    cmds = []
348    if to_install:
349        cmd = copy.deepcopy(cmd_prefix)
350        cmd.extend(to_install)
351        cmds.append(cmd)
352    if to_downgrade:
353        cmd = copy.deepcopy(cmd_prefix)
354        cmd.append("--force-downgrade")
355        cmd.extend(to_downgrade)
356        cmds.append(cmd)
357    if to_reinstall:
358        cmd = copy.deepcopy(cmd_prefix)
359        cmd.append("--force-reinstall")
360        cmd.extend(to_reinstall)
361        cmds.append(cmd)
362
363    return cmds
364
365
366def _parse_reported_packages_from_install_output(output):
367    """
368    Parses the output of "opkg install" to determine what packages would have been
369    installed by an operation run with the --noaction flag.
370
371    We are looking for lines like:
372        Installing <package> (<version>) on <target>
373    or
374        Upgrading <package> from <oldVersion> to <version> on root
375    """
376    reported_pkgs = {}
377    install_pattern = re.compile(
378        r"Installing\s(?P<package>.*?)\s\((?P<version>.*?)\)\son\s(?P<target>.*?)"
379    )
380    upgrade_pattern = re.compile(
381        r"Upgrading\s(?P<package>.*?)\sfrom\s(?P<oldVersion>.*?)\sto\s(?P<version>.*?)\son\s(?P<target>.*?)"
382    )
383    for line in salt.utils.itertools.split(output, "\n"):
384        match = install_pattern.match(line)
385        if match is None:
386            match = upgrade_pattern.match(line)
387        if match:
388            reported_pkgs[match.group("package")] = match.group("version")
389
390    return reported_pkgs
391
392
393def _execute_install_command(cmd, parse_output, errors, parsed_packages):
394    """
395    Executes a command for the install operation.
396    If the command fails, its error output will be appended to the errors list.
397    If the command succeeds and parse_output is true, updated packages will be appended
398    to the parsed_packages dictionary.
399    """
400    out = __salt__["cmd.run_all"](cmd, output_loglevel="trace", python_shell=False)
401    if out["retcode"] != 0:
402        if out["stderr"]:
403            errors.append(out["stderr"])
404        else:
405            errors.append(out["stdout"])
406    elif parse_output:
407        parsed_packages.update(
408            _parse_reported_packages_from_install_output(out["stdout"])
409        )
410
411
412def install(
413    name=None, refresh=False, pkgs=None, sources=None, reinstall=False, **kwargs
414):
415    """
416    Install the passed package, add refresh=True to update the opkg database.
417
418    name
419        The name of the package to be installed. Note that this parameter is
420        ignored if either "pkgs" or "sources" is passed. Additionally, please
421        note that this option can only be used to install packages from a
422        software repository. To install a package file manually, use the
423        "sources" option.
424
425        CLI Example:
426
427        .. code-block:: bash
428
429            salt '*' pkg.install <package name>
430
431    refresh
432        Whether or not to refresh the package database before installing.
433
434    version
435        Install a specific version of the package, e.g. 1.2.3~0ubuntu0. Ignored
436        if "pkgs" or "sources" is passed.
437
438        .. versionadded:: 2017.7.0
439
440    reinstall : False
441        Specifying reinstall=True will use ``opkg install --force-reinstall``
442        rather than simply ``opkg install`` for requested packages that are
443        already installed.
444
445        If a version is specified with the requested package, then ``opkg
446        install --force-reinstall`` will only be used if the installed version
447        matches the requested version.
448
449        .. versionadded:: 2017.7.0
450
451
452    Multiple Package Installation Options:
453
454    pkgs
455        A list of packages to install from a software repository. Must be
456        passed as a python list.
457
458        CLI Example:
459
460        .. code-block:: bash
461
462            salt '*' pkg.install pkgs='["foo", "bar"]'
463            salt '*' pkg.install pkgs='["foo", {"bar": "1.2.3-0ubuntu0"}]'
464
465    sources
466        A list of IPK packages to install. Must be passed as a list of dicts,
467        with the keys being package names, and the values being the source URI
468        or local path to the package.  Dependencies are automatically resolved
469        and marked as auto-installed.
470
471        CLI Example:
472
473        .. code-block:: bash
474
475            salt '*' pkg.install sources='[{"foo": "salt://foo.deb"},{"bar": "salt://bar.deb"}]'
476
477    install_recommends
478        Whether to install the packages marked as recommended. Default is True.
479
480    only_upgrade
481        Only upgrade the packages (disallow downgrades), if they are already
482        installed. Default is False.
483
484        .. versionadded:: 2017.7.0
485
486    Returns a dict containing the new package names and versions::
487
488        {'<package>': {'old': '<old-version>',
489                       'new': '<new-version>'}}
490    """
491    refreshdb = salt.utils.data.is_true(refresh)
492
493    try:
494        pkg_params, pkg_type = __salt__["pkg_resource.parse_targets"](
495            name, pkgs, sources, **kwargs
496        )
497    except MinionError as exc:
498        raise CommandExecutionError(exc)
499
500    old = list_pkgs()
501    cmd_prefix = ["opkg", "install"]
502    to_install = []
503    to_reinstall = []
504    to_downgrade = []
505
506    _append_noaction_if_testmode(cmd_prefix, **kwargs)
507    if pkg_params is None or len(pkg_params) == 0:
508        return {}
509    elif pkg_type == "file":
510        if reinstall:
511            cmd_prefix.append("--force-reinstall")
512        if not kwargs.get("only_upgrade", False):
513            cmd_prefix.append("--force-downgrade")
514        to_install.extend(pkg_params)
515    elif pkg_type == "repository":
516        if not kwargs.get("install_recommends", True):
517            cmd_prefix.append("--no-install-recommends")
518        for pkgname, pkgversion in pkg_params.items():
519            if name and pkgs is None and kwargs.get("version") and len(pkg_params) == 1:
520                # Only use the 'version' param if 'name' was not specified as a
521                # comma-separated list
522                version_num = kwargs["version"]
523            else:
524                version_num = pkgversion
525
526            if version_num is None:
527                # Don't allow downgrades if the version
528                # number is not specified.
529                if reinstall and pkgname in old:
530                    to_reinstall.append(pkgname)
531                else:
532                    to_install.append(pkgname)
533            else:
534                pkgstr = "{}={}".format(pkgname, version_num)
535                cver = old.get(pkgname, "")
536                if (
537                    reinstall
538                    and cver
539                    and salt.utils.versions.compare(
540                        ver1=version_num, oper="==", ver2=cver, cmp_func=version_cmp
541                    )
542                ):
543                    to_reinstall.append(pkgstr)
544                elif not cver or salt.utils.versions.compare(
545                    ver1=version_num, oper=">=", ver2=cver, cmp_func=version_cmp
546                ):
547                    to_install.append(pkgstr)
548                else:
549                    if not kwargs.get("only_upgrade", False):
550                        to_downgrade.append(pkgstr)
551                    else:
552                        # This should cause the command to fail.
553                        to_install.append(pkgstr)
554
555    cmds = _build_install_command_list(
556        cmd_prefix, to_install, to_downgrade, to_reinstall
557    )
558
559    if not cmds:
560        return {}
561
562    if refreshdb:
563        refresh_db()
564
565    errors = []
566    is_testmode = _is_testmode(**kwargs)
567    test_packages = {}
568    for cmd in cmds:
569        _execute_install_command(cmd, is_testmode, errors, test_packages)
570
571    __context__.pop("pkg.list_pkgs", None)
572    new = list_pkgs()
573    if is_testmode:
574        new = copy.deepcopy(new)
575        new.update(test_packages)
576
577    ret = salt.utils.data.compare_dicts(old, new)
578
579    if pkg_type == "file" and reinstall:
580        # For file-based packages, prepare 'to_reinstall' to have a list
581        # of all the package names that may have been reinstalled.
582        # This way, we could include reinstalled packages in 'ret'.
583        for pkgfile in to_install:
584            # Convert from file name to package name.
585            cmd = ["opkg", "info", pkgfile]
586            out = __salt__["cmd.run_all"](
587                cmd, output_loglevel="trace", python_shell=False
588            )
589            if out["retcode"] == 0:
590                # Just need the package name.
591                pkginfo_dict = _process_info_installed_output(out["stdout"], [])
592                if pkginfo_dict:
593                    to_reinstall.append(next(iter(pkginfo_dict)))
594
595    for pkgname in to_reinstall:
596        if pkgname not in ret or pkgname in old:
597            ret.update(
598                {pkgname: {"old": old.get(pkgname, ""), "new": new.get(pkgname, "")}}
599            )
600
601    rs_result = _get_restartcheck_result(errors)
602
603    if errors:
604        raise CommandExecutionError(
605            "Problem encountered installing package(s)",
606            info={"errors": errors, "changes": ret},
607        )
608
609    _process_restartcheck_result(rs_result)
610
611    return ret
612
613
614def _parse_reported_packages_from_remove_output(output):
615    """
616    Parses the output of "opkg remove" to determine what packages would have been
617    removed by an operation run with the --noaction flag.
618
619    We are looking for lines like
620        Removing <package> (<version>) from <Target>...
621    """
622    reported_pkgs = {}
623    remove_pattern = re.compile(
624        r"Removing\s(?P<package>.*?)\s\((?P<version>.*?)\)\sfrom\s(?P<target>.*?)..."
625    )
626    for line in salt.utils.itertools.split(output, "\n"):
627        match = remove_pattern.match(line)
628        if match:
629            reported_pkgs[match.group("package")] = ""
630
631    return reported_pkgs
632
633
634def remove(name=None, pkgs=None, **kwargs):  # pylint: disable=unused-argument
635    """
636    Remove packages using ``opkg remove``.
637
638    name
639        The name of the package to be deleted.
640
641
642    Multiple Package Options:
643
644    pkgs
645        A list of packages to delete. Must be passed as a python list. The
646        ``name`` parameter will be ignored if this option is passed.
647
648    remove_dependencies
649        Remove package and all dependencies
650
651        .. versionadded:: 2019.2.0
652
653    auto_remove_deps
654        Remove packages that were installed automatically to satisfy dependencies
655
656        .. versionadded:: 2019.2.0
657
658    Returns a dict containing the changes.
659
660    CLI Example:
661
662    .. code-block:: bash
663
664        salt '*' pkg.remove <package name>
665        salt '*' pkg.remove <package1>,<package2>,<package3>
666        salt '*' pkg.remove pkgs='["foo", "bar"]'
667        salt '*' pkg.remove pkgs='["foo", "bar"]' remove_dependencies=True auto_remove_deps=True
668    """
669    try:
670        pkg_params = __salt__["pkg_resource.parse_targets"](name, pkgs)[0]
671    except MinionError as exc:
672        raise CommandExecutionError(exc)
673
674    old = list_pkgs()
675    targets = [x for x in pkg_params if x in old]
676    if not targets:
677        return {}
678    cmd = ["opkg", "remove"]
679    _append_noaction_if_testmode(cmd, **kwargs)
680    if kwargs.get("remove_dependencies", False):
681        cmd.append("--force-removal-of-dependent-packages")
682    if kwargs.get("auto_remove_deps", False):
683        cmd.append("--autoremove")
684    cmd.extend(targets)
685
686    out = __salt__["cmd.run_all"](cmd, output_loglevel="trace", python_shell=False)
687    if out["retcode"] != 0:
688        if out["stderr"]:
689            errors = [out["stderr"]]
690        else:
691            errors = [out["stdout"]]
692    else:
693        errors = []
694
695    __context__.pop("pkg.list_pkgs", None)
696    new = list_pkgs()
697    if _is_testmode(**kwargs):
698        reportedPkgs = _parse_reported_packages_from_remove_output(out["stdout"])
699        new = {k: v for k, v in new.items() if k not in reportedPkgs}
700    ret = salt.utils.data.compare_dicts(old, new)
701
702    rs_result = _get_restartcheck_result(errors)
703
704    if errors:
705        raise CommandExecutionError(
706            "Problem encountered removing package(s)",
707            info={"errors": errors, "changes": ret},
708        )
709
710    _process_restartcheck_result(rs_result)
711
712    return ret
713
714
715def purge(name=None, pkgs=None, **kwargs):  # pylint: disable=unused-argument
716    """
717    Package purges are not supported by opkg, this function is identical to
718    :mod:`pkg.remove <salt.modules.opkg.remove>`.
719
720    name
721        The name of the package to be deleted.
722
723
724    Multiple Package Options:
725
726    pkgs
727        A list of packages to delete. Must be passed as a python list. The
728        ``name`` parameter will be ignored if this option is passed.
729
730
731    Returns a dict containing the changes.
732
733    CLI Example:
734
735    .. code-block:: bash
736
737        salt '*' pkg.purge <package name>
738        salt '*' pkg.purge <package1>,<package2>,<package3>
739        salt '*' pkg.purge pkgs='["foo", "bar"]'
740    """
741    return remove(name=name, pkgs=pkgs)
742
743
744def upgrade(refresh=True, **kwargs):  # pylint: disable=unused-argument
745    """
746    Upgrades all packages via ``opkg upgrade``
747
748    Returns a dictionary containing the changes:
749
750    .. code-block:: python
751
752        {'<package>':  {'old': '<old-version>',
753                        'new': '<new-version>'}}
754
755    CLI Example:
756
757    .. code-block:: bash
758
759        salt '*' pkg.upgrade
760    """
761    ret = {
762        "changes": {},
763        "result": True,
764        "comment": "",
765    }
766
767    errors = []
768
769    if salt.utils.data.is_true(refresh):
770        refresh_db()
771
772    old = list_pkgs()
773
774    cmd = ["opkg", "upgrade"]
775    result = __salt__["cmd.run_all"](cmd, output_loglevel="trace", python_shell=False)
776    __context__.pop("pkg.list_pkgs", None)
777    new = list_pkgs()
778    ret = salt.utils.data.compare_dicts(old, new)
779
780    if result["retcode"] != 0:
781        errors.append(result)
782
783    rs_result = _get_restartcheck_result(errors)
784
785    if errors:
786        raise CommandExecutionError(
787            "Problem encountered upgrading packages",
788            info={"errors": errors, "changes": ret},
789        )
790
791    _process_restartcheck_result(rs_result)
792
793    return ret
794
795
796def hold(name=None, pkgs=None, sources=None, **kwargs):  # pylint: disable=W0613
797    """
798    Set package in 'hold' state, meaning it will not be upgraded.
799
800    name
801        The name of the package, e.g., 'tmux'
802
803        CLI Example:
804
805        .. code-block:: bash
806
807            salt '*' pkg.hold <package name>
808
809    pkgs
810        A list of packages to hold. Must be passed as a python list.
811
812        CLI Example:
813
814        .. code-block:: bash
815
816            salt '*' pkg.hold pkgs='["foo", "bar"]'
817    """
818    if not name and not pkgs and not sources:
819        raise SaltInvocationError("One of name, pkgs, or sources must be specified.")
820    if pkgs and sources:
821        raise SaltInvocationError("Only one of pkgs or sources can be specified.")
822
823    targets = []
824    if pkgs:
825        targets.extend(pkgs)
826    elif sources:
827        for source in sources:
828            targets.append(next(iter(source)))
829    else:
830        targets.append(name)
831
832    ret = {}
833    for target in targets:
834        if isinstance(target, dict):
835            target = next(iter(target))
836
837        ret[target] = {"name": target, "changes": {}, "result": False, "comment": ""}
838
839        state = _get_state(target)
840        if not state:
841            ret[target]["comment"] = "Package {} not currently held.".format(target)
842        elif state != "hold":
843            if "test" in __opts__ and __opts__["test"]:
844                ret[target].update(result=None)
845                ret[target]["comment"] = "Package {} is set to be held.".format(target)
846            else:
847                result = _set_state(target, "hold")
848                ret[target].update(changes=result[target], result=True)
849                ret[target]["comment"] = "Package {} is now being held.".format(target)
850        else:
851            ret[target].update(result=True)
852            ret[target]["comment"] = "Package {} is already set to be held.".format(
853                target
854            )
855    return ret
856
857
858def unhold(name=None, pkgs=None, sources=None, **kwargs):  # pylint: disable=W0613
859    """
860    Set package current in 'hold' state to install state,
861    meaning it will be upgraded.
862
863    name
864        The name of the package, e.g., 'tmux'
865
866        CLI Example:
867
868        .. code-block:: bash
869
870            salt '*' pkg.unhold <package name>
871
872    pkgs
873        A list of packages to hold. Must be passed as a python list.
874
875        CLI Example:
876
877        .. code-block:: bash
878
879            salt '*' pkg.unhold pkgs='["foo", "bar"]'
880    """
881    if not name and not pkgs and not sources:
882        raise SaltInvocationError("One of name, pkgs, or sources must be specified.")
883    if pkgs and sources:
884        raise SaltInvocationError("Only one of pkgs or sources can be specified.")
885
886    targets = []
887    if pkgs:
888        targets.extend(pkgs)
889    elif sources:
890        for source in sources:
891            targets.append(next(iter(source)))
892    else:
893        targets.append(name)
894
895    ret = {}
896    for target in targets:
897        if isinstance(target, dict):
898            target = next(iter(target))
899
900        ret[target] = {"name": target, "changes": {}, "result": False, "comment": ""}
901
902        state = _get_state(target)
903        if not state:
904            ret[target]["comment"] = "Package {} does not have a state.".format(target)
905        elif state == "hold":
906            if "test" in __opts__ and __opts__["test"]:
907                ret[target].update(result=None)
908                ret["comment"] = "Package {} is set not to be held.".format(target)
909            else:
910                result = _set_state(target, "ok")
911                ret[target].update(changes=result[target], result=True)
912                ret[target]["comment"] = "Package {} is no longer being held.".format(
913                    target
914                )
915        else:
916            ret[target].update(result=True)
917            ret[target]["comment"] = "Package {} is already set not to be held.".format(
918                target
919            )
920    return ret
921
922
923def _get_state(pkg):
924    """
925    View package state from the opkg database
926
927    Return the state of pkg
928    """
929    cmd = ["opkg", "status"]
930    cmd.append(pkg)
931    out = __salt__["cmd.run"](cmd, python_shell=False)
932    state_flag = ""
933    for line in salt.utils.itertools.split(out, "\n"):
934        if line.startswith("Status"):
935            _status, _state_want, state_flag, _state_status = line.split()
936
937    return state_flag
938
939
940def _set_state(pkg, state):
941    """
942    Change package state on the opkg database
943
944    The state can be any of:
945
946     - hold
947     - noprune
948     - user
949     - ok
950     - installed
951     - unpacked
952
953    This command is commonly used to mark a specific package to be held from
954    being upgraded, that is, to be kept at a certain version.
955
956    Returns a dict containing the package name, and the new and old
957    versions.
958    """
959    ret = {}
960    valid_states = ("hold", "noprune", "user", "ok", "installed", "unpacked")
961    if state not in valid_states:
962        raise SaltInvocationError("Invalid state: {}".format(state))
963    oldstate = _get_state(pkg)
964    cmd = ["opkg", "flag"]
965    cmd.append(state)
966    cmd.append(pkg)
967    _out = __salt__["cmd.run"](cmd, python_shell=False)
968
969    # Missing return value check due to opkg issue 160
970    ret[pkg] = {"old": oldstate, "new": state}
971    return ret
972
973
974def _list_pkgs_from_context(versions_as_list):
975    """
976    Use pkg list from __context__
977    """
978    if versions_as_list:
979        return __context__["pkg.list_pkgs"]
980    else:
981        ret = copy.deepcopy(__context__["pkg.list_pkgs"])
982        __salt__["pkg_resource.stringify"](ret)
983        return ret
984
985
986def list_pkgs(versions_as_list=False, **kwargs):
987    """
988    List the packages currently installed in a dict::
989
990        {'<package_name>': '<version>'}
991
992    CLI Example:
993
994    .. code-block:: bash
995
996        salt '*' pkg.list_pkgs
997        salt '*' pkg.list_pkgs versions_as_list=True
998    """
999    versions_as_list = salt.utils.data.is_true(versions_as_list)
1000    # not yet implemented or not applicable
1001    if any(
1002        [salt.utils.data.is_true(kwargs.get(x)) for x in ("removed", "purge_desired")]
1003    ):
1004        return {}
1005
1006    if "pkg.list_pkgs" in __context__:
1007        return _list_pkgs_from_context(versions_as_list)
1008
1009    cmd = ["opkg", "list-installed"]
1010    ret = {}
1011    out = __salt__["cmd.run"](cmd, output_loglevel="trace", python_shell=False)
1012    for line in salt.utils.itertools.split(out, "\n"):
1013        # This is a continuation of package description
1014        if not line or line[0] == " ":
1015            continue
1016
1017        # This contains package name, version, and description.
1018        # Extract the first two.
1019        pkg_name, pkg_version = line.split(" - ", 2)[:2]
1020        __salt__["pkg_resource.add_pkg"](ret, pkg_name, pkg_version)
1021
1022    __salt__["pkg_resource.sort_pkglist"](ret)
1023    __context__["pkg.list_pkgs"] = copy.deepcopy(ret)
1024    if not versions_as_list:
1025        __salt__["pkg_resource.stringify"](ret)
1026    return ret
1027
1028
1029def list_upgrades(refresh=True, **kwargs):  # pylint: disable=unused-argument
1030    """
1031    List all available package upgrades.
1032
1033    CLI Example:
1034
1035    .. code-block:: bash
1036
1037        salt '*' pkg.list_upgrades
1038    """
1039    ret = {}
1040    if salt.utils.data.is_true(refresh):
1041        refresh_db()
1042
1043    cmd = ["opkg", "list-upgradable"]
1044    call = __salt__["cmd.run_all"](cmd, output_loglevel="trace", python_shell=False)
1045
1046    if call["retcode"] != 0:
1047        comment = ""
1048        if "stderr" in call:
1049            comment += call["stderr"]
1050        if "stdout" in call:
1051            comment += call["stdout"]
1052        raise CommandExecutionError(comment)
1053    else:
1054        out = call["stdout"]
1055
1056    for line in out.splitlines():
1057        name, _oldversion, newversion = line.split(" - ")
1058        ret[name] = newversion
1059
1060    return ret
1061
1062
1063def _convert_to_standard_attr(attr):
1064    """
1065    Helper function for _process_info_installed_output()
1066
1067    Converts an opkg attribute name to a standard attribute
1068    name which is used across 'pkg' modules.
1069    """
1070    ret_attr = ATTR_MAP.get(attr, None)
1071    if ret_attr is None:
1072        # All others convert to lowercase
1073        return attr.lower()
1074    return ret_attr
1075
1076
1077def _process_info_installed_output(out, filter_attrs):
1078    """
1079    Helper function for info_installed()
1080
1081    Processes stdout output from a single invocation of
1082    'opkg status'.
1083    """
1084    ret = {}
1085    name = None
1086    attrs = {}
1087    attr = None
1088
1089    for line in salt.utils.itertools.split(out, "\n"):
1090        if line and line[0] == " ":
1091            # This is a continuation of the last attr
1092            if filter_attrs is None or attr in filter_attrs:
1093                line = line.strip()
1094                if attrs[attr]:
1095                    # If attr is empty, don't add leading newline
1096                    attrs[attr] += "\n"
1097                attrs[attr] += line
1098            continue
1099        line = line.strip()
1100        if not line:
1101            # Separator between different packages
1102            if name:
1103                ret[name] = attrs
1104            name = None
1105            attrs = {}
1106            attr = None
1107            continue
1108        key, value = line.split(":", 1)
1109        value = value.lstrip()
1110        attr = _convert_to_standard_attr(key)
1111        if attr == "name":
1112            name = value
1113        elif filter_attrs is None or attr in filter_attrs:
1114            attrs[attr] = value
1115
1116    if name:
1117        ret[name] = attrs
1118    return ret
1119
1120
1121def info_installed(*names, **kwargs):
1122    """
1123    Return the information of the named package(s), installed on the system.
1124
1125    .. versionadded:: 2017.7.0
1126
1127    :param names:
1128        Names of the packages to get information about. If none are specified,
1129        will return information for all installed packages.
1130
1131    :param attr:
1132        Comma-separated package attributes. If no 'attr' is specified, all available attributes returned.
1133
1134        Valid attributes are:
1135            arch, conffiles, conflicts, depends, description, filename, group,
1136            install_date_time_t, md5sum, packager, provides, recommends,
1137            replaces, size, source, suggests, url, version
1138
1139    CLI Example:
1140
1141    .. code-block:: bash
1142
1143        salt '*' pkg.info_installed
1144        salt '*' pkg.info_installed attr=version,packager
1145        salt '*' pkg.info_installed <package1>
1146        salt '*' pkg.info_installed <package1> <package2> <package3> ...
1147        salt '*' pkg.info_installed <package1> attr=version,packager
1148        salt '*' pkg.info_installed <package1> <package2> <package3> ... attr=version,packager
1149    """
1150    attr = kwargs.pop("attr", None)
1151    if attr is None:
1152        filter_attrs = None
1153    elif isinstance(attr, str):
1154        filter_attrs = set(attr.split(","))
1155    else:
1156        filter_attrs = set(attr)
1157
1158    ret = {}
1159    if names:
1160        # Specific list of names of installed packages
1161        for name in names:
1162            cmd = ["opkg", "status", name]
1163            call = __salt__["cmd.run_all"](
1164                cmd, output_loglevel="trace", python_shell=False
1165            )
1166            if call["retcode"] != 0:
1167                comment = ""
1168                if call["stderr"]:
1169                    comment += call["stderr"]
1170                else:
1171                    comment += call["stdout"]
1172
1173                raise CommandExecutionError(comment)
1174            ret.update(_process_info_installed_output(call["stdout"], filter_attrs))
1175    else:
1176        # All installed packages
1177        cmd = ["opkg", "status"]
1178        call = __salt__["cmd.run_all"](cmd, output_loglevel="trace", python_shell=False)
1179        if call["retcode"] != 0:
1180            comment = ""
1181            if call["stderr"]:
1182                comment += call["stderr"]
1183            else:
1184                comment += call["stdout"]
1185
1186            raise CommandExecutionError(comment)
1187        ret.update(_process_info_installed_output(call["stdout"], filter_attrs))
1188
1189    return ret
1190
1191
1192def upgrade_available(name, **kwargs):  # pylint: disable=unused-argument
1193    """
1194    Check whether or not an upgrade is available for a given package
1195
1196    CLI Example:
1197
1198    .. code-block:: bash
1199
1200        salt '*' pkg.upgrade_available <package name>
1201    """
1202    return latest_version(name) != ""
1203
1204
1205def version_cmp(
1206    pkg1, pkg2, ignore_epoch=False, **kwargs
1207):  # pylint: disable=unused-argument
1208    """
1209    Do a cmp-style comparison on two packages. Return -1 if pkg1 < pkg2, 0 if
1210    pkg1 == pkg2, and 1 if pkg1 > pkg2. Return None if there was a problem
1211    making the comparison.
1212
1213    ignore_epoch : False
1214        Set to ``True`` to ignore the epoch when comparing versions
1215
1216        .. versionadded:: 2016.3.4
1217
1218    CLI Example:
1219
1220    .. code-block:: bash
1221
1222        salt '*' pkg.version_cmp '0.2.4-0' '0.2.4.1-0'
1223    """
1224    normalize = lambda x: str(x).split(":", 1)[-1] if ignore_epoch else str(x)
1225    pkg1 = normalize(pkg1)
1226    pkg2 = normalize(pkg2)
1227
1228    output = __salt__["cmd.run_stdout"](
1229        ["opkg", "--version"], output_loglevel="trace", python_shell=False
1230    )
1231    opkg_version = output.split(" ")[2].strip()
1232    if salt.utils.versions.LooseVersion(
1233        opkg_version
1234    ) >= salt.utils.versions.LooseVersion("0.3.4"):
1235        cmd_compare = ["opkg", "compare-versions"]
1236    elif salt.utils.path.which("opkg-compare-versions"):
1237        cmd_compare = ["opkg-compare-versions"]
1238    else:
1239        log.warning(
1240            "Unable to find a compare-versions utility installed. Either upgrade opkg"
1241            " to version > 0.3.4 (preferred) or install the older opkg-compare-versions"
1242            " script."
1243        )
1244        return None
1245
1246    for oper, ret in (("<<", -1), ("=", 0), (">>", 1)):
1247        cmd = cmd_compare[:]
1248        cmd.append(shlex.quote(pkg1))
1249        cmd.append(oper)
1250        cmd.append(shlex.quote(pkg2))
1251        retcode = __salt__["cmd.retcode"](
1252            cmd, output_loglevel="trace", ignore_retcode=True, python_shell=False
1253        )
1254        if retcode == 0:
1255            return ret
1256    return None
1257
1258
1259def _set_repo_option(repo, option):
1260    """
1261    Set the option to repo
1262    """
1263    if not option:
1264        return
1265    opt = option.split("=")
1266    if len(opt) != 2:
1267        return
1268    if opt[0] == "trusted":
1269        repo["trusted"] = opt[1] == "yes"
1270    else:
1271        repo[opt[0]] = opt[1]
1272
1273
1274def _set_repo_options(repo, options):
1275    """
1276    Set the options to the repo.
1277    """
1278    delimiters = "[", "]"
1279    pattern = "|".join(map(re.escape, delimiters))
1280    for option in options:
1281        splitted = re.split(pattern, option)
1282        for opt in splitted:
1283            _set_repo_option(repo, opt)
1284
1285
1286def _create_repo(line, filename):
1287    """
1288    Create repo
1289    """
1290    repo = {}
1291    if line.startswith("#"):
1292        repo["enabled"] = False
1293        line = line[1:]
1294    else:
1295        repo["enabled"] = True
1296    cols = salt.utils.args.shlex_split(line.strip())
1297    repo["compressed"] = not cols[0] in "src"
1298    repo["name"] = cols[1]
1299    repo["uri"] = cols[2]
1300    repo["file"] = os.path.join(OPKG_CONFDIR, filename)
1301    if len(cols) > 3:
1302        _set_repo_options(repo, cols[3:])
1303    return repo
1304
1305
1306def _read_repos(conf_file, repos, filename, regex):
1307    """
1308    Read repos from configuration file
1309    """
1310    for line in conf_file:
1311        line = salt.utils.stringutils.to_unicode(line)
1312        if not regex.search(line):
1313            continue
1314        repo = _create_repo(line, filename)
1315
1316        # do not store duplicated uri's
1317        if repo["uri"] not in repos:
1318            repos[repo["uri"]] = [repo]
1319
1320
1321def list_repos(**kwargs):  # pylint: disable=unused-argument
1322    """
1323    Lists all repos on ``/etc/opkg/*.conf``
1324
1325    CLI Example:
1326
1327    .. code-block:: bash
1328
1329       salt '*' pkg.list_repos
1330    """
1331    repos = {}
1332    regex = re.compile(REPO_REGEXP)
1333    for filename in os.listdir(OPKG_CONFDIR):
1334        if not filename.endswith(".conf"):
1335            continue
1336        with salt.utils.files.fopen(os.path.join(OPKG_CONFDIR, filename)) as conf_file:
1337            _read_repos(conf_file, repos, filename, regex)
1338    return repos
1339
1340
1341def get_repo(repo, **kwargs):  # pylint: disable=unused-argument
1342    """
1343    Display a repo from the ``/etc/opkg/*.conf``
1344
1345    CLI Examples:
1346
1347    .. code-block:: bash
1348
1349        salt '*' pkg.get_repo repo
1350    """
1351    repos = list_repos()
1352
1353    if repos:
1354        for source in repos.values():
1355            for sub in source:
1356                if sub["name"] == repo:
1357                    return sub
1358    return {}
1359
1360
1361def _del_repo_from_file(repo, filepath):
1362    """
1363    Remove a repo from filepath
1364    """
1365    with salt.utils.files.fopen(filepath) as fhandle:
1366        output = []
1367        regex = re.compile(REPO_REGEXP)
1368        for line in fhandle:
1369            line = salt.utils.stringutils.to_unicode(line)
1370            if regex.search(line):
1371                if line.startswith("#"):
1372                    line = line[1:]
1373                cols = salt.utils.args.shlex_split(line.strip())
1374                if repo != cols[1]:
1375                    output.append(salt.utils.stringutils.to_str(line))
1376    with salt.utils.files.fopen(filepath, "w") as fhandle:
1377        fhandle.writelines(output)
1378
1379
1380def _set_trusted_option_if_needed(repostr, trusted):
1381    """
1382    Set trusted option to repo if needed
1383    """
1384    if trusted is True:
1385        repostr += " [trusted=yes]"
1386    elif trusted is False:
1387        repostr += " [trusted=no]"
1388    return repostr
1389
1390
1391def _add_new_repo(repo, properties):
1392    """
1393    Add a new repo entry
1394    """
1395    repostr = "# " if not properties.get("enabled") else ""
1396    repostr += "src/gz " if properties.get("compressed") else "src "
1397    if " " in repo:
1398        repostr += '"' + repo + '" '
1399    else:
1400        repostr += repo + " "
1401    repostr += properties.get("uri")
1402    repostr = _set_trusted_option_if_needed(repostr, properties.get("trusted"))
1403    repostr += "\n"
1404    conffile = os.path.join(OPKG_CONFDIR, repo + ".conf")
1405
1406    with salt.utils.files.fopen(conffile, "a") as fhandle:
1407        fhandle.write(salt.utils.stringutils.to_str(repostr))
1408
1409
1410def _mod_repo_in_file(repo, repostr, filepath):
1411    """
1412    Replace a repo entry in filepath with repostr
1413    """
1414    with salt.utils.files.fopen(filepath) as fhandle:
1415        output = []
1416        for line in fhandle:
1417            cols = salt.utils.args.shlex_split(
1418                salt.utils.stringutils.to_unicode(line).strip()
1419            )
1420            if repo not in cols:
1421                output.append(line)
1422            else:
1423                output.append(salt.utils.stringutils.to_str(repostr + "\n"))
1424    with salt.utils.files.fopen(filepath, "w") as fhandle:
1425        fhandle.writelines(output)
1426
1427
1428def del_repo(repo, **kwargs):  # pylint: disable=unused-argument
1429    """
1430    Delete a repo from ``/etc/opkg/*.conf``
1431
1432    If the file does not contain any other repo configuration, the file itself
1433    will be deleted.
1434
1435    CLI Examples:
1436
1437    .. code-block:: bash
1438
1439        salt '*' pkg.del_repo repo
1440    """
1441    refresh = salt.utils.data.is_true(kwargs.get("refresh", True))
1442    repos = list_repos()
1443    if repos:
1444        deleted_from = dict()
1445        for repository in repos:
1446            source = repos[repository][0]
1447            if source["name"] == repo:
1448                deleted_from[source["file"]] = 0
1449                _del_repo_from_file(repo, source["file"])
1450
1451        if deleted_from:
1452            ret = ""
1453            for repository in repos:
1454                source = repos[repository][0]
1455                if source["file"] in deleted_from:
1456                    deleted_from[source["file"]] += 1
1457            for repo_file, count in deleted_from.items():
1458                msg = "Repo '{}' has been removed from {}.\n"
1459                if count == 1 and os.path.isfile(repo_file):
1460                    msg = "File {1} containing repo '{0}' has been removed.\n"
1461                    try:
1462                        os.remove(repo_file)
1463                    except OSError:
1464                        pass
1465                ret += msg.format(repo, repo_file)
1466            if refresh:
1467                refresh_db()
1468            return ret
1469
1470    return "Repo {} doesn't exist in the opkg repo lists".format(repo)
1471
1472
1473def mod_repo(repo, **kwargs):
1474    """
1475    Modify one or more values for a repo.  If the repo does not exist, it will
1476    be created, so long as uri is defined.
1477
1478    The following options are available to modify a repo definition:
1479
1480    repo
1481        alias by which opkg refers to the repo.
1482    uri
1483        the URI to the repo.
1484    compressed
1485        defines (True or False) if the index file is compressed
1486    enabled
1487        enable or disable (True or False) repository
1488        but do not remove if disabled.
1489    refresh
1490        enable or disable (True or False) auto-refresh of the repositories
1491
1492    CLI Examples:
1493
1494    .. code-block:: bash
1495
1496        salt '*' pkg.mod_repo repo uri=http://new/uri
1497        salt '*' pkg.mod_repo repo enabled=False
1498    """
1499    repos = list_repos()
1500    found = False
1501    uri = ""
1502    if "uri" in kwargs:
1503        uri = kwargs["uri"]
1504
1505    for repository in repos:
1506        source = repos[repository][0]
1507        if source["name"] == repo:
1508            found = True
1509            repostr = ""
1510            if "enabled" in kwargs and not kwargs["enabled"]:
1511                repostr += "# "
1512            if "compressed" in kwargs:
1513                repostr += "src/gz " if kwargs["compressed"] else "src"
1514            else:
1515                repostr += "src/gz" if source["compressed"] else "src"
1516            repo_alias = kwargs["alias"] if "alias" in kwargs else repo
1517            if " " in repo_alias:
1518                repostr += ' "{}"'.format(repo_alias)
1519            else:
1520                repostr += " {}".format(repo_alias)
1521            repostr += " {}".format(kwargs["uri"] if "uri" in kwargs else source["uri"])
1522            trusted = kwargs.get("trusted")
1523            repostr = (
1524                _set_trusted_option_if_needed(repostr, trusted)
1525                if trusted is not None
1526                else _set_trusted_option_if_needed(repostr, source.get("trusted"))
1527            )
1528            _mod_repo_in_file(repo, repostr, source["file"])
1529        elif uri and source["uri"] == uri:
1530            raise CommandExecutionError(
1531                "Repository '{}' already exists as '{}'.".format(uri, source["name"])
1532            )
1533
1534    if not found:
1535        # Need to add a new repo
1536        if "uri" not in kwargs:
1537            raise CommandExecutionError(
1538                "Repository '{}' not found and no URI passed to create one.".format(
1539                    repo
1540                )
1541            )
1542        properties = {"uri": kwargs["uri"]}
1543        # If compressed is not defined, assume True
1544        properties["compressed"] = (
1545            kwargs["compressed"] if "compressed" in kwargs else True
1546        )
1547        # If enabled is not defined, assume True
1548        properties["enabled"] = kwargs["enabled"] if "enabled" in kwargs else True
1549        properties["trusted"] = kwargs.get("trusted")
1550        _add_new_repo(repo, properties)
1551
1552    if "refresh" in kwargs:
1553        refresh_db()
1554
1555
1556def file_list(*packages, **kwargs):  # pylint: disable=unused-argument
1557    """
1558    List the files that belong to a package. Not specifying any packages will
1559    return a list of _every_ file on the system's package database (not
1560    generally recommended).
1561
1562    CLI Examples:
1563
1564    .. code-block:: bash
1565
1566        salt '*' pkg.file_list httpd
1567        salt '*' pkg.file_list httpd postfix
1568        salt '*' pkg.file_list
1569    """
1570    output = file_dict(*packages)
1571    files = []
1572    for package in list(output["packages"].values()):
1573        files.extend(package)
1574    return {"errors": output["errors"], "files": files}
1575
1576
1577def file_dict(*packages, **kwargs):  # pylint: disable=unused-argument
1578    """
1579    List the files that belong to a package, grouped by package. Not
1580    specifying any packages will return a list of _every_ file on the system's
1581    package database (not generally recommended).
1582
1583    CLI Examples:
1584
1585    .. code-block:: bash
1586
1587        salt '*' pkg.file_list httpd
1588        salt '*' pkg.file_list httpd postfix
1589        salt '*' pkg.file_list
1590    """
1591    errors = []
1592    ret = {}
1593    cmd_files = ["opkg", "files"]
1594
1595    if not packages:
1596        packages = list(list_pkgs().keys())
1597
1598    for package in packages:
1599        files = []
1600        cmd = cmd_files[:]
1601        cmd.append(package)
1602        out = __salt__["cmd.run_all"](cmd, output_loglevel="trace", python_shell=False)
1603        for line in out["stdout"].splitlines():
1604            if line.startswith("/"):
1605                files.append(line)
1606            elif line.startswith(" * "):
1607                errors.append(line[3:])
1608                break
1609            else:
1610                continue
1611        if files:
1612            ret[package] = files
1613
1614    return {"errors": errors, "packages": ret}
1615
1616
1617def owner(*paths, **kwargs):  # pylint: disable=unused-argument
1618    """
1619    Return the name of the package that owns the file. Multiple file paths can
1620    be passed. Like :mod:`pkg.version <salt.modules.opkg.version`, if a single
1621    path is passed, a string will be returned, and if multiple paths are passed,
1622    a dictionary of file/package name pairs will be returned.
1623
1624    If the file is not owned by a package, or is not present on the minion,
1625    then an empty string will be returned for that path.
1626
1627    CLI Example:
1628
1629    .. code-block:: bash
1630
1631        salt '*' pkg.owner /usr/bin/apachectl
1632        salt '*' pkg.owner /usr/bin/apachectl /usr/bin/basename
1633    """
1634    if not paths:
1635        return ""
1636    ret = {}
1637    cmd_search = ["opkg", "search"]
1638    for path in paths:
1639        cmd = cmd_search[:]
1640        cmd.append(path)
1641        output = __salt__["cmd.run_stdout"](
1642            cmd, output_loglevel="trace", python_shell=False
1643        )
1644        if output:
1645            ret[path] = output.split(" - ")[0].strip()
1646        else:
1647            ret[path] = ""
1648    if len(ret) == 1:
1649        return next(iter(ret.values()))
1650    return ret
1651
1652
1653def version_clean(version):
1654    """
1655    Clean the version string removing extra data.
1656    There's nothing do to here for nipkg.py, therefore it will always
1657    return the given version.
1658    """
1659    return version
1660
1661
1662def check_extra_requirements(pkgname, pkgver):
1663    """
1664    Check if the installed package already has the given requirements.
1665    There's nothing do to here for nipkg.py, therefore it will always
1666    return True.
1667    """
1668    return True
1669