From 6688eeef7aed33fd5328fb2f124b8689b82f4f55 Mon Sep 17 00:00:00 2001 From: Shivam Mehta Date: Thu, 4 Dec 2025 21:33:32 -0700 Subject: [PATCH 1/2] Allow setting additional DNF arguments Signed-off-by: Shivam Mehta --- src/arguments.py | 1 + src/image-build | 1 + src/installer.py | 114 ++++++++++++++++++++++++++++++++++++----------- src/layer.py | 50 ++++++++++----------- 4 files changed, 115 insertions(+), 51 deletions(-) diff --git a/src/arguments.py b/src/arguments.py index 82e23ae..85bb37f 100644 --- a/src/arguments.py +++ b/src/arguments.py @@ -69,6 +69,7 @@ def process_args(terminal_args, config_options): processed_args['scap_benchmark'] = terminal_args.scap_benchmark or config_options.get('scap_benchmark', False) processed_args['oval_eval'] = terminal_args.oval_eval or config_options.get('oval_eval', False) processed_args['install_scap'] = terminal_args.install_scap or config_options.get('install_scap', False) + processed_args['dnf_options'] = terminal_args.dnf_options or config_options.get('dnf_options', []) # If no publish options were passed in either the CLI or the config file, store locally. if not (processed_args['publish_s3'] diff --git a/src/image-build b/src/image-build index da50852..9188901 100755 --- a/src/image-build +++ b/src/image-build @@ -43,6 +43,7 @@ def main(): parser.add_argument('--scap-benchmark', dest="scap_benchmark", action='store_true', required=False) parser.add_argument('--oval-eval', dest="oval_eval", action='store_true', required=False) parser.add_argument('--install-scap', dest="install_scap", action='store_true', required=False) + parser.add_argument('--dnf-options', dest="dnf_options", action='store', nargs='+', type=str, default=[], required=False, help='List of dnf options') try: diff --git a/src/installer.py b/src/installer.py index e287945..376fd1d 100644 --- a/src/installer.py +++ b/src/installer.py @@ -3,6 +3,7 @@ import os import pathmod import tempfile +import sys # Written Modules from utils import cmd @@ -22,7 +23,7 @@ def __init__(self, pkg_man, cname, mname, gpgcheck=True): # DNF complains if the log directory is not present os.makedirs(os.path.join(self.tdir, "dnf/log")) - def install_scratch_repos(self, repos, repo_dest, proxy): + def install_scratch_repos(self, repos, repo_dest, proxy, dnf_options): # check if there are repos passed for install if len(repos) == 0: logging.info("REPOS: no repos passed to install\n") @@ -45,17 +46,24 @@ def install_scratch_repos(self, repos, repo_dest, proxy): args.append(r['url']) args.append(r['alias']) elif self.pkg_man == "dnf": - args.append("--setopt=reposdir="+os.path.join(self.mname, pathmod.sep_strip(repo_dest))) - args.append("--setopt=logdir="+os.path.join(self.tdir, self.pkg_man, "log")) - args.append("--setopt=cachedir="+os.path.join(self.tdir, self.pkg_man, "cache")) + defaults = { + "reposdir": os.path.join(self.mname, pathmod.sep_strip(repo_dest)), + "logdir": os.path.join(self.tdir, self.pkg_man, "log"), + "cachedir": os.path.join(self.tdir, self.pkg_man, "cache") + } if proxy != "": - args.append("--setopt=proxy="+proxy) + defaults["proxy"] = proxy + config_path = self.generate_config(defaults, dnf_options) + + args.append(f"-c={config_path}") args.append("config-manager") args.append("--save") args.append("--add-repo") args.append(r['url']) rc = cmd([self.pkg_man] + args) + # Remove Temp config + os.remove(config_path) if rc != 0: raise Exception("Failed to install repo", r['alias'], r['url']) @@ -66,16 +74,25 @@ def install_scratch_repos(self, repos, repo_dest, proxy): repo_name = r['url'].split('https://')[1].replace('/','_') elif r['url'].startswith('http'): repo_name = r['url'].split('http://')[1].replace('/','_') + + defaults = { + "reposdir": os.path.join(self.mname, pathmod.sep_strip(repo_dest)), + "logdir": os.path.join(self.tdir, self.pkg_man, "log"), + "cachedir": os.path.join(self.tdir, self.pkg_man, "cache") + } + if proxy != "": + defaults["proxy"] = proxy + config_path = self.generate_config(defaults, dnf_options) + args = [] args.append('config-manager') args.append('--save') - args.append("--setopt=reposdir="+os.path.join(self.mname, pathmod.sep_strip(repo_dest))) - args.append("--setopt=logdir="+os.path.join(self.tdir, self.pkg_man, "log")) - args.append("--setopt=cachedir="+os.path.join(self.tdir, self.pkg_man, "cache")) - args.append('--setopt=*.proxy='+proxy) + args.append(f"-c={config_path}") args.append(repo_name) rc = cmd([self.pkg_man] + args) + # Remove Temp config + os.remove(config_path) if rc != 0: raise Exception("Failed to set proxy for repo", r['alias'], r['url'], proxy) @@ -93,7 +110,7 @@ def install_scratch_repos(self, repos, repo_dest, proxy): if rc != 0: raise Exception("Failed to install gpg key for", r['alias'], "at URL", r['gpg']) - def install_scratch_packages(self, packages, registry_loc, proxy): + def install_scratch_packages(self, packages, registry_loc, proxy, dnf_options): # check if there are packages to install if len(packages) == 0: logging.warn("PACKAGES: no packages passed to install\n") @@ -116,11 +133,17 @@ def install_scratch_packages(self, packages, registry_loc, proxy): args.append("-l") args.extend(packages) elif self.pkg_man == "dnf": - args.append("--setopt=reposdir="+os.path.join(self.mname, pathmod.sep_strip(registry_loc))) - args.append("--setopt=logdir="+os.path.join(self.tdir, self.pkg_man, "log")) - args.append("--setopt=cachedir="+os.path.join(self.tdir, self.pkg_man, "cache")) + + defaults = { + "reposdir": os.path.join(self.mname, pathmod.sep_strip(registry_loc)), + "logdir": os.path.join(self.tdir, self.pkg_man, "log"), + "cachedir": os.path.join(self.tdir, self.pkg_man, "cache") + } if proxy != "": - args.append("--setopt=proxy="+proxy) + defaults["proxy"] = proxy + config_path = self.generate_config(defaults, dnf_options) + + args.append(f"-c={config_path}") args.append("install") args.append("-y") args.append("--nogpgcheck") @@ -129,13 +152,15 @@ def install_scratch_packages(self, packages, registry_loc, proxy): args.extend(packages) rc = cmd([self.pkg_man] + args) + # Remove Temp config + os.remove(config_path) if rc == 104: raise Exception("Installing base packages failed") if rc == 107: logging.warn("one or more RPM postscripts failed to run") - def install_scratch_package_groups(self, package_groups, registry_loc, proxy): + def install_scratch_package_groups(self, package_groups, registry_loc, proxy, dnf_options): # check if there are packages groups to install if len(package_groups) == 0: logging.warn("PACKAGE GROUPS: no package groups passed to install\n") @@ -148,11 +173,16 @@ def install_scratch_package_groups(self, package_groups, registry_loc, proxy): if self.pkg_man == "zypper": logging.warn("zypper does not support package groups") elif self.pkg_man == "dnf": - args.append("--setopt=reposdir="+os.path.join(self.mname, pathmod.sep_strip(registry_loc))) - args.append("--setopt=logdir="+os.path.join(self.tdir, self.pkg_man, "log")) - args.append("--setopt=cachedir="+os.path.join(self.tdir, self.pkg_man, "cache")) + defaults = { + "reposdir": os.path.join(self.mname, pathmod.sep_strip(registry_loc)), + "logdir": os.path.join(self.tdir, self.pkg_man, "log"), + "cachedir": os.path.join(self.tdir, self.pkg_man, "cache") + } if proxy != "": - args.append("--setopt=proxy="+proxy) + defaults["proxy"] = proxy + config_path = self.generate_config(defaults, dnf_options) + + args.append(f"-c={config_path}") args.append("groupinstall") args.append("-y") args.append("--nogpgcheck") @@ -161,10 +191,12 @@ def install_scratch_package_groups(self, package_groups, registry_loc, proxy): args.extend(package_groups) rc = cmd([self.pkg_man] + args) + # Remove Temp config + os.remove(config_path) if rc == 104: raise Exception("Installing base packages failed") - - def install_scratch_modules(self, modules, registry_loc, proxy): + + def install_scratch_modules(self, modules, registry_loc, proxy, dnf_options): # check if there are modules groups to install if len(modules) == 0: logging.warn("PACKAGE MODULES: no modules passed to install\n") @@ -178,11 +210,16 @@ def install_scratch_modules(self, modules, registry_loc, proxy): logging.warn("zypper does not support package groups") return elif self.pkg_man == "dnf": - args.append("--setopt=reposdir="+os.path.join(self.mname, pathmod.sep_strip(registry_loc))) - args.append("--setopt=logdir="+os.path.join(self.tdir, self.pkg_man, "log")) - args.append("--setopt=cachedir="+os.path.join(self.tdir, self.pkg_man, "cache")) + defaults = { + "reposdir": os.path.join(self.mname, pathmod.sep_strip(registry_loc)), + "logdir": os.path.join(self.tdir, self.pkg_man, "log"), + "cachedir": os.path.join(self.tdir, self.pkg_man, "cache") + } if proxy != "": - args.append("--setopt=proxy="+proxy) + defaults["proxy"] = proxy + config_path = self.generate_config(defaults, dnf_options) + + args.append(f"-c={config_path}") args.append("module") args.append(mod_cmd) args.append("-y") @@ -191,10 +228,12 @@ def install_scratch_modules(self, modules, registry_loc, proxy): args.append(self.mname) args.extend(mod_list) rc = cmd([self.pkg_man] + args) + # Remove Temp config + os.remove(config_path) if rc != 0: raise Exception("Failed to run module cmd", mod_cmd, ' '.join(mod_list)) - def install_repos(self, repos, proxy): + def install_repos(self, repos, proxy, dnf_options): # check if there are repos passed for install if len(repos) == 0: logging.info("REPOS: no repos passed to install\n") @@ -321,3 +360,26 @@ def install_copyfiles(self, copyfiles): logging.info(f['src'] + ' -> ' + f['dest']) args += [ self.cname, f['src'], f['dest'] ] cmd(["buildah","copy"] + args) + + def generate_config(self, defaults, dnf_opts): + _, config_path = tempfile.mkstemp(prefix='temp-dnf-conf-') + dnf_opt_dict = {} + + for option in dnf_opts: + key, value = option.split('=') + dnf_opt_dict[key] = value + + try: + file = open(config_path, 'w') + file.write('[main]\n') + for key in dnf_opt_dict: + file.write(f'{key}={dnf_opt_dict[key]}\n') + + for key in defaults: + if key not in dnf_opt_dict.keys(): + file.write(f'{key}={defaults[key]}\n') + file.close() + except Exception as e: + logging.error(f"Could not create a temporary dnf config file: {e}") + + return config_path \ No newline at end of file diff --git a/src/layer.py b/src/layer.py index df02c14..652366d 100644 --- a/src/layer.py +++ b/src/layer.py @@ -33,6 +33,8 @@ def _build_base(self, repos, modules, packages, package_groups, remove_packages, else: proxy = "" + dnf_options = self.args['dnf_options'] + # container and mount name def buildah_handler(line): out.append(line) @@ -54,7 +56,12 @@ def buildah_handler(line): if package_manager == "zypper": repo_dest = "/etc/zypp/repos.d" elif package_manager == "dnf": + # When "minimal install" group is installed, it stores .repo files in the default /etc/yum.repos.d location + # this sometimes creates an issue where dnf will start using the latest versions of packages instead of the + # ones defined in the config file. Adding a different repo location to dnf.conf fixes that issue since + # /etc/yum.repos.d is hard coded by packages in "minimal install" group. repo_dest = os.path.expanduser("~/.pkg_repos/yum.repos.d") + # Create repo dest, if needed os.makedirs(os.path.join(mname, pathmod.sep_strip(repo_dest)), exist_ok=True) @@ -64,27 +71,20 @@ def buildah_handler(line): os.mknod(os.path.join(mname, "etc/dnf/dnf.conf"), mode=0o644) # Add repo directory path to dnf.conf, if needed - # Collect the contents of the file - dnf_conf = open(os.path.join(mname, "etc/dnf/dnf.conf"), "r") - dnf_conf_contents = dnf_conf.readlines() - dnf_conf.close() - - ## If repodir line does not exists, add it - if not str("reposdir=" + repo_dest + "\n") in dnf_conf_contents: - ## If there is "[main]" section, add just the repodir - dnf_conf = open(os.path.join(mname, "etc/dnf/dnf.conf"), "a") - line_not_found = True - for line in dnf_conf_contents: - if "[main]\n" == line: - line = line + "reposdir=" + repo_dest + "\n" - line_not_found = False - dnf_conf.write(line) - break - ## Otherwise, add "[main]" and reposdir line - if line_not_found: - dnf_conf.write("[main]\n" + "reposdir=" + repo_dest + "\n") - + try: + # Collect the contents of the file + dnf_conf = open(os.path.join(mname, "etc/dnf/dnf.conf"), "a+") + dnf_conf_contents = dnf_conf.readlines() + if not str("reposdir=" + repo_dest + "\n") in dnf_conf_contents: + ## If there is "[main]" section, add just the repodir + if "[main]\n" in dnf_conf_contents: + dnf_conf.write("reposdir=" + repo_dest + "\n") + ## Otherwise, add "[main]" and reposdir line + else: + dnf_conf.write("[main]\n" + "reposdir=" + repo_dest + "\n") dnf_conf.close() + except Exception as e: + logging.error("Could not update the contents of dnf.conf: {e}") else: self.logger.error("unsupported package manager") @@ -104,9 +104,9 @@ def buildah_handler(line): # Install Repos try: if parent == "scratch": - inst.install_scratch_repos(repos, repo_dest, proxy) + inst.install_scratch_repos(repos, repo_dest, proxy, dnf_options) else: - inst.install_repos(repos, proxy) + inst.install_repos(repos, proxy, dnf_options) except Exception as e: self.logger.error(f"Error installing repos: {e}") cmd(["buildah","rm"] + [cname]) @@ -120,11 +120,11 @@ def buildah_handler(line): try: if parent == "scratch": # Enable modules - inst.install_scratch_modules(modules, repo_dest, self.args['proxy']) + inst.install_scratch_modules(modules, repo_dest, proxy, dnf_options) # Base Package Groups - inst.install_scratch_package_groups(package_groups, repo_dest, proxy) + inst.install_scratch_package_groups(package_groups, repo_dest, proxy, dnf_options) # Packages - inst.install_scratch_packages(packages, repo_dest, proxy) + inst.install_scratch_packages(packages, repo_dest, proxy, dnf_options) else: inst.install_package_groups(package_groups) inst.install_packages(packages) From 544a7758f65b9d08192183354e37041263822227 Mon Sep 17 00:00:00 2001 From: Shivam Mehta Date: Wed, 7 Jan 2026 14:21:01 -0700 Subject: [PATCH 2/2] add dnf_options to readme Signed-off-by: Shivam Mehta --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index cf42755..c4a334b 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,10 @@ options: # Distribution flavor of image. pkg_manager: 'dnf' + # DNF options to use + dnf_options: + - 'gpgcheck=0' + - 'keepcache=1' # Starting filesystem of image. 'scratch' means to start with a blank # filesystem. Currently, only OCI images can be used as parents. In