0

I am toubleshooting a basic .deb package built from the include script below, specifically its operation if it is installed twice via dpkg -i <package-name>. I found that the postrm script is always called.

The official maintainer scripts do not show a case for the same package being installed twice. When I install the following script twice, it will execute the postrm (or prerm if I configure the package with one) in between the preisnt and postinst.

This flow doesn't seem to make sense to me since why would installing again follow the flow:

  1. preinst
  2. prerm
  3. postrm
  4. postinst

If anything from rm would be executed I assume it would be the following:

  1. prerm
  2. postrm
  3. preinst
  4. postinst

The preinst returns an exit code 0 even the second time its executed, so that would not trigger the prerm or postrm.

My question is: Is there a way to avoid calling prerm and postrm under these circumstances? Is there a way to determine how prerm or postrm was called from within the script itself? (since it was not called during a normal install).

Here Is the script in case it is useful to see:

#!/bin/bash
# Command line parameters
USERNAME="$1"
PUBKEY_FILE="$2"
TEMP_DIR="$3"
OUTPUT_PATH=$4
MAINTAINER_EMAIL="$5"

# Verify required args
if [ -z "${USERNAME}" ] || [ -z "${PUBKEY_FILE}" ] || [ -z "${TEMP_DIR}" ] || [ -z "${OUTPUT_PATH}" ]; then
    echo "Usage: $0 USERNAME PUBKEY_FILE TEMP_DIR OUTPUT_PATH" >&2
    exit 1
fi

# Ensure maintainer email
if [ -z "${MAINTAINER_EMAIL}" ]
then
  MAINTAINER_HOSTNAME="$(hostname)"
  if [ -z "$HOSTNAME" ]
  then
    MAINTAINER_HOSTNAME="localhost"
  fi
  MAINTAINER_EMAIL="admin@${MAINTAINER_HOSTNAME}"
fi

# Constants
# Package data
PACKAGE_NAME="xyz-lsc-target-${USERNAME}"
PACKAGE_VERSION="0.5-1"
PACKAGE_NAME_VERSION="${PACKAGE_NAME}_${PACKAGE_VERSION}"
MAINTAINER="Maintainer Name  <${MAINTAINER_EMAIL}>"
PACKAGE_DATE=$(date "+%a, %d %b %Y %H:%M:%S %z")
# Used for identifying username entry in /etc/passwd
USER_COMMENT="XYZ Local Security Checks"
USER_COMMENT_GREP="XYZ\\ Local\\ Security\\ Checks"

# Paths
PACKAGE_BASE_DIR="${TEMP_DIR}/${PACKAGE_NAME_VERSION}"

# Data paths
DATA_DIR="${PACKAGE_BASE_DIR}"
HOME_SUBDIR="home/${USERNAME}"
HOME_DATA_DIR="${DATA_DIR}/${HOME_SUBDIR}"
SSH_DATA_DIR="${HOME_DATA_DIR}/.ssh"
DOC_SUBDIR="usr/share/doc/${PACKAGE_NAME}"
DOC_DATA_DIR="${DATA_DIR}/${DOC_SUBDIR}"

# Control file path
CONTROL_DIR="${PACKAGE_BASE_DIR}/DEBIAN"

#
# Test dependencies
#
if [ -z "$(which dpkg)" ]
then
  echo "dpkg not found" >&2
  exit 1
fi

if [ -z "$(which fakeroot)" ]
then
  echo "fakeroot not found" >&2
  exit 1
fi

if [ -z "$(which md5sum)" ]
then
  echo "md5sum not found" >&2
  exit 1
fi


#
# Create data files
#

# Create .ssh directory
mkdir -p "${SSH_DATA_DIR}"

# Copy public key
AUTH_KEYS_FILE="${SSH_DATA_DIR}/authorized_keys"
cp "${PUBKEY_FILE}" "${AUTH_KEYS_FILE}"

# Create doc directory
mkdir -p "${DOC_DATA_DIR}"

# Create Changelog
cd "${DOC_DATA_DIR}"
CHANGELOG_FILE="${DOC_DATA_DIR}/changelog.Debian"
{
  echo "${PACKAGE_NAME} (${PACKAGE_VERSION}) experimental; urgency=low"
  echo ""
  echo "  * Automatically generated local security check credential package"
  echo "  "
  echo ""
  echo " -- ${MAINTAINER}  ${PACKAGE_DATE}"
} > "${CHANGELOG_FILE}"

# Compress Changelog
gzip -f --best "${CHANGELOG_FILE}"
CHANGELOG_FILE="${CHANGELOG_FILE}.gz"

# Create Copyright info
COPYRIGHT_FILE="${DOC_DATA_DIR}/copyright"
{
  echo "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/"
  echo ""
  echo "Files: *"
  echo "Copyright: 2018-2020 Greenbone AG"
  echo "License: GPL-2+ (/usr/share/common-licenses/GPL-2)"
} > "${COPYRIGHT_FILE}"

# Create data archive
cd "${DATA_DIR}"
tar -C "${DATA_DIR}" -z -cf "../data.tar.gz" "${HOME_SUBDIR}" "${DOC_SUBDIR}"


#
# Create control files
#

# Create directory
mkdir -p "${CONTROL_DIR}"
chmod "0755" "${CONTROL_DIR}"

# Create "control" file
CONTROL_FILE="${CONTROL_DIR}/control"
{
  echo "Package: ${PACKAGE_NAME}"
  echo "Version: ${PACKAGE_VERSION}"
  echo "Maintainer: ${MAINTAINER}"
  echo "Priority: optional"
  echo "Architecture: all"
  echo "Description: XYZ local security check preparation"
  echo " This package prepares a system for XYZ local security checks."
  echo " A user is created with a specific SSH authorized key."
  echo " The corresponding private key is located at the respective"
  echo " XYZ installation."
} > "${CONTROL_FILE}"

# Create "preinst" file run before installation
PREINST_FILE="${CONTROL_DIR}/preinst"
touch "${PREINST_FILE}"
chmod "0755" "${PREINST_FILE}"
{
  echo "#!/bin/sh"
  echo "# Delete XYZ Local Security Checks by username and comment"
  echo "grep \"${USERNAME}.*${USER_COMMENT_GREP}\" /etc/passwd && userdel -fr ${USERNAME}"
  echo "# XYZ Local Security Checks user and create home directory"
  echo "useradd -c \"${USER_COMMENT}\" -d /home/${USERNAME} -m -s /bin/bash ${USERNAME}"
  echo "# Return exit status 0 to prevent instrm from being triggered"
  echo "# https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html"
  echo "exit 0"
} > "${PREINST_FILE}"

# Create "postinst" file run after installation
POSTINST_FILE="${CONTROL_DIR}/postinst"
touch "${POSTINST_FILE}"
chmod "0755" "${POSTINST_FILE}"
{
  echo "#!/bin/sh"
  echo "# Change file permissions"
  echo "chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}"
  echo "chmod 500 /home/${USERNAME}/.ssh"
  echo "chmod 400 /home/${USERNAME}/.ssh/authorized_keys"
  echo "exit 0"
} > "${POSTINST_FILE}"

# Create "postinst" file run after removal or on error
POSTRM_FILE="${CONTROL_DIR}/prerm"
touch "${POSTRM_FILE}"
chmod "0755" "${POSTRM_FILE}"
{
  echo "#!/bin/sh"
  echo "echo 'prerm file called!!'"
  echo "# Remove user only if it was created by this package."
  echo "# The debian package will run the postun script in case of errors"
  echo "# (e.g. user already existed)."
  echo "# Delete the user only if /etc/passwd lists content that suggests"
  echo "# that the user was created by this package."
  # echo "set -e  # abort on errors"
  echo "grep \"${USERNAME}.*${USER_COMMENT_GREP}\" /etc/passwd && userdel -fr ${USERNAME}"
} > "${POSTRM_FILE}"

# Calculate md5 checksums
MD5SUMS_FILE="${CONTROL_DIR}/md5sums"
cd "${DATA_DIR}"
{
  md5sum "${HOME_SUBDIR}/.ssh/authorized_keys"
  md5sum "${DOC_SUBDIR}/changelog.Debian.gz"
  md5sum "${DOC_SUBDIR}/copyright"
} > "${MD5SUMS_FILE}"

#
# Build package
#

# Combine into .deb file
cd "${TEMP_DIR}"
fakeroot -- dpkg --build "${PACKAGE_NAME_VERSION}" "${OUTPUT_PATH}"

2 Answers 2

2

It seems to me that instead of trying to distinguish initial installations, upgrades and repeat installations, you might want to change your postrm so that it only removes the user on purge. That way, the user remains during upgrades etc. and is only removed if you remove the package with apt purge or dpkg --purge (or equivalent).

To do this, check postrm’s first argument; if it’s purge, remove the user, otherwise do nothing. You might also want to change your preinst so that it doesn’t remove an existing user and only creates the user if it doesn’t already exist.

6
  • Although this solution seems like a good one at first, in this case, I do want the package to remove the user if errors were created during the install process. So, I would have to test that to determine if the process from a failed preinst would pass the purge argument. If it does then this would work, thanks!
    – Root James
    May 9, 2023 at 13:33
  • Given that you found the maintainer script flowcharts, I thought you would have noticed that there are specific error-handling flows. That’s how you should handle errors. May 9, 2023 at 13:52
  • Thanks, I agree with that. However, looking through the Debian documentation, especially the [Chapter 2] (debian.org/doc/manuals/debian-reference/ch02.en.html) I don't see a coherent description of the arguments used to call each script. Are you able to provide any hints as to (1) Where a comprehensive description of the process flow can be found and (2) guide to terminology related to the command arguments. The diagram helpful, but a bit lacking.
    – Root James
    May 10, 2023 at 17:31
  • While I could try echoing them to debug, it's not such a reliable method to account for what I may encounter and end up causing more problems by implementing a half-baked solution.
    – Root James
    May 10, 2023 at 17:37
  • They’re documented in Policy. May 10, 2023 at 18:01
0

It is possible to get the procedure that is being used to call the prerm or postrm from the first argument $1. In the case of installing the same version, the upgrade procedure will be used.

The diagram show the process flow for upgrade.

enter image description here

So, adding a conditional statement to the prerm or postrm file such as the one below will not execute the package removal process when executing it twice, but will also not execute it it upgrading the package.

if [ $1 != "upgrade" ]; then
  # do regular remove process
fi
1
  • The problem with the only solution I can find is that it doesn't distinguish between re-install the same package and upgrading.
    – Root James
    May 8, 2023 at 0:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .