1# Copyright (C) 2017-2018  Red Hat, Inc.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU Library General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16
17from collections import OrderedDict
18
19import hawkey
20import libdnf.smartcols
21import libdnf.module
22import dnf.selector
23import dnf.exceptions
24
25from dnf.module.exceptions import EnableMultipleStreamsException
26from dnf.util import logger
27from dnf.i18n import _, P_, ucd
28
29import functools
30
31STATE_DEFAULT = libdnf.module.ModulePackageContainer.ModuleState_DEFAULT
32STATE_ENABLED = libdnf.module.ModulePackageContainer.ModuleState_ENABLED
33STATE_DISABLED = libdnf.module.ModulePackageContainer.ModuleState_DISABLED
34STATE_UNKNOWN = libdnf.module.ModulePackageContainer.ModuleState_UNKNOWN
35MODULE_TABLE_HINT = _("\n\nHint: [d]efault, [e]nabled, [x]disabled, [i]nstalled")
36MODULE_INFO_TABLE_HINT = _("\n\nHint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive")
37
38
39def _profile_comparison_key(profile):
40    return profile.getName()
41
42
43class ModuleBase(object):
44    # :api
45
46    def __init__(self, base):
47        # :api
48        self.base = base
49
50    def enable(self, module_specs):
51        # :api
52        no_match_specs, error_specs, solver_errors, module_dicts = \
53            self._resolve_specs_enable_update_sack(module_specs)
54        for spec, (nsvcap, module_dict) in module_dicts.items():
55            if nsvcap.profile:
56                logger.info(_("Ignoring unnecessary profile: '{}/{}'").format(
57                    nsvcap.name, nsvcap.profile))
58        if no_match_specs or error_specs or solver_errors:
59            raise dnf.exceptions.MarkingErrors(no_match_group_specs=no_match_specs,
60                                               error_group_specs=error_specs,
61                                               module_depsolv_errors=solver_errors)
62
63    def disable(self, module_specs):
64        # :api
65        no_match_specs, solver_errors = self._modules_reset_or_disable(module_specs, STATE_DISABLED)
66        if no_match_specs or solver_errors:
67            raise dnf.exceptions.MarkingErrors(no_match_group_specs=no_match_specs,
68                                               module_depsolv_errors=solver_errors)
69
70    def install(self, module_specs, strict=True):
71        # :api
72        no_match_specs, error_specs, solver_errors, module_dicts = \
73            self._resolve_specs_enable_update_sack(module_specs)
74
75        # <package_name, set_of_spec>
76        fail_safe_repo = hawkey.MODULE_FAIL_SAFE_REPO_NAME
77        install_dict = {}
78        install_set_artifacts = set()
79        fail_safe_repo_used = False
80        for spec, (nsvcap, moduledict) in module_dicts.items():
81            for name, streamdict in moduledict.items():
82                for stream, module_list in streamdict.items():
83                    install_module_list = [x for x in module_list
84                                           if self.base._moduleContainer.isModuleActive(x.getId())]
85                    if not install_module_list:
86                        logger.error(_("All matches for argument '{0}' in module '{1}:{2}' are not "
87                                       "active").format(spec, name, stream))
88                        error_specs.append(spec)
89                        continue
90                    profiles = []
91                    latest_module = self._get_latest(install_module_list)
92                    if latest_module.getRepoID() == fail_safe_repo:
93                        msg = _(
94                            "Installing module '{0}' from Fail-Safe repository {1} is not allowed")
95                        logger.critical(msg.format(latest_module.getNameStream(), fail_safe_repo))
96                        fail_safe_repo_used = True
97                    if nsvcap.profile:
98                        profiles.extend(latest_module.getProfiles(nsvcap.profile))
99                        if not profiles:
100                            available_profiles = latest_module.getProfiles()
101                            if available_profiles:
102                                profile_names = ", ".join(sorted(
103                                    [profile.getName() for profile in available_profiles]))
104                                msg = _("Unable to match profile for argument {}. Available "
105                                        "profiles for '{}:{}': {}").format(
106                                    spec, name, stream, profile_names)
107                            else:
108                                msg = _("Unable to match profile for argument {}").format(spec)
109                            logger.error(msg)
110                            no_match_specs.append(spec)
111                            continue
112                    else:
113                        profiles_strings = self.base._moduleContainer.getDefaultProfiles(
114                            name, stream)
115                        if not profiles_strings:
116                            available_profiles = latest_module.getProfiles()
117                            if available_profiles:
118                                profile_names = ", ".join(sorted(
119                                    [profile.getName() for profile in available_profiles]))
120                                msg = _("No default profiles for module {}:{}. Available profiles"
121                                        ": {}").format(
122                                    name, stream, profile_names)
123                            else:
124                                msg = _("No profiles for module {}:{}").format(name, stream)
125                            logger.error(msg)
126                            error_specs.append(spec)
127                        for profile in set(profiles_strings):
128                            module_profiles = latest_module.getProfiles(profile)
129                            if not module_profiles:
130                                logger.error(
131                                    _("Default profile {} not available in module {}:{}").format(
132                                        profile, name, stream))
133                                error_specs.append(spec)
134
135                            profiles.extend(module_profiles)
136                    for profile in profiles:
137                        self.base._moduleContainer.install(latest_module ,profile.getName())
138                        for pkg_name in profile.getContent():
139                            install_dict.setdefault(pkg_name, set()).add(spec)
140                    for module in install_module_list:
141                        install_set_artifacts.update(module.getArtifacts())
142        if fail_safe_repo_used:
143            raise dnf.exceptions.Error(_(
144                "Installing module from Fail-Safe repository is not allowed"))
145        __, profiles_errors = self._install_profiles_internal(
146            install_set_artifacts, install_dict, strict)
147        if profiles_errors:
148            error_specs.extend(profiles_errors)
149
150        if no_match_specs or error_specs or solver_errors:
151            raise dnf.exceptions.MarkingErrors(no_match_group_specs=no_match_specs,
152                                               error_group_specs=error_specs,
153                                               module_depsolv_errors=solver_errors)
154
155    def switch_to(self, module_specs, strict=True):
156        # :api
157        no_match_specs, error_specs, module_dicts = self._resolve_specs_enable(module_specs)
158        # collect name of artifacts from new modules for distrosync
159        new_artifacts_names = set()
160        # collect name of artifacts from active modules for distrosync before sack update
161        active_artifacts_names = set()
162        src_arches = {"nosrc", "src"}
163        for spec, (nsvcap, moduledict) in module_dicts.items():
164            for name in moduledict.keys():
165                for module in self.base._moduleContainer.query(name, "", "", "", ""):
166                    if self.base._moduleContainer.isModuleActive(module):
167                        for artifact in module.getArtifacts():
168                            arch = artifact.rsplit(".", 1)[1]
169                            if arch in src_arches:
170                                continue
171                            pkg_name = artifact.rsplit("-", 2)[0]
172                            active_artifacts_names.add(pkg_name)
173
174        solver_errors = self._update_sack()
175
176        dependency_error_spec = self._enable_dependencies(module_dicts)
177        if dependency_error_spec:
178            error_specs.extend(dependency_error_spec)
179
180        # <package_name, set_of_spec>
181        fail_safe_repo = hawkey.MODULE_FAIL_SAFE_REPO_NAME
182        install_dict = {}
183        install_set_artifacts = set()
184        fail_safe_repo_used = False
185
186        # list of name: [profiles] for module profiles being removed
187        removed_profiles = self.base._moduleContainer.getRemovedProfiles()
188
189        for spec, (nsvcap, moduledict) in module_dicts.items():
190            for name, streamdict in moduledict.items():
191                for stream, module_list in streamdict.items():
192                    install_module_list = [x for x in module_list
193                                           if self.base._moduleContainer.isModuleActive(x.getId())]
194                    if not install_module_list:
195                        "No active matches for argument '{0}' in module '{1}:{2}'"
196                        logger.error(_("No active matches for argument '{0}' in module "
197                                       "'{1}:{2}'").format(spec, name, stream))
198                        error_specs.append(spec)
199                        continue
200                    profiles = []
201                    latest_module = self._get_latest(install_module_list)
202                    if latest_module.getRepoID() == fail_safe_repo:
203                        msg = _(
204                            "Installing module '{0}' from Fail-Safe repository {1} is not allowed")
205                        logger.critical(msg.format(latest_module.getNameStream(), fail_safe_repo))
206                        fail_safe_repo_used = True
207                    if nsvcap.profile:
208                        profiles.extend(latest_module.getProfiles(nsvcap.profile))
209                        if not profiles:
210                            available_profiles = latest_module.getProfiles()
211                            if available_profiles:
212                                profile_names = ", ".join(sorted(
213                                    [profile.getName() for profile in available_profiles]))
214                                msg = _("Unable to match profile for argument {}. Available "
215                                        "profiles for '{}:{}': {}").format(
216                                    spec, name, stream, profile_names)
217                            else:
218                                msg = _("Unable to match profile for argument {}").format(spec)
219                            logger.error(msg)
220                            no_match_specs.append(spec)
221                            continue
222                    elif name in removed_profiles:
223
224                        for profile in removed_profiles[name]:
225                            module_profiles = latest_module.getProfiles(profile)
226                            if not module_profiles:
227                                logger.warning(
228                                    _("Installed profile '{0}' is not available in module "
229                                      "'{1}' stream '{2}'").format(profile, name, stream))
230                                continue
231                            profiles.extend(module_profiles)
232                    for profile in profiles:
233                        self.base._moduleContainer.install(latest_module, profile.getName())
234                        for pkg_name in profile.getContent():
235                            install_dict.setdefault(pkg_name, set()).add(spec)
236                    for module in install_module_list:
237                        artifacts = module.getArtifacts()
238                        install_set_artifacts.update(artifacts)
239                        for artifact in artifacts:
240                            arch = artifact.rsplit(".", 1)[1]
241                            if arch in src_arches:
242                                continue
243                            pkg_name = artifact.rsplit("-", 2)[0]
244                            new_artifacts_names.add(pkg_name)
245        if fail_safe_repo_used:
246            raise dnf.exceptions.Error(_(
247                "Installing module from Fail-Safe repository is not allowed"))
248        install_base_query, profiles_errors = self._install_profiles_internal(
249            install_set_artifacts, install_dict, strict)
250        if profiles_errors:
251            error_specs.extend(profiles_errors)
252
253        # distrosync module name
254        all_names = set()
255        all_names.update(new_artifacts_names)
256        all_names.update(active_artifacts_names)
257        remove_query = self.base.sack.query().filterm(empty=True)
258        base_no_source_query = self.base.sack.query().filterm(arch__neq=['src', 'nosrc']).apply()
259
260        for pkg_name in all_names:
261            query = base_no_source_query.filter(name=pkg_name)
262            installed = query.installed()
263            if not installed:
264                continue
265            available = query.available()
266            if not available:
267                logger.warning(_("No packages available to distrosync for package name "
268                                 "'{}'").format(pkg_name))
269                if pkg_name not in new_artifacts_names:
270                    remove_query = remove_query.union(query)
271                continue
272
273            only_new_module = query.intersection(install_base_query)
274            if only_new_module:
275                query = only_new_module
276            sltr = dnf.selector.Selector(self.base.sack)
277            sltr.set(pkg=query)
278            self.base._goal.distupgrade(select=sltr)
279        self.base._remove_if_unneeded(remove_query)
280
281        if no_match_specs or error_specs or solver_errors:
282            raise dnf.exceptions.MarkingErrors(no_match_group_specs=no_match_specs,
283                                               error_group_specs=error_specs,
284                                               module_depsolv_errors=solver_errors)
285
286    def reset(self, module_specs):
287        # :api
288        no_match_specs, solver_errors = self._modules_reset_or_disable(module_specs, STATE_UNKNOWN)
289        if no_match_specs:
290            raise dnf.exceptions.MarkingErrors(no_match_group_specs=no_match_specs,
291                                               module_depsolv_errors=solver_errors)
292
293    def upgrade(self, module_specs):
294        # :api
295        no_match_specs = []
296        fail_safe_repo = hawkey.MODULE_FAIL_SAFE_REPO_NAME
297        fail_safe_repo_used = False
298
299        #  Remove source packages because they cannot be installed or upgraded
300        base_no_source_query = self.base.sack.query().filterm(arch__neq=['src', 'nosrc']).apply()
301
302        for spec in module_specs:
303            module_list, nsvcap = self._get_modules(spec)
304            if not module_list:
305                no_match_specs.append(spec)
306                continue
307            update_module_list = [x for x in module_list
308                                  if self.base._moduleContainer.isModuleActive(x.getId())]
309            if not update_module_list:
310                logger.error(_("Unable to resolve argument {}").format(spec))
311                continue
312            module_dict = self._create_module_dict_and_enable(update_module_list, spec, False)
313            upgrade_package_set = set()
314            for name, streamdict in module_dict.items():
315                for stream, module_list_from_dict in streamdict.items():
316                    upgrade_package_set.update(self._get_package_name_set_and_remove_profiles(
317                        module_list_from_dict, nsvcap))
318                    latest_module = self._get_latest(module_list_from_dict)
319                    if latest_module.getRepoID() == fail_safe_repo:
320                        msg = _(
321                            "Upgrading module '{0}' from Fail-Safe repository {1} is not allowed")
322                        logger.critical(msg.format(latest_module.getNameStream(), fail_safe_repo))
323                        fail_safe_repo_used = True
324                    if nsvcap.profile:
325                        profiles_set = latest_module.getProfiles(nsvcap.profile)
326                        if not profiles_set:
327                            continue
328                        for profile in profiles_set:
329                            upgrade_package_set.update(profile.getContent())
330                    else:
331                        for profile in latest_module.getProfiles():
332                            upgrade_package_set.update(profile.getContent())
333                        for artifact in latest_module.getArtifacts():
334                            subj = hawkey.Subject(artifact)
335                            for nevra_obj in subj.get_nevra_possibilities(
336                                    forms=[hawkey.FORM_NEVRA]):
337                                upgrade_package_set.add(nevra_obj.name)
338
339            if not upgrade_package_set:
340                logger.error(_("Unable to match profile in argument {}").format(spec))
341            query = base_no_source_query.filter(name=upgrade_package_set)
342            if query:
343                sltr = dnf.selector.Selector(self.base.sack)
344                sltr.set(pkg=query)
345                self.base._goal.upgrade(select=sltr)
346        if fail_safe_repo_used:
347            raise dnf.exceptions.Error(_(
348                "Upgrading module from Fail-Safe repository is not allowed"))
349        return no_match_specs
350
351    def remove(self, module_specs):
352        # :api
353        no_match_specs = []
354        remove_package_set = set()
355
356        for spec in module_specs:
357            module_list, nsvcap = self._get_modules(spec)
358            if not module_list:
359                no_match_specs.append(spec)
360                continue
361            module_dict = self._create_module_dict_and_enable(module_list, spec, False)
362            remove_packages_names = []
363            for name, streamdict in module_dict.items():
364                for stream, module_list_from_dict in streamdict.items():
365                    remove_packages_names.extend(self._get_package_name_set_and_remove_profiles(
366                        module_list_from_dict, nsvcap, True))
367            if not remove_packages_names:
368                logger.error(_("Unable to match profile in argument {}").format(spec))
369            remove_package_set.update(remove_packages_names)
370
371        if remove_package_set:
372            keep_pkg_names = self.base._moduleContainer.getInstalledPkgNames()
373            remove_package_set = remove_package_set.difference(keep_pkg_names)
374            if remove_package_set:
375                query = self.base.sack.query().installed().filterm(name=remove_package_set)
376                if query:
377                    self.base._remove_if_unneeded(query)
378        return no_match_specs
379
380    def get_modules(self, module_spec):
381        # :api
382        return self._get_modules(module_spec)
383
384    def _get_modules(self, module_spec):
385        # used by ansible (lib/ansible/modules/packaging/os/dnf.py)
386        subj = hawkey.Subject(module_spec)
387        for nsvcap in subj.nsvcap_possibilities():
388            name = nsvcap.name if nsvcap.name else ""
389            stream = nsvcap.stream if nsvcap.stream else ""
390            version = ""
391            context = nsvcap.context if nsvcap.context else ""
392            arch = nsvcap.arch if nsvcap.arch else ""
393            if nsvcap.version and nsvcap.version != -1:
394                version = str(nsvcap.version)
395            modules = self.base._moduleContainer.query(name, stream, version, context, arch)
396            if modules:
397                return modules, nsvcap
398        return (), None
399
400    def _get_latest(self, module_list):
401        latest = None
402        if module_list:
403            latest = module_list[0]
404            for module in module_list[1:]:
405                if module.getVersionNum() > latest.getVersionNum():
406                    latest = module
407        return latest
408
409    def _create_module_dict_and_enable(self, module_list, spec, enable=True):
410        moduleDict = {}
411        for module in module_list:
412            moduleDict.setdefault(
413                module.getName(), {}).setdefault(module.getStream(), []).append(module)
414
415        for moduleName, streamDict in moduleDict.items():
416            moduleState = self.base._moduleContainer.getModuleState(moduleName)
417            if len(streamDict) > 1:
418                if moduleState != STATE_DEFAULT and moduleState != STATE_ENABLED \
419                        and moduleState != STATE_DISABLED:
420                    streams_str = "', '".join(
421                        sorted(streamDict.keys(), key=functools.cmp_to_key(self.base.sack.evr_cmp)))
422                    msg = _("Argument '{argument}' matches {stream_count} streams ('{streams}') of "
423                            "module '{module}', but none of the streams are enabled or "
424                            "default").format(
425                        argument=spec, stream_count=len(streamDict), streams=streams_str,
426                        module=moduleName)
427                    raise EnableMultipleStreamsException(moduleName, msg)
428                if moduleState == STATE_ENABLED:
429                    stream = self.base._moduleContainer.getEnabledStream(moduleName)
430                else:
431                    stream = self.base._moduleContainer.getDefaultStream(moduleName)
432                if not stream or stream not in streamDict:
433                    raise EnableMultipleStreamsException(moduleName)
434                for key in sorted(streamDict.keys()):
435                    if key == stream:
436                        if enable:
437                            self.base._moduleContainer.enable(moduleName, key)
438                        continue
439                    del streamDict[key]
440            elif enable:
441                for key in streamDict.keys():
442                    self.base._moduleContainer.enable(moduleName, key)
443            assert len(streamDict) == 1
444        return moduleDict
445
446    def _resolve_specs_enable(self, module_specs):
447        no_match_specs = []
448        error_spec = []
449        module_dicts = {}
450        for spec in module_specs:
451            module_list, nsvcap = self._get_modules(spec)
452            if not module_list:
453                no_match_specs.append(spec)
454                continue
455            try:
456                module_dict = self._create_module_dict_and_enable(module_list, spec, True)
457                module_dicts[spec] = (nsvcap, module_dict)
458            except (RuntimeError, EnableMultipleStreamsException) as e:
459                error_spec.append(spec)
460                logger.error(ucd(e))
461                logger.error(_("Unable to resolve argument {}").format(spec))
462        return no_match_specs, error_spec, module_dicts
463
464    def _update_sack(self):
465        hot_fix_repos = [i.id for i in self.base.repos.iter_enabled() if i.module_hotfixes]
466        try:
467            solver_errors = self.base.sack.filter_modules(
468                self.base._moduleContainer, hot_fix_repos, self.base.conf.installroot,
469                self.base.conf.module_platform_id, update_only=True,
470                debugsolver=self.base.conf.debug_solver)
471        except hawkey.Exception as e:
472            raise dnf.exceptions.Error(ucd(e))
473        return solver_errors
474
475    def _enable_dependencies(self, module_dicts):
476        error_spec = []
477        for spec, (nsvcap, moduleDict) in module_dicts.items():
478            for streamDict in moduleDict.values():
479                for modules in streamDict.values():
480                    try:
481                        self.base._moduleContainer.enableDependencyTree(
482                            libdnf.module.VectorModulePackagePtr(modules))
483                    except RuntimeError as e:
484                        error_spec.append(spec)
485                        logger.error(ucd(e))
486                        logger.error(_("Unable to resolve argument {}").format(spec))
487        return error_spec
488
489    def _resolve_specs_enable_update_sack(self, module_specs):
490        no_match_specs, error_spec, module_dicts = self._resolve_specs_enable(module_specs)
491
492        solver_errors = self._update_sack()
493
494        dependency_error_spec = self._enable_dependencies(module_dicts)
495        if dependency_error_spec:
496            error_spec.extend(dependency_error_spec)
497
498        return no_match_specs, error_spec, solver_errors, module_dicts
499
500    def _modules_reset_or_disable(self, module_specs, to_state):
501        no_match_specs = []
502        for spec in module_specs:
503            module_list, nsvcap = self._get_modules(spec)
504            if not module_list:
505                logger.error(_("Unable to resolve argument {}").format(spec))
506                no_match_specs.append(spec)
507                continue
508            if nsvcap.stream or nsvcap.version or nsvcap.context or nsvcap.arch or nsvcap.profile:
509                logger.info(_("Only module name is required. "
510                              "Ignoring unneeded information in argument: '{}'").format(spec))
511            module_names = set()
512            for module in module_list:
513                module_names.add(module.getName())
514            for name in module_names:
515                if to_state == STATE_UNKNOWN:
516                    self.base._moduleContainer.reset(name)
517                if to_state == STATE_DISABLED:
518                    self.base._moduleContainer.disable(name)
519
520        solver_errors = self._update_sack()
521        return no_match_specs, solver_errors
522
523    def _get_package_name_set_and_remove_profiles(self, module_list, nsvcap, remove=False):
524        package_name_set = set()
525        latest_module = self._get_latest(module_list)
526        installed_profiles_strings = set(self.base._moduleContainer.getInstalledProfiles(
527            latest_module.getName()))
528        if not installed_profiles_strings:
529            return set()
530        if nsvcap.profile:
531            profiles_set = latest_module.getProfiles(nsvcap.profile)
532            if not profiles_set:
533                return set()
534            for profile in profiles_set:
535                if profile.getName() in installed_profiles_strings:
536                    if remove:
537                        self.base._moduleContainer.uninstall(latest_module, profile.getName())
538                    package_name_set.update(profile.getContent())
539        else:
540            for profile_string in installed_profiles_strings:
541                if remove:
542                    self.base._moduleContainer.uninstall(latest_module, profile_string)
543                for profile in latest_module.getProfiles(profile_string):
544                    package_name_set.update(profile.getContent())
545        return package_name_set
546
547    def _get_info_profiles(self, module_specs):
548        output = set()
549        for module_spec in module_specs:
550            module_list, nsvcap = self._get_modules(module_spec)
551            if not module_list:
552                logger.info(_("Unable to resolve argument {}").format(module_spec))
553                continue
554
555            if nsvcap.profile:
556                logger.info(_("Ignoring unnecessary profile: '{}/{}'").format(
557                    nsvcap.name, nsvcap.profile))
558            for module in module_list:
559
560                lines = OrderedDict()
561                lines["Name"] = module.getFullIdentifier()
562
563                for profile in sorted(module.getProfiles(), key=_profile_comparison_key):
564                    lines[profile.getName()] = "\n".join(
565                        [pkgName for pkgName in profile.getContent()])
566
567                output.add(self._create_simple_table(lines).toString())
568        return "\n\n".join(sorted(output))
569
570    def _profile_report_formatter(self, modulePackage, default_profiles, enabled_str):
571        installed_profiles = self.base._moduleContainer.getInstalledProfiles(
572            modulePackage.getName())
573        available_profiles = modulePackage.getProfiles()
574        profiles_str = ""
575        for profile in sorted(available_profiles, key=_profile_comparison_key):
576            profiles_str += "{}{}".format(
577                profile.getName(), " [d]" if profile.getName() in default_profiles else "")
578            profiles_str += " [i], " if profile.getName() in installed_profiles and enabled_str \
579                else ", "
580        return profiles_str[:-2]
581
582    def _summary_report_formatter(self, summary):
583        return summary.strip().replace("\n", " ")
584
585    def _module_strs_formatter(self, modulePackage, markActive=False):
586        default_str = ""
587        enabled_str = ""
588        disabled_str = ""
589        if modulePackage.getStream() == self.base._moduleContainer.getDefaultStream(
590                modulePackage.getName()):
591            default_str = " [d]"
592        if self.base._moduleContainer.isEnabled(modulePackage):
593            if not default_str:
594                enabled_str = " "
595            enabled_str += "[e]"
596        elif self.base._moduleContainer.isDisabled(modulePackage):
597            if not default_str:
598                disabled_str = " "
599            disabled_str += "[x]"
600        if markActive and self.base._moduleContainer.isModuleActive(modulePackage):
601            if not default_str:
602                disabled_str = " "
603            disabled_str += "[a]"
604        return default_str, enabled_str, disabled_str
605
606    def _get_info(self, module_specs):
607        output = set()
608        for module_spec in module_specs:
609            module_list, nsvcap = self._get_modules(module_spec)
610            if not module_list:
611                logger.info(_("Unable to resolve argument {}").format(module_spec))
612                continue
613
614            if nsvcap.profile:
615                logger.info(_("Ignoring unnecessary profile: '{}/{}'").format(
616                    nsvcap.name, nsvcap.profile))
617            for modulePackage in module_list:
618                default_str, enabled_str, disabled_str = self._module_strs_formatter(
619                    modulePackage, markActive=True)
620                default_profiles = self.base._moduleContainer.getDefaultProfiles(
621                    modulePackage.getName(), modulePackage.getStream())
622
623                profiles_str = self._profile_report_formatter(
624                    modulePackage, default_profiles, enabled_str)
625
626                lines = OrderedDict()
627                lines["Name"] = modulePackage.getName()
628                lines["Stream"] = modulePackage.getStream() + default_str + enabled_str + \
629                                  disabled_str
630                lines["Version"] = modulePackage.getVersion()
631                lines["Context"] = modulePackage.getContext()
632                lines["Architecture"] = modulePackage.getArch()
633                lines["Profiles"] = profiles_str
634                lines["Default profiles"] = " ".join(default_profiles)
635                lines["Repo"] = modulePackage.getRepoID()
636                lines["Summary"] = modulePackage.getSummary()
637                lines["Description"] = modulePackage.getDescription()
638                req_set = set()
639                for req in modulePackage.getModuleDependencies():
640                    for require_dict in req.getRequires():
641                        for mod_require, stream in require_dict.items():
642                            req_set.add("{}:[{}]".format(mod_require, ",".join(stream)))
643                lines["Requires"] = "\n".join(sorted(req_set))
644                demodularized = modulePackage.getDemodularizedRpms()
645                if demodularized:
646                    lines["Demodularized rpms"] = "\n".join(demodularized)
647                lines["Artifacts"] = "\n".join(sorted(modulePackage.getArtifacts()))
648                output.add(self._create_simple_table(lines).toString())
649        str_table = "\n\n".join(sorted(output))
650        if str_table:
651            str_table += MODULE_INFO_TABLE_HINT
652        return str_table
653
654    @staticmethod
655    def _create_simple_table(lines):
656        table = libdnf.smartcols.Table()
657        table.enableNoheadings(True)
658        table.setColumnSeparator(" : ")
659
660        column_name = table.newColumn("Name")
661        column_value = table.newColumn("Value")
662        column_value.setWrap(True)
663        column_value.setSafechars("\n")
664        column_value.setNewlineWrapFunction()
665
666        for line_name, value in lines.items():
667            if value is None:
668                value = ""
669            line = table.newLine()
670            line.getColumnCell(column_name).setData(line_name)
671            line.getColumnCell(column_value).setData(str(value))
672
673        return table
674
675    def _get_full_info(self, module_specs):
676        output = set()
677        for module_spec in module_specs:
678            module_list, nsvcap = self._get_modules(module_spec)
679            if not module_list:
680                logger.info(_("Unable to resolve argument {}").format(module_spec))
681                continue
682
683            if nsvcap.profile:
684                logger.info(_("Ignoring unnecessary profile: '{}/{}'").format(
685                    nsvcap.name, nsvcap.profile))
686            for modulePackage in module_list:
687                info = modulePackage.getYaml()
688                if info:
689                    output.add(info)
690        output_string = "\n\n".join(sorted(output))
691        return output_string
692
693    def _what_provides(self, rpm_specs):
694        output = set()
695        modulePackages = self.base._moduleContainer.getModulePackages()
696        baseQuery = self.base.sack.query().filterm(empty=True).apply()
697        getBestInitQuery = self.base.sack.query(flags=hawkey.IGNORE_MODULAR_EXCLUDES)
698
699        for spec in rpm_specs:
700            subj = dnf.subject.Subject(spec)
701            baseQuery = baseQuery.union(subj.get_best_query(
702                self.base.sack, with_nevra=True, with_provides=False, with_filenames=False,
703                query=getBestInitQuery))
704
705        baseQuery.apply()
706
707        for modulePackage in modulePackages:
708            artifacts = modulePackage.getArtifacts()
709            if not artifacts:
710                continue
711            query = baseQuery.filter(nevra_strict=artifacts)
712            if query:
713                for pkg in query:
714                    string_output = ""
715                    profiles = []
716                    for profile in sorted(modulePackage.getProfiles(), key=_profile_comparison_key):
717                        if pkg.name in profile.getContent():
718                            profiles.append(profile.getName())
719                    lines = OrderedDict()
720                    lines["Module"] = modulePackage.getFullIdentifier()
721                    lines["Profiles"] = " ".join(sorted(profiles))
722                    lines["Repo"] = modulePackage.getRepoID()
723                    lines["Summary"] = modulePackage.getSummary()
724
725                    table = self._create_simple_table(lines)
726
727                    string_output += "{}\n".format(self.base.output.term.bold(str(pkg)))
728                    string_output += "{}".format(table.toString())
729                    output.add(string_output)
730
731        return "\n\n".join(sorted(output))
732
733    def _create_and_fill_table(self, latest):
734        table = libdnf.smartcols.Table()
735        table.setTermforce(libdnf.smartcols.Table.TermForce_AUTO)
736        table.enableMaxout(True)
737        column_name = table.newColumn("Name")
738        column_stream = table.newColumn("Stream")
739        column_profiles = table.newColumn("Profiles")
740        column_profiles.setWrap(True)
741        column_info = table.newColumn("Summary")
742        column_info.setWrap(True)
743
744        if not self.base.conf.verbose:
745            column_info.hidden = True
746
747        for latest_per_repo in latest:
748            for nameStreamArch in latest_per_repo:
749                if len(nameStreamArch) == 1:
750                    modulePackage = nameStreamArch[0]
751                else:
752                    active = [module for module in nameStreamArch
753                              if self.base._moduleContainer.isModuleActive(module)]
754                    if active:
755                        modulePackage = active[0]
756                    else:
757                        modulePackage = nameStreamArch[0]
758                line = table.newLine()
759                default_str, enabled_str, disabled_str = self._module_strs_formatter(
760                    modulePackage, markActive=False)
761                default_profiles = self.base._moduleContainer.getDefaultProfiles(
762                    modulePackage.getName(), modulePackage.getStream())
763                profiles_str = self._profile_report_formatter(modulePackage, default_profiles,
764                                                             enabled_str)
765                line.getColumnCell(column_name).setData(modulePackage.getName())
766                line.getColumnCell(
767                    column_stream).setData(
768                    modulePackage.getStream() + default_str + enabled_str + disabled_str)
769                line.getColumnCell(column_profiles).setData(profiles_str)
770                summary_str = self._summary_report_formatter(modulePackage.getSummary())
771                line.getColumnCell(column_info).setData(summary_str)
772
773        return table
774
775    def _get_brief_description(self, module_specs, module_state):
776        modules = []
777        if module_specs:
778            for spec in module_specs:
779                module_list, nsvcap = self._get_modules(spec)
780                modules.extend(module_list)
781        else:
782            modules = self.base._moduleContainer.getModulePackages()
783        latest = self.base._moduleContainer.getLatestModulesPerRepo(module_state, modules)
784        if not latest:
785            return ""
786
787        table = self._create_and_fill_table(latest)
788        current_repo_id_index = 0
789        already_printed_lines = 0
790        try:
791            repo_name = self.base.repos[latest[0][0][0].getRepoID()].name
792        except KeyError:
793            repo_name = latest[0][0][0].getRepoID()
794        versions = len(latest[0])
795        header = self._format_header(table)
796        str_table = self._format_repoid(repo_name)
797        str_table += header
798        for i in range(0, table.getNumberOfLines()):
799            if versions + already_printed_lines <= i:
800                already_printed_lines += versions
801                current_repo_id_index += 1
802                # Fail-Safe repository is not in self.base.repos
803                try:
804                    repo_name = self.base.repos[
805                        latest[current_repo_id_index][0][0].getRepoID()].name
806                except KeyError:
807                    repo_name = latest[current_repo_id_index][0][0].getRepoID()
808                versions = len(latest[current_repo_id_index])
809                str_table += "\n"
810                str_table += self._format_repoid(repo_name)
811                str_table += header
812
813            line = table.getLine(i)
814            str_table += table.toString(line, line)
815        return str_table + MODULE_TABLE_HINT
816
817    def _format_header(self, table):
818        line = table.getLine(0)
819        return table.toString(line, line).split('\n', 1)[0] + '\n'
820
821    def _format_repoid(self, repo_name):
822        return "{}\n".format(self.base.output.term.bold(repo_name))
823
824    def _install_profiles_internal(self, install_set_artifacts, install_dict, strict):
825        #  Remove source packages because they cannot be installed or upgraded
826        base_no_source_query = self.base.sack.query().filterm(arch__neq=['src', 'nosrc']).apply()
827        install_base_query = base_no_source_query.filter(nevra_strict=install_set_artifacts)
828        error_specs = []
829
830        # add hot-fix packages
831        hot_fix_repos = [i.id for i in self.base.repos.iter_enabled() if i.module_hotfixes]
832        hotfix_packages = base_no_source_query.filter(
833            reponame=hot_fix_repos, name=install_dict.keys())
834        install_base_query = install_base_query.union(hotfix_packages)
835
836        for pkg_name, set_specs in install_dict.items():
837            query = install_base_query.filter(name=pkg_name)
838            if not query:
839                # package can also be non-modular or part of another stream
840                query = base_no_source_query.filter(name=pkg_name)
841                if not query:
842                    for spec in set_specs:
843                        logger.error(_("Unable to resolve argument {}").format(spec))
844                    logger.error(_("No match for package {}").format(pkg_name))
845                    error_specs.extend(set_specs)
846                    continue
847            self.base._goal.group_members.add(pkg_name)
848            sltr = dnf.selector.Selector(self.base.sack)
849            sltr.set(pkg=query)
850            self.base._goal.install(select=sltr, optional=(not strict))
851        return install_base_query, error_specs
852
853
854def format_modular_solver_errors(errors):
855    msg = dnf.util._format_resolve_problems(errors)
856    return "\n".join(
857        [P_('Modular dependency problem:', 'Modular dependency problems:', len(errors)), msg])
858