diff --git a/system_setup/server/controllers/configure.py b/system_setup/server/controllers/configure.py index e951583a..8306e798 100644 --- a/system_setup/server/controllers/configure.py +++ b/system_setup/server/controllers/configure.py @@ -16,6 +16,7 @@ import os import shutil import logging import re +from typing import Tuple import apt import apt_pkg @@ -40,89 +41,189 @@ class ConfigureController(SubiquityController): def start(self): self.install_task = self.app.aio_loop.create_task(self.configure()) - def _update_locale_gen(self, localeGenPath, lang): - """ Uncomments the line in locale.gen file where lang is found, - if found commented. A fully qualified language is expected, - since that would have passed thru the Locale - controller validation. e.g. en_UK.UTF-8. """ + def __locale_gen_cmd(self) -> Tuple[str, bool]: + """ Returns the locale-gen command path if not in dry-run. + Otherwise, copies the locale-gen script, altering the localedef + command line to output into a specified directory. + Additionally, indicates success by returning True + as the second element of the tuple. """ - fileContents: str - locGenNeedsWrite = False - with open(localeGenPath, "r") as f: - fileContents = f.read() - lineFound = fileContents.find(lang) - if lineFound == -1: - # An unsupported locale coming from our UI is a bug - log.error("Selected language %s not supported.", lang) - return + cmd = "usr/sbin/locale-gen" + if self.app.opts.dry_run is False or self.app.opts.dry_run is None: + return (os.path.join("/", cmd), True) - pattern = r'#+\s*({}.*)'.format(lang) - commented = re.compile(pattern) - (fileContents, n) = commented.subn(r'\1', fileContents, count=1) - locGenNeedsWrite = n == 1 + outDir = os.path.join(self.model.root, "usr/lib/locale") + usrSbinDir = os.path.join(self.model.root, "usr/sbin") + os.makedirs(outDir, exist_ok=True) + os.makedirs(usrSbinDir, exist_ok=True) + cmdFile = os.path.join(self.model.root, cmd) + shutil.copy(os.path.join("/", cmd), cmdFile) + # Supply LC_* definition files to avoid complains from localedef. + shutil.copytree("/usr/lib/locale/C.UTF-8/", outDir, + dirs_exist_ok=True) - if locGenNeedsWrite: - try: - with open(localeGenPath, "wt") as f: - f.write(fileContents) - except IOError as err: - log.error("Failed to modify %s file. %s", localeGenPath, err) + try: + # Altering locale-gen script to output to the desired folder. + with open(cmdFile, "r+") as f: + script = f.read() + pattern = r'(localedef .+) (\|\| :; \\)' + localeDefRe = re.compile(pattern) + replacement = r'\1 "{}/" \2'.format(outDir) + (fileContents, n) = localeDefRe.subn(replacement, script, + count=1) + if n != 1: + log.error("locale-gen script contents were unexpected." + " Aborting mock creation") + return ("", False) - async def _install_check_lang_support_packages(self, lang, env): + f.seek(0) + f.write(fileContents) + + return (cmdFile, True) + + except IOError as err: + log.error("Failed to modify %s file. %s", cmdFile, err) + return ("", False) + + def __update_locale_cmd(self, lang) -> list[str]: + """ Adds mocking cli to update-locale if in dry-run. """ + updateLocCmd = ["update-locale", "LANG={}".format(lang)] + if self.app.opts.dry_run: + defaultLocDir = os.path.join(self.model.root, + "etc/default/") + os.makedirs(defaultLocDir, exist_ok=True) + updateLocCmd += ["--locale-file", + os.path.join(defaultLocDir, "locale"), + "--no-checks"] + + return updateLocCmd + + async def _activate_locale(self, lang, env) -> bool: + """ Last commands to run for locale support. Returns True on success""" + + (locGenCmd, ok) = self.__locale_gen_cmd() + if ok is False: + log.error("Locale generation failed.") + return False + + updateCmd = self.__update_locale_cmd(lang) + cmds = [[locGenCmd], updateCmd] + for cmd in cmds: + cp = await arun_command(cmd, env=env) + if cp.returncode != 0: + log.error('Command "{}" failed with return code {}' + .format(cp.args, cp.returncode)) + return False + return True + + async def _install_check_lang_support_packages(self, lang, env) -> bool: """ Installs any packages recommended by check-language-support command. """ clsCommand = "check-language-support" + # lang may have more than 5 chars and be separated by dot or space. + clsLang = lang.split('@')[0].split('.')[0].split(' ')[0] - cp = await arun_command([clsCommand, "-l", lang[0:5]], env=env) + # Running that command doesn't require root. + cp = await arun_command([clsCommand, "-l", clsLang], env=env) if cp.returncode != 0: - log.error('Command \"%s\" failed with return code %d', + log.error('Command "%s" failed with return code %d', cp.args, cp.returncode) - return + return False packages = cp.stdout.strip('\n').split(' ') if len(packages) == 0: log.debug("%s didn't recommend any packages. Nothing to do.", clsCommand) - return + return True cache = apt.Cache() if self.app.opts.dry_run: + packs_dir = os.path.join(self.model.root, + apt_pkg.config + .find_dir("Dir::Cache::Archives")[1:]) + os.makedirs(packs_dir, exist_ok=True) for package in packages: # Downloading instead of installing. Doesn't require root. - packs_dir = os.path.join(self.model.root, - apt_pkg.config - .find_dir("Dir::Cache::Archives")[1:]) archive = os.path.join(packs_dir, cache[package].fullname) cmd = ["wget", "-O", archive, cache[package].candidate.uri] - os.makedirs(packs_dir, exist_ok=True) await arun_command(cmd, env=env) - else: - cache.update() - cache.open(None) - with cache.actiongroup(): - for package in packages: - cache[package].mark_install() - cache.commit() - async def _activate_locale(self, lang, env): - """ Final commands to run for locale support """ + return True - cmds = [["locale-gen"], - ["update-locale", "LANG={}".format(lang)]] - if self.app.opts.dry_run: - for cmd in cmds: - # TODO Figure out about mocks. - # It would be good if they offered a -P prefix option, - # but they don't. - # Chroot'ing is not an option. - log.debug('Would run: %s', ' '.join(cmd)) - else: - for cmd in cmds: - cp = await arun_command(cmd, env=env) - if cp.returncode != 0: - raise SystemError('Command {} failed with return code {}' - .format(cp.args, cp.returncode)) + cache.update() + cache.open(None) + with cache.actiongroup(): + for package in packages: + cache[package].mark_install() + + cache.commit() + + return True + + def _update_locale_gen_file(self, localeGenPath, lang) -> bool: + """ Uncomments the line in locale.gen file where lang is found, + if found commented. A fully qualified language is expected, + since that would have passed thru the Locale controller + validation. e.g. en_UK.UTF-8. Returns True for success. """ + + fileContents: str + try: + with open(localeGenPath, "r+") as f: + fileContents = f.read() + lineFound = fileContents.find(lang) + if lineFound == -1: + # An unsupported locale coming from our UI is a bug + log.error("Selected language %s not supported.", lang) + return False + + pattern = r'[# ]*({}.*)'.format(lang) + commented = re.compile(pattern) + (fileContents, n) = commented.subn(r'\1', fileContents, + count=1) + if n != 1: + log.error("Unexpected locale.gen file contents. Aborting.") + return False + + f.seek(0) + f.write(fileContents) + return True + + except IOError as err: + log.error("Failed to modify %s file. %s", localeGenPath, err) + return False + + def _locale_gen_file_path(self): + localeGenPath = "/etc/locale.gen" + if self.app.opts.dry_run is False or self.app.opts.dry_run is None: + return localeGenPath + + # For testing purposes. + etc_dir = os.path.join(self.model.root, "etc") + testLocGenPath = os.path.join(etc_dir, + os.path.basename(localeGenPath)) + shutil.copy(localeGenPath, testLocGenPath) + shutil.copy(localeGenPath, "{}-".format(testLocGenPath)) + return testLocGenPath + + async def apply_locale(self, lang): + """ Effectively apply the locale configuration to the new system. """ + env = os.environ.copy() + localeGenPath = self._locale_gen_file_path() + if self._update_locale_gen_file(localeGenPath, lang) is False: + log.error("Failed to update locale.gen") + return + + ok = await self._install_check_lang_support_packages(lang, env) + + if ok is False: + log.error("Failed to install recommended language packs.") + return + + ok = await self._activate_locale(lang, env) + if ok is False: + log.error("Failed to run locale activation commands.") + return @with_context( description="final system configuration", level="INFO", @@ -154,7 +255,6 @@ class ConfigureController(SubiquityController): self.app.update_state(ApplicationState.POST_RUNNING) - localeGenPath = "/etc/locale.gen" dryrun = self.app.opts.dry_run variant = self.app.variant root_dir = self.model.root @@ -176,18 +276,6 @@ class ConfigureController(SubiquityController): pseudo_files = ["passwd", "shadow", "gshadow", "group", "subgid", "subuid"] - # locale - testLocGenPath = os.path.join(etc_dir, - os.path - .basename(localeGenPath)) - shutil.copy(localeGenPath, testLocGenPath) - shutil.copy(localeGenPath, - "{}-".format(testLocGenPath)) - # After copies are done, overwrite the original path - # because there is code outside of this 'if' - # depending on 'localeGenPath' being initialized. - localeGenPath = testLocGenPath - for file in pseudo_files: filepath = os.path.join(etc_dir, file) open(filepath, "a").close() @@ -210,11 +298,6 @@ class ConfigureController(SubiquityController): create_user_base = ["-P", root_dir] assign_grp_base = ["-P", root_dir] - env = os.environ.copy() - self._update_locale_gen(localeGenPath, lang) - await self._install_check_lang_support_packages(lang, env) - await self._activate_locale(lang, env) - create_user_cmd = ["useradd"] + create_user_base + \ ["-m", "-s", "/bin/bash", "-c", wsl_id.realname, @@ -233,6 +316,8 @@ class ConfigureController(SubiquityController): if assign_grp_proc.returncode != 0: raise Exception(("Failed to assign group to user %s: %s") % (username, assign_grp_proc.stderr)) + + await self.apply_locale(lang) else: wsl_config_update(self.model.wslconfadvanced.wslconfadvanced, root_dir)