1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9DOCUMENTATION = '''
10---
11module: locale_gen
12short_description: Creates or removes locales
13description:
14     - Manages locales by editing /etc/locale.gen and invoking locale-gen.
15author:
16- Augustus Kling (@AugustusKling)
17options:
18    name:
19        type: str
20        description:
21             - Name and encoding of the locale, such as "en_GB.UTF-8".
22        required: true
23    state:
24      type: str
25      description:
26           - Whether the locale shall be present.
27      choices: [ absent, present ]
28      default: present
29'''
30
31EXAMPLES = '''
32- name: Ensure a locale exists
33  community.general.locale_gen:
34    name: de_CH.UTF-8
35    state: present
36'''
37
38import os
39import re
40from subprocess import Popen, PIPE, call
41
42from ansible.module_utils.basic import AnsibleModule
43from ansible.module_utils.common.text.converters import to_native
44
45LOCALE_NORMALIZATION = {
46    ".utf8": ".UTF-8",
47    ".eucjp": ".EUC-JP",
48    ".iso885915": ".ISO-8859-15",
49    ".cp1251": ".CP1251",
50    ".koi8r": ".KOI8-R",
51    ".armscii8": ".ARMSCII-8",
52    ".euckr": ".EUC-KR",
53    ".gbk": ".GBK",
54    ".gb18030": ".GB18030",
55    ".euctw": ".EUC-TW",
56}
57
58
59# ===========================================
60# location module specific support methods.
61#
62
63def is_available(name, ubuntuMode):
64    """Check if the given locale is available on the system. This is done by
65    checking either :
66    * if the locale is present in /etc/locales.gen
67    * or if the locale is present in /usr/share/i18n/SUPPORTED"""
68    if ubuntuMode:
69        __regexp = r'^(?P<locale>\S+_\S+) (?P<charset>\S+)\s*$'
70        __locales_available = '/usr/share/i18n/SUPPORTED'
71    else:
72        __regexp = r'^#{0,1}\s*(?P<locale>\S+_\S+) (?P<charset>\S+)\s*$'
73        __locales_available = '/etc/locale.gen'
74
75    re_compiled = re.compile(__regexp)
76    fd = open(__locales_available, 'r')
77    for line in fd:
78        result = re_compiled.match(line)
79        if result and result.group('locale') == name:
80            return True
81    fd.close()
82    return False
83
84
85def is_present(name):
86    """Checks if the given locale is currently installed."""
87    output = Popen(["locale", "-a"], stdout=PIPE).communicate()[0]
88    output = to_native(output)
89    return any(fix_case(name) == fix_case(line) for line in output.splitlines())
90
91
92def fix_case(name):
93    """locale -a might return the encoding in either lower or upper case.
94    Passing through this function makes them uniform for comparisons."""
95    for s, r in LOCALE_NORMALIZATION.items():
96        name = name.replace(s, r)
97    return name
98
99
100def replace_line(existing_line, new_line):
101    """Replaces lines in /etc/locale.gen"""
102    try:
103        f = open("/etc/locale.gen", "r")
104        lines = [line.replace(existing_line, new_line) for line in f]
105    finally:
106        f.close()
107    try:
108        f = open("/etc/locale.gen", "w")
109        f.write("".join(lines))
110    finally:
111        f.close()
112
113
114def set_locale(name, enabled=True):
115    """ Sets the state of the locale. Defaults to enabled. """
116    search_string = r'#{0,1}\s*%s (?P<charset>.+)' % name
117    if enabled:
118        new_string = r'%s \g<charset>' % (name)
119    else:
120        new_string = r'# %s \g<charset>' % (name)
121    try:
122        f = open("/etc/locale.gen", "r")
123        lines = [re.sub(search_string, new_string, line) for line in f]
124    finally:
125        f.close()
126    try:
127        f = open("/etc/locale.gen", "w")
128        f.write("".join(lines))
129    finally:
130        f.close()
131
132
133def apply_change(targetState, name):
134    """Create or remove locale.
135
136    Keyword arguments:
137    targetState -- Desired state, either present or absent.
138    name -- Name including encoding such as de_CH.UTF-8.
139    """
140    if targetState == "present":
141        # Create locale.
142        set_locale(name, enabled=True)
143    else:
144        # Delete locale.
145        set_locale(name, enabled=False)
146
147    localeGenExitValue = call("locale-gen")
148    if localeGenExitValue != 0:
149        raise EnvironmentError(localeGenExitValue, "locale.gen failed to execute, it returned " + str(localeGenExitValue))
150
151
152def apply_change_ubuntu(targetState, name):
153    """Create or remove locale.
154
155    Keyword arguments:
156    targetState -- Desired state, either present or absent.
157    name -- Name including encoding such as de_CH.UTF-8.
158    """
159    if targetState == "present":
160        # Create locale.
161        # Ubuntu's patched locale-gen automatically adds the new locale to /var/lib/locales/supported.d/local
162        localeGenExitValue = call(["locale-gen", name])
163    else:
164        # Delete locale involves discarding the locale from /var/lib/locales/supported.d/local and regenerating all locales.
165        try:
166            f = open("/var/lib/locales/supported.d/local", "r")
167            content = f.readlines()
168        finally:
169            f.close()
170        try:
171            f = open("/var/lib/locales/supported.d/local", "w")
172            for line in content:
173                locale, charset = line.split(' ')
174                if locale != name:
175                    f.write(line)
176        finally:
177            f.close()
178        # Purge locales and regenerate.
179        # Please provide a patch if you know how to avoid regenerating the locales to keep!
180        localeGenExitValue = call(["locale-gen", "--purge"])
181
182    if localeGenExitValue != 0:
183        raise EnvironmentError(localeGenExitValue, "locale.gen failed to execute, it returned " + str(localeGenExitValue))
184
185
186def main():
187    module = AnsibleModule(
188        argument_spec=dict(
189            name=dict(type='str', required=True),
190            state=dict(type='str', default='present', choices=['absent', 'present']),
191        ),
192        supports_check_mode=True,
193    )
194
195    name = module.params['name']
196    state = module.params['state']
197
198    if not os.path.exists("/etc/locale.gen"):
199        if os.path.exists("/var/lib/locales/supported.d/"):
200            # Ubuntu created its own system to manage locales.
201            ubuntuMode = True
202        else:
203            module.fail_json(msg="/etc/locale.gen and /var/lib/locales/supported.d/local are missing. Is the package \"locales\" installed?")
204    else:
205        # We found the common way to manage locales.
206        ubuntuMode = False
207
208    if not is_available(name, ubuntuMode):
209        module.fail_json(msg="The locale you've entered is not available "
210                             "on your system.")
211
212    if is_present(name):
213        prev_state = "present"
214    else:
215        prev_state = "absent"
216    changed = (prev_state != state)
217
218    if module.check_mode:
219        module.exit_json(changed=changed)
220    else:
221        if changed:
222            try:
223                if ubuntuMode is False:
224                    apply_change(state, name)
225                else:
226                    apply_change_ubuntu(state, name)
227            except EnvironmentError as e:
228                module.fail_json(msg=to_native(e), exitValue=e.errno)
229
230        module.exit_json(name=name, changed=changed, msg="OK")
231
232
233if __name__ == '__main__':
234    main()
235