Poor man’s RPM cache

Poor man’s RPM cache is a script that moves rpm files to a shared location and creates a link instead. This avoids repeated downloads of the same RPM.

I am using several VMs that I install / update regularly. In an effort to avoid downloading the same rpm again for multiple servers I created this script. I’am currently testing the script with RedHat Enterprise Linux 8.10. It does not require to adjust the repository configuration, and if the shared location is unavailable it can be easily switched back to the usual download of the packages directly from the internet. Please let me know if you have issues with the script or an environment where it works. The shared location in my case is a Oracle VirtualBox shared folder (/sw) from the Windows 10 host. It should work with other shared filesystems as well (samba, NFS, shared disk device).

I tried also other solutions, but they had disadvantages for me:

  • create a local mirror with reposync: Requires downloading a lot of packages and different versions of packages that I might never need. Also we need to adjust the repository config to use the local mirror.
  • DNF local plugin: Looks promising, but I couldn’t find the software for RHEL 8.

script

You can download the script:

Or create it with copy & paste:

cat << EOF > /usr/local/bin/pmrc.sh
#!/usr/bin/bash
#
# v1.0 10.02.2025 Jochen Bandhauer (www.jbitc.de): Initial Release
# v1.1 13.02.2025 Jochen Bandhauer (www.jbitc.de): Cleanup, quit when dnf is running, performance improvement
#
# moves all local cached rpm files to a shared location and creates a symbolic link to the moved file
# package caching needs to be enabled by setting keepcache=1 in /etc/dnf/dnf.conf
#
# enable debug output: /usr/local/bin/pmrc.sh 1
#

### step 1: initialization
# set debug to 1 to enable debug output
debug=\$1
# set the path where the shared rpms should reside
spath=/sw/rpmcache
date=\`date\`

### step 2: quit when dnf is currently running
if [ -f /var/cache/dnf/metadata_lock.pid ] || [ -f /var/cache/dnf/download_lock.pid ]; then
  echo \$date 'Quitting because dnf is currently running. The lockfile is:' \`ls /var/cache/dnf/*_lock.pid\`
  exit 1
fi

### step 3: move local rpm file from the cache to the shared location
[[ \$debug -eq 1 ]] && echo \$date 'Starting step 3'
for source in \`find /var/cache/dnf/ -type f -name '*.rpm'\`; do

  # define variables
  path=\`echo \$source|cut -d "/" -f -6\`
  shortpath=\`echo \$path|cut -d "/" -f 5-\`
  filename=\`echo \$source|cut -d "/" -f 7\`

  # create destination path
  if [ ! -d \$spath/\$shortpath ]; then
    [[ \$debug -eq 1 ]] && echo \$date 'Creating path in shared location' \$spath/\$shortpath
    mkdir -p \$spath/\$shortpath
  fi

  # move rpm file if it doesn't exist
  if [ -f \$spath/\$shortpath/\$filename ]; then
    [[ \$debug -eq 1 ]] && echo \$date 'Delete rpm file from local cache:' \$path/\$filename
    rm -f \$path/\$filename
  else
    [[ \$debug -eq 1 ]] && echo \$date 'Moving rpm to shared location:' \$path/\$filename
    mv \$path/\$filename \$spath/\$shortpath/
  fi

done

### step 4: create a symbolic link in the local cache directory for all rpm files on the shared location
[[ \$debug -eq 1 ]] && echo \$date 'Starting step 4'
for sharedrpm in \`find \$spath/ -type f -name '*.rpm'\`; do

  # define variables
  tmp=\${sharedrpm#\$spath/}   # remove \$spath
  shortpath=\${tmp%/*rpm}     # remove rpm filename
  filename=\${sharedrpm##/*/} # extract rpm filename

  # create directory
  if [ ! -d /var/cache/dnf/\$shortpath ]; then
    [[ \$debug -eq 1 ]] && echo \$date 'Creating directory for link:' /var/cache/dnf/\$shortpath
    mkdir -p /var/cache/dnf/\$shortpath ]
  fi

  # create symbolic link
  if [ ! -h /var/cache/dnf/\$shortpath/\$filename ]; then
    [[ \$debug -eq 1 ]] && echo \$date 'Creating link:' /var/cache/dnf/\$shortpath/\$filename
    ln -s \$spath/\$shortpath/\$filename /var/cache/dnf/\$shortpath/\$filename
  fi

done
EOF
chmod 700 /usr/local/bin/pmrc.sh

enable dnf/yum caching and first run

# enable dnf/yum caching
sed -i '/^keepcache.*/d' /etc/dnf/dnf.conf
echo keepcache=1 >> /etc/dnf/dnf.conf
# run script for the first time with debug output on
/usr/local/bin/pmrc.sh 1
# create root cronjob (runs every 5 minutes)
if [ `crontab -l 2>/dev/null|grep /usr/local/bin/pmrc.sh|wc -l` -eq 0 ] ; then
  (crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/pmrc.sh 1>>/var/log/pmrc.log 2>&1") | crontab -
fi
Sample output of debug run:
[root@lin3 ~]# /usr/local/bin/pmrc.sh 1
Mon Feb 10 10:22:13 CET 2025 Creating link:  /var/cache/dnf/rhel-8-for-x86_64-appstream-rpms-9d3886b51bb367d7/packages/nodejs-10.24.0-1.module+el8.3.0+10166+b07ac28e.x86_64.rpm
Mon Feb 10 10:22:13 CET 2025 Creating link:  /var/cache/dnf/rhel-8-for-x86_64-appstream-rpms-9d3886b51bb367d7/packages/nodejs-full-i18n-10.24.0-1.module+el8.3.0+10166+b07ac28e.x86_64.rpm
Mon Feb 10 10:22:13 CET 2025 Creating link:  /var/cache/dnf/rhel-8-for-x86_64-appstream-rpms-9d3886b51bb367d7/packages/npm-6.14.11-1.10.24.0.1.module+el8.3.0+10166+b07ac28e.x86_64.rpm
Mon Feb 10 10:22:13 CET 2025 Creating link:  /var/cache/dnf/rhel-8-for-x86_64-appstream-rpms-9d3886b51bb367d7/packages/thunderbird-128.6.0-3.el8_10.x86_64.rpm
[root@lin3 ~]#

Useful commands

In case the shared location becomes unavailable you can clean the dnf package cache. This removes only the links in the cache and not the actual rpm files on the shared location. When running a dnf install command afterwards it will download the packages as usual from the repo URL:

# clear local package cache (removes only links, not the rpm files on the shared location)
dnf clean packages
# monitor cron runs
tail -f /var/log/cron