From 75400f6a7fd221a22aeceb31eb39963660517dda Mon Sep 17 00:00:00 2001 From: Adam Williamson Date: Thu, 30 Aug 2018 11:31:55 -0700 Subject: [PATCH] Ditch all use of pyanaconda's simpleconfig lorax uses pyanaconda's SimpleConfigParser in three different places (twice with a copy that's been dumped into pylorax, once by importing it), just to do a fairly simple job: read some values out of /etc/os-release. The only value SimpleConfigParser is adding over Python's own ConfigParser here is to read a file with no section headers, and to unquote the values. The cost is either a dependency on pyanaconda, or needing to copy the whole of simpleparser plus some other utility bits from pyanaconda into lorax. This seems like a bad trade-off. This changes the approach: we copy one very simple utility function from pyanaconda (`unquote`), and do some very simple wrapping of ConfigParser to handle reading a file without any section headers, and returning unquoted values. This way we can read what we need out of os-release without needing a dep on pyanaconda or to copy lots of things from it into pylorax. Resolves: #449 Resolves: #450 Signed-off-by: Adam Williamson --- src/pylorax/api/compose.py | 15 ++- src/pylorax/api/dnfbase.py | 7 +- src/pylorax/simpleconfig.py | 203 ------------------------------------ src/pylorax/sysutils.py | 23 ++++ src/sbin/lorax | 7 +- 5 files changed, 35 insertions(+), 220 deletions(-) delete mode 100644 src/pylorax/simpleconfig.py diff --git a/src/pylorax/api/compose.py b/src/pylorax/api/compose.py index c0ed3b7d..816ea535 100644 --- a/src/pylorax/api/compose.py +++ b/src/pylorax/api/compose.py @@ -40,8 +40,6 @@ import pytoml as toml import shutil from uuid import uuid4 -from pyanaconda.simpleconfig import SimpleConfigFile - # Use pykickstart to calculate disk image size from pykickstart.parser import KickstartParser from pykickstart.version import makeVersion @@ -51,7 +49,7 @@ from pylorax.api.projects import ProjectsError from pylorax.api.recipes import read_recipe_and_id from pylorax.api.timestamp import TS_CREATED, write_timestamp from pylorax.imgutils import default_image_name -from pylorax.sysutils import joinpaths +from pylorax.sysutils import joinpaths, flatconfig def test_templates(dbo, share_dir): @@ -356,14 +354,13 @@ def start_build(cfg, dnflock, gitlock, branch, recipe_name, compose_type, test_m # Get the title, project, and release version from the host if not os.path.exists("/etc/os-release"): log.error("/etc/os-release is missing, cannot determine product or release version") - os_release = SimpleConfigFile("/etc/os-release") - os_release.read() + os_release = flatconfig("/etc/os-release") - log.debug("os_release = %s", os_release) + log.debug("os_release = %s", dict(os_release.items())) - cfg_args["title"] = os_release.get("PRETTY_NAME") - cfg_args["project"] = os_release.get("NAME") - cfg_args["releasever"] = os_release.get("VERSION_ID") + cfg_args["title"] = os_release.get("PRETTY_NAME", "") + cfg_args["project"] = os_release.get("NAME", "") + cfg_args["releasever"] = os_release.get("VERSION_ID", "") cfg_args["volid"] = "" cfg_args.update({ diff --git a/src/pylorax/api/dnfbase.py b/src/pylorax/api/dnfbase.py index df4444dc..b7a4d1c6 100644 --- a/src/pylorax/api/dnfbase.py +++ b/src/pylorax/api/dnfbase.py @@ -26,7 +26,7 @@ import os import shutil from pylorax import DEFAULT_PLATFORM_ID -from pylorax.simpleconfig import SimpleConfigFile +from pylorax.sysutils import flatconfig def get_base_object(conf): """Get the DNF object with settings from the config file @@ -76,9 +76,8 @@ def get_base_object(conf): log.warning("/etc/os-release is missing, cannot determine platform id, falling back to %s", DEFAULT_PLATFORM_ID) platform_id = DEFAULT_PLATFORM_ID else: - os_release = SimpleConfigFile("/etc/os-release") - os_release.read() - platform_id = os_release.get("PLATFORM_ID") or DEFAULT_PLATFORM_ID + os_release = flatconfig("/etc/os-release") + platform_id = os_release.get("PLATFORM_ID", DEFAULT_PLATFORM_ID) log.info("Using %s for module_platform_id", platform_id) dbc.module_platform_id = platform_id diff --git a/src/pylorax/simpleconfig.py b/src/pylorax/simpleconfig.py deleted file mode 100644 index f527c76e..00000000 --- a/src/pylorax/simpleconfig.py +++ /dev/null @@ -1,203 +0,0 @@ -# -# simpleconifg.py - representation of a simple configuration file (sh-like) -# -# 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 -# the GNU General Public License v.2, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY expressed or implied, including the implied warranties of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. You should have received a copy of the -# GNU General Public License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the -# source code or documentation are not subject to the GNU General Public -# License and may only be used or replicated with the express permission of -# Red Hat, Inc. -# -import os -import shlex -import string # pylint: disable=deprecated-module -import tempfile -from pyanaconda.core.util import upperASCII - -_SAFECHARS = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./') - -def unquote(s): - return ' '.join(shlex.split(s)) - -def quote(s, always=False): - """ If always is set it returns a quoted value - """ - if not always: - for c in s: - if c not in _SAFECHARS: - break - else: - return s - return '"' + s.replace('"', '\\"') + '"' - -def find_comment(s): - """ Look for a # comment outside of a quoted string. - If there are no quotes, find the last # in the string. - - :param str s: string to check for comment and quotes - :returns: index of comment or None - :rtype: int or None - - Handles comments inside quotes and quotes inside quotes. - """ - q = None - for i in range(len(s)): - if not q and s[i] == '#': - return i - - # Ignore quotes inside other quotes - if s[i] in "'\"": - if s[i] == q: - q = None - elif q is None: - q = s[i] - 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 - os.chmod(tmpf.name, 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. - Supports adding new keys. - Supports deleting keys. - Preserves comment, blank lines and comments on KEY lines - Does not support duplicate key entries. - """ - def __init__(self, filename=None, read_unquote=True, write_quote=True, - always_quote=False): - self.filename = filename - self.read_unquote = read_unquote - self.write_quote = write_quote - self.always_quote = always_quote - self.reset() - - def reset(self): - self._lines = [] - self.info = {} - - def read(self, filename=None): - """ passing filename will override the filename passed to init. - - save the lines into self._lines and the key/value pairs into - self.info - """ - filename = filename or self.filename - with open(filename) as f: - for line in f: - self._lines.append(line) - key, value, _comment = self._parseline(line) - if key: - self.info[key] = value - - def write(self, filename=None, use_tmp=True): - """ passing filename will override the filename passed to init. - """ - filename = filename or self.filename - if not filename: - return None - - if use_tmp: - write_tmpfile(filename, str(self)) - else: - # write directly to the file - with open(filename, "w") as fobj: - fobj.write(str(self)) - - def set(self, *args): - for key, value in args: - self.info[upperASCII(key)] = value - - def unset(self, *keys): - for key in (upperASCII(k) for k in keys): - if key in self.info: - del self.info[key] - - def get(self, key): - return self.info.get(upperASCII(key), "") - - def _parseline(self, line): - """ parse a line into a key, value and comment - - :param str line: Line to be parsed - :returns: Tuple of key, value, comment - :rtype: tuple - - Handle comments and optionally unquote quoted strings - Returns (key, value, comment) or (None, None, comment) - key is always UPPERCASE and comment may by "" if none was found. - """ - s = line.strip() - # Look for a # outside any quotes - comment = "" - comment_index = find_comment(s) - if comment_index is not None: - comment = s[comment_index:] - s = s[:comment_index] # remove from comment to EOL - - key, eq, val = s.partition('=') - key = key.strip() - val = val.strip() - if self.read_unquote: - val = unquote(val) - if key != '' and eq == '=': - return (upperASCII(key), val, comment) - else: - return (None, None, comment) - - def _kvpair(self, key, comment=""): - value = self.info[key] - if self.write_quote or self.always_quote: - value = quote(value, self.always_quote) - if comment: - comment = " " + comment - return key + '=' + value + comment + "\n" - - def __str__(self): - """ Return the file that was read, replacing existing keys with new values - removing keys that have been deleted and adding new keys. - """ - oldkeys = [] - s = "" - for line in self._lines: - key, _value, comment = self._parseline(line) - if key is None: - s += line - else: - if key not in self.info: - continue - oldkeys.append(key) - s += self._kvpair(key, comment) - - # Add new keys - for key in self.info: - if key not in oldkeys: - s += self._kvpair(key) - - return s diff --git a/src/pylorax/sysutils.py b/src/pylorax/sysutils.py index c2f46f1e..9735a7ff 100644 --- a/src/pylorax/sysutils.py +++ b/src/pylorax/sysutils.py @@ -30,6 +30,8 @@ import pwd import grp import glob import shutil +import shlex +from configparser import ConfigParser from pylorax.executils import runcmd @@ -106,3 +108,24 @@ def remove(target): def linktree(src, dst): runcmd(["/bin/cp", "-alx", src, dst]) + +def unquote(s): + return ' '.join(shlex.split(s)) + +class UnquotingConfigParser(ConfigParser): + """A ConfigParser, only with unquoting of the values.""" + def get(self, *args, **kwargs): + ret = super().get(*args, **kwargs) + if ret: + ret = unquote(ret) + return ret + +def flatconfig(filename): + """Use UnquotingConfigParser to read a flat config file (without + section headers) by adding a section header. + """ + with open (filename, 'r') as conffh: + conftext = "[main]\n" + conffh.read() + config = UnquotingConfigParser() + config.read_string(conftext) + return config['main'] diff --git a/src/sbin/lorax b/src/sbin/lorax index ce21d8f5..30b9cadc 100755 --- a/src/sbin/lorax +++ b/src/sbin/lorax @@ -35,7 +35,7 @@ import librepo import pylorax from pylorax import DRACUT_DEFAULT, DEFAULT_PLATFORM_ID from pylorax.cmdline import lorax_parser -from pylorax.simpleconfig import SimpleConfigFile +from pylorax.sysutils import flatconfig import selinux def setup_logging(opts): @@ -225,9 +225,8 @@ def get_dnf_base_object(installroot, sources, mirrorlists=None, repos=None, log.warning("/etc/os-release is missing, cannot determine platform id, falling back to %s", DEFAULT_PLATFORM_ID) platform_id = DEFAULT_PLATFORM_ID else: - os_release = SimpleConfigFile("/etc/os-release") - os_release.read() - platform_id = os_release.get("PLATFORM_ID") or DEFAULT_PLATFORM_ID + os_release = flatconfig("/etc/os-release") + platform_id = os_release.get("PLATFORM_ID", DEFAULT_PLATFORM_ID) log.info("Using %s for module_platform_id", platform_id) conf.module_platform_id = platform_id