diff --git a/src/pylorax/__init__.py b/src/pylorax/__init__.py index c70e959f..8020f839 100644 --- a/src/pylorax/__init__.py +++ b/src/pylorax/__init__.py @@ -64,6 +64,9 @@ DRACUT_DEFAULT = ["--xz", "--install", "/.buildstamp", "--no-early-microcode", " REMOVE_PPC64_DRIVERS = "floppy scsi_debug nouveau radeon cirrus mgag200" REMOVE_PPC64_MODULES = "drm plymouth" +# Used for DNF conf.module_platform_id +DEFAULT_PLATFORM_ID = "platform:f30" + class ArchData(DataHolder): lib64_arches = ("x86_64", "ppc64", "ppc64le", "s390x", "ia64", "aarch64") bcj_arch = dict(i386="x86", x86_64="x86", diff --git a/src/pylorax/api/dnfbase.py b/src/pylorax/api/dnfbase.py index c551ccad..df4444dc 100644 --- a/src/pylorax/api/dnfbase.py +++ b/src/pylorax/api/dnfbase.py @@ -25,6 +25,8 @@ from glob import glob import os import shutil +from pylorax import DEFAULT_PLATFORM_ID +from pylorax.simpleconfig import SimpleConfigFile def get_base_object(conf): """Get the DNF object with settings from the config file @@ -69,6 +71,17 @@ def get_base_object(conf): log.info("releasever = %s", _releasever) dbc.releasever = _releasever + # DNF 3.2 needs to have module_platform_id set, otherwise depsolve won't work correctly + if not os.path.exists("/etc/os-release"): + 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 + log.info("Using %s for module_platform_id", platform_id) + dbc.module_platform_id = platform_id + # write the dnf configuration file with open(dnfconf, "w") as f: f.write(dbc.dump()) diff --git a/src/pylorax/simpleconfig.py b/src/pylorax/simpleconfig.py new file mode 100644 index 00000000..f527c76e --- /dev/null +++ b/src/pylorax/simpleconfig.py @@ -0,0 +1,203 @@ +# +# 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/sbin/lorax b/src/sbin/lorax index 6f2f716c..ce21d8f5 100755 --- a/src/sbin/lorax +++ b/src/sbin/lorax @@ -33,8 +33,9 @@ import dnf import dnf.logging import librepo import pylorax -from pylorax import DRACUT_DEFAULT +from pylorax import DRACUT_DEFAULT, DEFAULT_PLATFORM_ID from pylorax.cmdline import lorax_parser +from pylorax.simpleconfig import SimpleConfigFile import selinux def setup_logging(opts): @@ -219,6 +220,17 @@ def get_dnf_base_object(installroot, sources, mirrorlists=None, repos=None, if sslverify == False: conf.sslverify = False + # DNF 3.2 needs to have module_platform_id set, otherwise depsolve won't work correctly + if not os.path.exists("/etc/os-release"): + 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 + log.info("Using %s for module_platform_id", platform_id) + conf.module_platform_id = platform_id + # Add .repo files if repos: reposdir = os.path.join(tempdir, "dnf.repos")