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