From ab553fb9323f1f9fd88d79d09f706b8c5710b0aa Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 16 Jun 2015 15:32:58 -0700 Subject: [PATCH 1/2] Add simple_replace config file function When modifying values in config files we want to preserve the rest of the file as is. SimpleConfig does this for shell-like files using upper-case KEYS, but other files (eg. INI style) don't use the same format. simple_replace takes a list of tuples (key, string) and replaces every line in the file that starts with the key with the string. It is case sensitive and the string must be the whole line including key. eg. keys=[("switch_1", "switch_1=On")] simple_replace("/etc/control.conf", keys) By default if a key isn't replaced in the existing file its string will be appended to the end of the file. Set add=False to disable this behavior. (cherry picked from commit f5acc57fda7994ec8168d2027adfa2049bdffbf6) --- pyanaconda/simpleconfig.py | 75 +++++++++++++++------ tests/pyanaconda_tests/simpleconfig_test.py | 45 ++++++++++++- 2 files changed, 100 insertions(+), 20 deletions(-) diff --git a/pyanaconda/simpleconfig.py b/pyanaconda/simpleconfig.py index f30258fd687..01cc20c5802 100644 --- a/pyanaconda/simpleconfig.py +++ b/pyanaconda/simpleconfig.py @@ -1,7 +1,7 @@ # # simpleconifg.py - representation of a simple configuration file (sh-like) # -# Copyright (C) 1999-2014 Red Hat, Inc. +# Copyright (C) 1999-2015 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of @@ -69,6 +69,24 @@ def find_comment(s): return None +def write_tmpfile(filename, data): + # Create a temporary in the same directory as the target file to ensure + # the new file is on the same filesystem + tmpf = tempfile.NamedTemporaryFile(mode="w", delete=False, + dir=os.path.dirname(filename) or '.', prefix="." + os.path.basename(filename)) + tmpf.write(data) + tmpf.close() + + # Change the permissions (currently 0600) to match the original file + if os.path.exists(filename): + m = os.stat(filename).st_mode + else: + m = 0o0644 + eintr_retry_call(os.chmod, filename, m) + + # Move the temporary file over the top of the original + os.rename(tmpf.name, filename) + class SimpleConfigFile(object): """ Edit values in a configuration file without changing comments. Supports KEY=VALUE lines and ignores everything else. @@ -111,24 +129,7 @@ def write(self, filename=None, use_tmp=True): return None if use_tmp: - filename = os.path.realpath(filename) - - # Create a temporary in the same directory as the target file to ensure - # the new file is on the same filesystem - tmpf = tempfile.NamedTemporaryFile(mode="w", delete=False, - dir=os.path.dirname(filename), prefix="." + os.path.basename(filename)) - tmpf.write(str(self)) - tmpf.close() - - # Change the permissions (currently 0600) to match the original file - if os.path.exists(filename): - m = os.stat(filename).st_mode - else: - m = 0o0644 - eintr_retry_call(os.chmod, filename, m) - - # Move the temporary file over the top of the original - os.rename(tmpf.name, filename) + write_tmpfile(filename, str(self)) else: # write directly to the file with open(filename, "w") as fobj: @@ -205,3 +206,39 @@ def __str__(self): s += self._kvpair(key) return s + + +def simple_replace(fname, keys, add=True, add_comment="# Added by Anaconda"): + """ Replace lines in a file, optionally adding if missing. + + :param str fname: Filename to operate on + :param list keys: List of (key, string) tuples to search and replace + :param bool add: When True add strings that were not replaced + + This will read all the lines in a file, looking for ones that start + with keys and replacing the line with the associated string. The string + should be a COMPLETE replacement for the line, not just a value. + + When add is True any keys that haven't been found will be appended + to the end of the file along with the add_comment. + """ + # Helper to return the line or the first matching key's string + def _replace(l): + r = [s for k,s in keys if l.startswith(k)] + if r: + return r[0] + else: + return l + + # Replace lines that match any of the keys + with open(fname, "r") as f: + lines = [_replace(l.strip()) for l in f] + + # Add any strings that weren't already in the file + if add: + append = [s for k,s in keys if not any(l.startswith(k) for l in lines)] + if append: + lines += [add_comment] + lines += append + + write_tmpfile(fname, "\n".join(lines)+"\n") diff --git a/tests/pyanaconda_tests/simpleconfig_test.py b/tests/pyanaconda_tests/simpleconfig_test.py index 4d1879a137f..4c4fa8a978a 100644 --- a/tests/pyanaconda_tests/simpleconfig_test.py +++ b/tests/pyanaconda_tests/simpleconfig_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2014 Red Hat, Inc. +# Copyright (C) 2014-2015 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of @@ -22,6 +22,7 @@ # pylint: disable=interruptible-system-call from pyanaconda.simpleconfig import SimpleConfigFile +from pyanaconda.simpleconfig import simple_replace from pyanaconda import simpleconfig import unittest import tempfile @@ -189,3 +190,45 @@ def no_use_tmp_test(self): # Check that the original file handle points to the replaced contents self.assertEqual(testconfig.read(), 'KEY1=value2\n') + +class SimpleReplaceTests(unittest.TestCase): + TEST_CONFIG = """#SKIP=Skip this commented line +BOOT=always +""" + + def replace_test(self): + with tempfile.NamedTemporaryFile(mode="wt") as testconfig: + testconfig.write(self.TEST_CONFIG) + testconfig.flush() + + keys = [("BOOT", "BOOT=never")] + simple_replace(testconfig.name, keys) + + config = SimpleConfigFile(testconfig.name) + config.read() + self.assertEqual(config.get("BOOT"), "never") + + def append_test(self): + with tempfile.NamedTemporaryFile(mode="wt") as testconfig: + testconfig.write(self.TEST_CONFIG) + testconfig.flush() + + keys = [("NEWKEY", "NEWKEY=froboz")] + simple_replace(testconfig.name, keys) + + config = SimpleConfigFile(testconfig.name) + config.read() + self.assertEqual(config.get("NEWKEY"), "froboz") + + def no_append_test(self): + with tempfile.NamedTemporaryFile(mode="wt") as testconfig: + testconfig.write(self.TEST_CONFIG) + testconfig.flush() + + keys = [("BOOT", "BOOT=sometimes"), ("NEWKEY", "NEWKEY=froboz")] + simple_replace(testconfig.name, keys, add=False) + + config = SimpleConfigFile(testconfig.name) + config.read() + self.assertEqual(config.get("BOOT"), "sometimes") + self.assertEqual(config.get("NEWKEY"), "") From 1aedfe5743a7bcda3621eaa03ef1eab36c14681f Mon Sep 17 00:00:00 2001 From: David Shea Date: Fri, 17 Jul 2015 13:05:04 -0400 Subject: [PATCH 2/2] Connect kickstart lang data to dnf-langpacks (#1051816) Run the dnf transaction with a langpacks.conf configured with the kickstart locales, and copy langpacks.conf to the installed system. --- pyanaconda/packaging/dnfpayload.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyanaconda/packaging/dnfpayload.py b/pyanaconda/packaging/dnfpayload.py index 311c7f89cb7..15c67e8e3f9 100644 --- a/pyanaconda/packaging/dnfpayload.py +++ b/pyanaconda/packaging/dnfpayload.py @@ -26,6 +26,7 @@ from pyanaconda.flags import flags from pyanaconda.i18n import _ from pyanaconda.progress import progressQ +from pyanaconda.simpleconfig import simple_replace import configparser import collections @@ -54,6 +55,7 @@ import rpm DNF_CACHE_DIR = '/tmp/dnf.cache' +DNF_PLUGINCONF_DIR = '/tmp/dnf.pluginconf' DNF_PACKAGE_CACHE_DIR_SUFFIX = 'dnf.package.cache' DOWNLOAD_MPOINTS = {'/tmp', '/', @@ -68,6 +70,9 @@ '/tmp/product/anaconda.repos.d'] YUM_REPOS_DIR = "/etc/yum.repos.d/" +_dnf_installer_langpack_conf = DNF_PLUGINCONF_DIR + "/langpacks.conf" +_dnf_target_langpack_conf = "/etc/dnf/plugins/langpacks.conf" + def _failure_limbo(): progressQ.send_quit(1) while True: @@ -354,6 +359,7 @@ def _configure(self): self._base = dnf.Base() conf = self._base.conf conf.cachedir = DNF_CACHE_DIR + conf.pluginconfpath = DNF_PLUGINCONF_DIR conf.logdir = '/tmp/' # disable console output completely: conf.debuglevel = 0 @@ -700,9 +706,28 @@ def preInstall(self, packages=None, groups=None): self.requiredPackages += packages self.requiredGroups = groups + # Write the langpacks config + os.makedirs(DNF_PLUGINCONF_DIR, exist_ok=True) + langs = [self.data.lang.lang] + self.data.lang.addsupport + + # Start with the file in /etc, if one exists. Otherwise make an empty config + if os.path.exists(_dnf_target_langpack_conf): + shutil.copy2(_dnf_target_langpack_conf, _dnf_installer_langpack_conf) + else: + with open(_dnf_target_langpack_conf, "w") as f: + f.write("[main]\n") + + # langpacks.conf is an INI style config file, read it and + # add or change the enabled and langpack_locales entries without + # changing anything else. + keys=[("langpack_locales", "langpack_locales=" + ", ".join(langs)), + ("enabled", "enabled=1")] + simple_replace(_dnf_installer_langpack_conf, keys) + def reset(self): super(DNFPayload, self).reset() shutil.rmtree(DNF_CACHE_DIR, ignore_errors=True) + shutil.rmtree(DNF_PLUGINCONF_DIR, ignore_errors=True) self.txID = None self._base.reset(sack=True, repos=True) @@ -847,6 +872,11 @@ def postInstall(self): except packaging.PayloadSetupError as e: log.error(e) + # Write the langpacks config to the target system + target_langpath = pyanaconda.iutil.getSysroot() + _dnf_target_langpack_conf + os.makedirs(os.path.dirname(target_langpath), exist_ok=True) + shutil.copy2(_dnf_installer_langpack_conf, target_langpath) + super(DNFPayload, self).postInstall() def writeStorageLate(self):