From 98aca27a8b45c39a515a2b7f62015261ac9bbab1 Mon Sep 17 00:00:00 2001 From: Alexei Znamensky <103110+russoz@users.noreply.github.com> Date: Sun, 16 Nov 2025 11:17:08 +1300 Subject: [PATCH] locale_gen: search for available locales in /usr/local as well (#11046) * locale_gen: search for available locales in /usr/local as well * better var name * add test for /usr/local * Apply suggestions from code review Co-authored-by: Felix Fontein * skip /usr/local/ for Archlinux * improve/update documentation * add license file for the custom locale * add changelog frag * Update plugins/modules/locale_gen.py Co-authored-by: Felix Fontein * Update changelogs/fragments/11046-locale-gen-usrlocal.yml Co-authored-by: Felix Fontein --------- Co-authored-by: Felix Fontein --- .../fragments/11046-locale-gen-usrlocal.yml | 2 + plugins/modules/locale_gen.py | 60 +++++---- .../targets/locale_gen/files/en_US@iso | 120 ++++++++++++++++++ .../locale_gen/files/en_US@iso.license | 3 + .../locale_gen/tasks/11046-usrlocal.yml | 85 +++++++++++++ .../targets/locale_gen/tasks/main.yml | 3 + 6 files changed, 247 insertions(+), 26 deletions(-) create mode 100644 changelogs/fragments/11046-locale-gen-usrlocal.yml create mode 100644 tests/integration/targets/locale_gen/files/en_US@iso create mode 100644 tests/integration/targets/locale_gen/files/en_US@iso.license create mode 100644 tests/integration/targets/locale_gen/tasks/11046-usrlocal.yml diff --git a/changelogs/fragments/11046-locale-gen-usrlocal.yml b/changelogs/fragments/11046-locale-gen-usrlocal.yml new file mode 100644 index 0000000000..51fa096b4c --- /dev/null +++ b/changelogs/fragments/11046-locale-gen-usrlocal.yml @@ -0,0 +1,2 @@ +minor_changes: + - locale_gen - extend the search for available locales to include ``/usr/local/share/i18n/SUPPORTED`` in Debian and Ubuntu systems (https://github.com/ansible-collections/community.general/issues/10964, https://github.com/ansible-collections/community.general/pull/11046). diff --git a/plugins/modules/locale_gen.py b/plugins/modules/locale_gen.py index b555fe0d66..2ae8ff4e00 100644 --- a/plugins/modules/locale_gen.py +++ b/plugins/modules/locale_gen.py @@ -34,17 +34,20 @@ options: - Whether the locales shall be present. choices: [absent, present] default: present + notes: + - Currently the module is B(only supported for Debian, Ubuntu, and Arch Linux) systems. + - This module requires the package C(locales) installed in Debian and Ubuntu systems. - If C(/etc/locale.gen) exists, the module assumes to be using the B(glibc) mechanism, else if C(/var/lib/locales/supported.d/) exists it assumes to be using the B(ubuntu_legacy) mechanism, else it raises an error. - - When using glibc mechanism, it manages locales by editing C(/etc/locale.gen) and running C(locale-gen). - - When using ubuntu_legacy mechanism, it manages locales by editing C(/var/lib/locales/supported.d/local) and then running + - When using V(glibc) mechanism, it manages locales by editing C(/etc/locale.gen) and running C(locale-gen). + - When using V(ubuntu_legacy) mechanism, it manages locales by editing C(/var/lib/locales/supported.d/local) and then running C(locale-gen). - - Please note that the code path that uses ubuntu_legacy mechanism has not been tested for a while, because Ubuntu is already - using the glibc mechanism. There is no support for that, given our inability to test it. Therefore, that mechanism is - B(deprecated) and will be removed in community.general 13.0.0. - - Currently the module is B(only supported for Debian and Ubuntu) systems. - - This module requires the package C(locales) installed in Debian and Ubuntu systems. + - Please note that the module asserts the availability of the locale by checking the files C(/usr/share/i18n/SUPPORTED) and + C(/usr/local/share/i18n/SUPPORTED), but the C(/usr/local) one is not supported by Archlinux. + - Please note that the code path that uses V(ubuntu_legacy) mechanism has not been tested for a while, because recent versions of + Ubuntu is already using the V(glibc) mechanism. There is no support for V(ubuntu_legacy), given our inability to test it. + Therefore, that mechanism is B(deprecated) and will be removed in community.general 13.0.0. """ EXAMPLES = r""" @@ -85,7 +88,7 @@ from ansible_collections.community.general.plugins.module_utils.locale_gen impor ETC_LOCALE_GEN = "/etc/locale.gen" VAR_LIB_LOCALES = "/var/lib/locales/supported.d" VAR_LIB_LOCALES_LOCAL = os.path.join(VAR_LIB_LOCALES, "local") -SUPPORTED_LOCALES = "/usr/share/i18n/SUPPORTED" +SUPPORTED_LOCALES = ["/usr/share/i18n/SUPPORTED", "/usr/local/share/i18n/SUPPORTED"] LOCALE_NORMALIZATION = { ".utf8": ".UTF-8", ".eucjp": ".EUC-JP", @@ -111,7 +114,7 @@ class LocaleGen(StateModuleHelper): ) def __init_module__(self): - self.MECHANISMS = dict( + self.mechanisms = dict( ubuntu_legacy=dict( available=SUPPORTED_LOCALES, apply_change=self.apply_change_ubuntu_legacy, @@ -156,19 +159,21 @@ class LocaleGen(StateModuleHelper): checking either : * if the locale is present in /etc/locales.gen * or if the locale is present in /usr/share/i18n/SUPPORTED""" - regexp = r"^\s*#?\s*(?P\S+[\._\S]+) (?P\S+)\s*$" - locales_available = self.MECHANISMS[self.vars.mechanism]["available"] - re_compiled = re.compile(regexp) - with open(locales_available, "r") as fd: - lines = fd.readlines() - res = [re_compiled.match(line) for line in lines] - self.vars.set("available_lines", lines, verbosity=4) + self.vars.set("available_lines", [], verbosity=4) + available_locale_entry_re_matches = [] + for locale_path in self.mechanisms[self.vars.mechanism]["available"]: + if os.path.exists(locale_path): + with open(locale_path, "r") as fd: + self.vars.available_lines.extend(fd.readlines()) + + re_locale_entry = re.compile(r"^\s*#?\s*(?P\S+[\._\S]+) (?P\S+)\s*$") + available_locale_entry_re_matches.extend([re_locale_entry.match(line) for line in self.vars.available_lines]) locales_not_found = [] for locale in self.vars.name: # Check if the locale is not found in any of the matches - if not any(match and match.group("locale") == locale for match in res): + if not any(match and match.group("locale") == locale for match in available_locale_entry_re_matches): locales_not_found.append(locale) # locale may be installed but not listed in the file, for example C.UTF-8 in some systems @@ -219,38 +224,41 @@ class LocaleGen(StateModuleHelper): re_search = re.compile(search_string) locale_regexes.append([re_search, new_string]) - for i in range(len(lines)): + def search_replace(line): for [search, replace] in locale_regexes: - lines[i] = search.sub(replace, lines[i]) + line = search.sub(replace, line) + return line + + lines = [search_replace(line) for line in lines] # Write the modified content back to the file with open(ETC_LOCALE_GEN, "w") as fw: fw.writelines(lines) - def apply_change_glibc(self, targetState, names): + def apply_change_glibc(self, target_state, names): """Create or remove locale. Keyword arguments: - targetState -- Desired state, either present or absent. + target_state -- Desired state, either present or absent. names -- Names list including encoding such as de_CH.UTF-8. """ - self.set_locale_glibc(names, enabled=(targetState == "present")) + self.set_locale_glibc(names, enabled=(target_state == "present")) runner = locale_gen_runner(self.module) with runner() as ctx: ctx.run() - def apply_change_ubuntu_legacy(self, targetState, names): + def apply_change_ubuntu_legacy(self, target_state, names): """Create or remove locale. Keyword arguments: - targetState -- Desired state, either present or absent. + target_state -- Desired state, either present or absent. names -- Name list including encoding such as de_CH.UTF-8. """ runner = locale_gen_runner(self.module) - if targetState == "present": + if target_state == "present": # Create locale. # Ubuntu's patched locale-gen automatically adds the new locale to /var/lib/locales/supported.d/local with runner() as ctx: @@ -273,7 +281,7 @@ class LocaleGen(StateModuleHelper): def __state_fallback__(self): if self.vars.state_tracking == self.vars.state: return - self.MECHANISMS[self.vars.mechanism]["apply_change"](self.vars.state, self.vars.name) + self.mechanisms[self.vars.mechanism]["apply_change"](self.vars.state, self.vars.name) def main(): diff --git a/tests/integration/targets/locale_gen/files/en_US@iso b/tests/integration/targets/locale_gen/files/en_US@iso new file mode 100644 index 0000000000..23b234048d --- /dev/null +++ b/tests/integration/targets/locale_gen/files/en_US@iso @@ -0,0 +1,120 @@ +comment_char % +escape_char / + +% This file is part of the GNU C Library and contains locale data. +% The Free Software Foundation does not claim any copyright interest +% in the locale data contained in this file. The foregoing does not +% affect the license of the GNU C Library as a whole. It does not +% exempt you from the conditions of the license if your use would +% otherwise be governed by that license. + +LC_IDENTIFICATION +title "English locale for the USA with ISO formats" +language "American English" + +category "i18n:2012";LC_IDENTIFICATION +category "i18n:2012";LC_CTYPE +category "i18n:2012";LC_COLLATE +category "i18n:2012";LC_TIME +category "i18n:2012";LC_NUMERIC +category "i18n:2012";LC_MONETARY +category "i18n:2012";LC_MESSAGES +category "i18n:2012";LC_PAPER +category "i18n:2012";LC_NAME +category "i18n:2012";LC_ADDRESS +category "i18n:2012";LC_TELEPHONE +category "i18n:2012";LC_MEASUREMENT +END LC_IDENTIFICATION + +LC_TIME +day "Sunday";/ + "Monday";/ + "Tuesday";/ + "Wednesday";/ + "Thursday";/ + "Friday";/ + "Saturday" +abday "Sun";/ + "Mon";/ + "Tue";/ + "Wed";/ + "Thu";/ + "Fri";/ + "Sat" + +mon "January";/ + "February";/ + "March";/ + "April";/ + "May";/ + "June";/ + "July";/ + "August";/ + "September";/ + "October";/ + "November";/ + "December" +abmon "Jan";/ + "Feb";/ + "Mar";/ + "Apr";/ + "May";/ + "Jun";/ + "Jul";/ + "Aug";/ + "Sep";/ + "Oct";/ + "Nov";/ + "Dec" + +date_fmt "%a %d %b %Y %T %Z" +d_t_fmt "%a %d %b %Y %T" +d_fmt "%Y-%m-%d" +t_fmt "%T" +t_fmt_ampm "" +am_pm "";"" +week 7;19971130;4 +first_weekday 2 +END LC_TIME + +LC_CTYPE +copy "en_US" +END LC_CTYPE + +LC_COLLATE +copy "en_US" +END LC_COLLATE + +LC_MONETARY +copy "en_US" +END LC_MONETARY + +LC_NUMERIC +decimal_point "." +thousands_sep "," +grouping 3;3 +END LC_NUMERIC + +LC_MESSAGES +copy "en_US" +END LC_MESSAGES + +LC_PAPER +copy "i18n" +END LC_PAPER + +LC_TELEPHONE +copy "en_US" +END LC_TELEPHONE + +LC_MEASUREMENT +copy "i18n" +END LC_MEASUREMENT + +LC_NAME +copy "en_US" +END LC_NAME + +LC_ADDRESS +copy "en_US" +END LC_ADDRESS diff --git a/tests/integration/targets/locale_gen/files/en_US@iso.license b/tests/integration/targets/locale_gen/files/en_US@iso.license new file mode 100644 index 0000000000..a1390a69ed --- /dev/null +++ b/tests/integration/targets/locale_gen/files/en_US@iso.license @@ -0,0 +1,3 @@ +Copyright (c) Ansible Project +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/tests/integration/targets/locale_gen/tasks/11046-usrlocal.yml b/tests/integration/targets/locale_gen/tasks/11046-usrlocal.yml new file mode 100644 index 0000000000..bcdad4c548 --- /dev/null +++ b/tests/integration/targets/locale_gen/tasks/11046-usrlocal.yml @@ -0,0 +1,85 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Arch-less block + when: ansible_distribution not in ['Archlinux'] + block: + - name: Is the locale we're going to test against installed? + command: locale -a + register: initial_state + + - name: Make sure the locale is not available + community.general.locale_gen: + name: en_US@iso + state: absent + ignore_errors: true + register: not_available + + - name: Ensure /usr/local/share/i18n/ + ansible.builtin.file: + path: /usr/local/share/i18n/locales + state: directory + mode: '0755' + + - name: Back up /etc/locale.gen + ansible.builtin.copy: + src: /etc/locale.gen + dest: /etc/locale.gen.bkp + remote_src: true + + - name: Add line to /etc/locale.gen + ansible.builtin.shell: > + echo "# en_US@iso UTF-8" >> /etc/locale.gen + + - name: Copy custom locale + ansible.builtin.copy: + dest: /usr/local/share/i18n/locales/en_US@iso + src: en_US@iso + mode: '0644' + + - name: Add custom locale to SUPPORTED + ansible.builtin.copy: + dest: /usr/local/share/i18n/SUPPORTED + content: | + en_US@iso UTF-8 + mode: '0644' + + - name: Make sure the locale is available + community.general.locale_gen: + name: en_US@iso + state: absent + register: available + + - name: Make sure the locale is installed + community.general.locale_gen: + name: en_US@iso + state: present + register: installed + + - name: Check assertions + ansible.builtin.assert: + that: + - not_available is failed + - > + "locales you have entered are not available on your system: en_US@iso" in not_available.msg + - available is not changed + - installed is changed + + always: + - name: Make sure the locale is not installed + community.general.locale_gen: + name: en_US@iso + state: absent + + - name: Remove /usr/local/share/i18n/ + ansible.builtin.file: + path: /usr/local/share/i18n/ + state: absent + + - name: Restore /etc/locale.gen + ansible.builtin.copy: + src: /etc/locale.gen.bkp + dest: /etc/locale.gen + remote_src: true diff --git a/tests/integration/targets/locale_gen/tasks/main.yml b/tests/integration/targets/locale_gen/tasks/main.yml index d24ef5d29d..76528af2ec 100644 --- a/tests/integration/targets/locale_gen/tasks/main.yml +++ b/tests/integration/targets/locale_gen/tasks/main.yml @@ -17,3 +17,6 @@ loop: "{{ locale_list_basic }}" loop_control: loop_var: locale_basic + +- name: Run tests for 11046 + ansible.builtin.include_tasks: 11046-usrlocal.yml