1# -*- coding: utf-8 -*-
2#
3# # Copyright: (c) 2012, Red Hat, Inc
4# Written by Seth Vidal <skvidal at fedoraproject.org>
5# Contributing Authors:
6#    - Ansible Core Team
7#    - Eduard Snesarev (@verm666)
8#    - Berend De Schouwer (@berenddeschouwer)
9#    - Abhijeet Kasurde (@Akasurde)
10# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
11
12import os
13import time
14import glob
15import tempfile
16from abc import ABCMeta, abstractmethod
17
18from ansible.module_utils._text import to_native
19from ansible.module_utils.six import with_metaclass
20
21yumdnf_argument_spec = dict(
22    argument_spec=dict(
23        allow_downgrade=dict(type='bool', default=False),
24        autoremove=dict(type='bool', default=False),
25        bugfix=dict(required=False, type='bool', default=False),
26        conf_file=dict(type='str'),
27        disable_excludes=dict(type='str', default=None),
28        disable_gpg_check=dict(type='bool', default=False),
29        disable_plugin=dict(type='list', default=[]),
30        disablerepo=dict(type='list', default=[]),
31        download_only=dict(type='bool', default=False),
32        download_dir=dict(type='str', default=None),
33        enable_plugin=dict(type='list', default=[]),
34        enablerepo=dict(type='list', default=[]),
35        exclude=dict(type='list', default=[]),
36        installroot=dict(type='str', default="/"),
37        install_repoquery=dict(type='bool', default=True),
38        install_weak_deps=dict(type='bool', default=True),
39        list=dict(type='str'),
40        name=dict(type='list', elements='str', aliases=['pkg'], default=[]),
41        releasever=dict(default=None),
42        security=dict(type='bool', default=False),
43        skip_broken=dict(type='bool', default=False),
44        # removed==absent, installed==present, these are accepted as aliases
45        state=dict(type='str', default=None, choices=['absent', 'installed', 'latest', 'present', 'removed']),
46        update_cache=dict(type='bool', default=False, aliases=['expire-cache']),
47        update_only=dict(required=False, default="no", type='bool'),
48        validate_certs=dict(type='bool', default=True),
49        lock_timeout=dict(type='int', default=30),
50    ),
51    required_one_of=[['name', 'list', 'update_cache']],
52    mutually_exclusive=[['name', 'list']],
53    supports_check_mode=True,
54)
55
56
57class YumDnf(with_metaclass(ABCMeta, object)):
58    """
59    Abstract class that handles the population of instance variables that should
60    be identical between both YUM and DNF modules because of the feature parity
61    and shared argument spec
62    """
63
64    def __init__(self, module):
65
66        self.module = module
67
68        self.allow_downgrade = self.module.params['allow_downgrade']
69        self.autoremove = self.module.params['autoremove']
70        self.bugfix = self.module.params['bugfix']
71        self.conf_file = self.module.params['conf_file']
72        self.disable_excludes = self.module.params['disable_excludes']
73        self.disable_gpg_check = self.module.params['disable_gpg_check']
74        self.disable_plugin = self.module.params['disable_plugin']
75        self.disablerepo = self.module.params.get('disablerepo', [])
76        self.download_only = self.module.params['download_only']
77        self.download_dir = self.module.params['download_dir']
78        self.enable_plugin = self.module.params['enable_plugin']
79        self.enablerepo = self.module.params.get('enablerepo', [])
80        self.exclude = self.module.params['exclude']
81        self.installroot = self.module.params['installroot']
82        self.install_repoquery = self.module.params['install_repoquery']
83        self.install_weak_deps = self.module.params['install_weak_deps']
84        self.list = self.module.params['list']
85        self.names = [p.strip() for p in self.module.params['name']]
86        self.releasever = self.module.params['releasever']
87        self.security = self.module.params['security']
88        self.skip_broken = self.module.params['skip_broken']
89        self.state = self.module.params['state']
90        self.update_only = self.module.params['update_only']
91        self.update_cache = self.module.params['update_cache']
92        self.validate_certs = self.module.params['validate_certs']
93        self.lock_timeout = self.module.params['lock_timeout']
94
95        # It's possible someone passed a comma separated string since it used
96        # to be a string type, so we should handle that
97        self.names = self.listify_comma_sep_strings_in_list(self.names)
98        self.disablerepo = self.listify_comma_sep_strings_in_list(self.disablerepo)
99        self.enablerepo = self.listify_comma_sep_strings_in_list(self.enablerepo)
100        self.exclude = self.listify_comma_sep_strings_in_list(self.exclude)
101
102        # Fail if someone passed a space separated string
103        # https://github.com/ansible/ansible/issues/46301
104        for name in self.names:
105            if ' ' in name and not any(spec in name for spec in ['@', '>', '<', '=']):
106                module.fail_json(
107                    msg='It appears that a space separated string of packages was passed in '
108                        'as an argument. To operate on several packages, pass a comma separated '
109                        'string of packages or a list of packages.'
110                )
111
112        # Sanity checking for autoremove
113        if self.state is None:
114            if self.autoremove:
115                self.state = "absent"
116            else:
117                self.state = "present"
118
119        if self.autoremove and (self.state != "absent"):
120            self.module.fail_json(
121                msg="Autoremove should be used alone or with state=absent",
122                results=[],
123            )
124
125        # This should really be redefined by both the yum and dnf module but a
126        # default isn't a bad idea
127        self.lockfile = '/var/run/yum.pid'
128
129    @abstractmethod
130    def is_lockfile_pid_valid(self):
131        return
132
133    def _is_lockfile_present(self):
134        return (os.path.isfile(self.lockfile) or glob.glob(self.lockfile)) and self.is_lockfile_pid_valid()
135
136    def wait_for_lock(self):
137        '''Poll until the lock is removed if timeout is a positive number'''
138
139        if not self._is_lockfile_present():
140            return
141
142        if self.lock_timeout > 0:
143            for iteration in range(0, self.lock_timeout):
144                time.sleep(1)
145                if not self._is_lockfile_present():
146                    return
147
148        self.module.fail_json(msg='{0} lockfile is held by another process'.format(self.pkg_mgr_name))
149
150    def listify_comma_sep_strings_in_list(self, some_list):
151        """
152        method to accept a list of strings as the parameter, find any strings
153        in that list that are comma separated, remove them from the list and add
154        their comma separated elements to the original list
155        """
156        new_list = []
157        remove_from_original_list = []
158        for element in some_list:
159            if ',' in element:
160                remove_from_original_list.append(element)
161                new_list.extend([e.strip() for e in element.split(',')])
162
163        for element in remove_from_original_list:
164            some_list.remove(element)
165
166        some_list.extend(new_list)
167
168        if some_list == [""]:
169            return []
170
171        return some_list
172
173    @abstractmethod
174    def run(self):
175        raise NotImplementedError
176