ISC DHCP Config Git Hooks - Pre-deploy Sanity Check

ISC DHCP Config Git Hooks - Pre-deploy Sanity Check

Below are some git hooks I have written to ease deployment of ISC DHCP configs to live environments. These will ensure that dhcpd breaking config will never be deployed to your live environment and give you the basic history and versioning that comes with git. Branching is not supported.

The following works on CentOS 7. Minor adjustments are probably needed for Debian based distros.

Background

I have written these git hooks for an environment where the configuration of new dhcp scopes are added manually. I needed a way to ensure the configuration was validated by dhcpd before it was deployed. The following setup will ensure this is the case.

Preparation

These packages are needed:

Misc:

Setting up the environment

Create group used for git:

groupadd gitdhcp

Create user(s) and add user to gitdhcp group:

useradd -s /bin/bash -G gitdhcp -d /home/someuser -m -c"Someuser info" someuser

Create sudoers file for gitdhcp users:

visudo -f /etc/sudoers.d/0300gitdhcp

Add this content to sudoers file:

Host_Alias  GITDHCP_HOSTS = hostname_please_change
Runas_Alias GITDHCP_RUNAS = root
Cmnd_Alias  GITDHCP_CMNDS = /bin/chgrp, /bin/cp, /usr/bin/git, /bin/systemctl restart dhcpd

Defaults!GITDHCP_CMNDS env_keep += "GIT_WORK_TREE", !requiretty

%%gitdhcp GITDHCP_HOSTS = (GITDHCP_RUNAS) NOPASSWD: GITDHCP_CMNDS

Create shared git repo (place it where you see fit, here I will use /opt and consider using a more descriptive name than dhcp_config):

cd /opt && git init --bare dhcp_config.git

Fix ownership and rights:

chown -R root:gitdhcp dhcp_config.git
chmod -R g+rw dhcp_config.git
chmod g+s $(find dhcp_config.git -type d)

The Hooks

Both of the hooks below should be placed in your newly created shared git repo in hooks folder.

cd /opt/dhcp_config.git/hooks
touch {pre,post}-receive
chmod +x {pre,post}-receive

Pre-receive

The pre-receive hook check the dhcp config separately from the running instance of dhcpd and rejects the commit all-together if the sanity check fails.

Paste the following in to /opt/dhcp_config.git/hooks/pre-receive:

#!/bin/bash
# Terminate immediately on non zero exit status. Don't allow empty variables. 
set -euo pipefail

# Set exit message on fail.
trap '[ "$?" -eq 0 ] || echo -en "\E[1;31mDeployment failed! See error message on line above.\E[m"' EXIT

# Environment specific variables. Please adjust or add if needed. Set your include path here. The one you defined in your dhcpd.conf or elsewhere. This is essential.
dhcpd_conf_path='/etc/dhcp'
include_path='REMEMBER TO ADD A PATH HERE'
git_group='gitdhcp'

# tmpdir var must include $HOME
tmpdir="$HOME/dhcptest/"

# Exit if dhcpd is not running
if ! systemctl status dhcpd; then
  printf '\e[1;31m%-6s\e[m\n' "Rejecting commit. Dhcpd is stopped for some reason."
  exit 1
fi

# Make tmpdir in users homedir
if [[ ! -d "$tmpdir" ]]; then
  mkdir "$tmpdir"
fi

# Transfer commit to tmpdir. Remember to add extra files if you added them above.
while read -r oldrev newrev refname; do

  if [[ $refname = "refs/heads/master" ]] ; then
    git archive "$newrev" | tar -x -C "$tmpdir"
    sudo cp "$dhcpd_conf_path"/dhcpd.conf "$tmpdir"/dhcpd.conf
    sudo chgrp "$git_group" "$tmpdir"/dhcpd.conf
  else
    printf '\e[1;31m%-6s\e[m\n' "Copy to test failed."
  fi

done

# Fix paths for testing
if ! find "$tmpdir" -type f -exec sed -i s="$include_path"="$tmpdir"=g {} \; ; then
  printf '\e[1;31m%-6s\e[m\n' "Path fixing in $tmpdir failed. Exiting."
  exit 1
fi

# Test dhcpd config in tmpdir
printf '\e[1;32m%-6s\e[m\n' "Testing dhcpd.conf outside of running config."
if ! /sbin/dhcpd -t -cf "$tmpdir"dhcpd.conf; then
  printf '\e[1;31m%-6s\e[m\n' "Sanity check failed! Fix the config."
  exit 1
else
  printf '\e[1;32m%-6s\e[m\n' "Dhcpd sanity check passed! Applying config."
fi

# Committing changes to live environment
# post-receive script will be called now

# Cleaning up
rm -rf "$tmpdir"

If you have files (like static ip configurations) generated by provisioning, remember to add these to .gitignore and to add checks for them to the pre-receive hook.

Post-receive

Below is the post-receive hook, which checkouts the updated repo to the GIT_WORK_TREE destination. Please read the comments.

Paste the following in to /opt/dhcp_config.git/hooks/post-receive:

#!/bin/bash
# Terminate immediately on non zero exit status. Don't allow empty variables
set -euo pipefail

# Set exit message on fail.
trap '[ "$?" -eq 0 ] || echo -en "\E[1;31mDeployment failed! See error message on line above.\E[m"' EXIT

# Environment specific directory variable. Please adjust. This is where configuration will be deployed to.
export GIT_WORK_TREE='REMEMBER TO ADD A PATH HERE!'

# Store commit message. For check below.
message="$(git --git-dir "$PWD" log -1 HEAD --pretty=format:%s)"

# @OVERRIDE in the commit message overrides if someone edited files outside git and forcibly checks out to GIT_WORK_TREE
if ! git diff --quiet; then
  if echo $message | grep -q '@OVERRIDE'; then
    git diff
    printf '\e[1;31m%-6s\e[m\n' "Overriding local changes as requested."
  else 
    printf '\e[1;31m%-6s\e[m\n' "Someone edited files outside git - never do this. Will not continue, please merge local changes to git. Override by including @OVERRIDE in commit message - this will overwrite all non-git edits!"
    exit 1
  fi
fi

# Untracked files git ls-files
nongit_files=$(git ls-files --others --exclude-standard || true)

if [[ -n $nongit_files ]]; then
  if echo $message | grep -q '@OVERRIDE'; then
    printf '\e[1;31m%-6s\e[m\n' "Nuking local files as requested."
  else
    printf '\e[1;31m%-6s\e[m\n' "Someone added the files below outside git. Untracked files are not accepted. Exiting before checkout to production. Delete or move files to git"
    echo $nongit_files
    exit 1
  fi
fi

if sudo git checkout -f; then
  printf '\e[1;32m%-6s\e[m\n' "Checkout successful!"
else
  printf '\e[1;31m%-6s\e[m\n' "Checkout failed."
fi

if sudo systemctl restart dhcpd; then
  printf '\e[1;32m%-6s\e[m\n' "Dhcpd restart successful."
else
  printf '\e[1;31m%-6s\e[m\n' "Dhcpd restart failed. Obviously."
fi

Initial commit and deployment

Now migrate any existing dhcp configs to the shared git repository and do the initial commit. Remember to use a user who is member of the gitdhcp group on the dhcp-server:

git clone dhcp-server:/opt/dhcp_config
cd dhcp_config
scp -r dhcp-server:/path/to/dhcp/configs/* .
git add ./*
git commit -am "Initial commit. @OVERRIDE"
git push

The @OVERRIDE string is used to overwrite existing configs already at the GIT_WORK_TREE destination. See post-receive hook for more info. The @OVERRIDE string is usually only needed for the initial commit and for those rare occasions where someone has edited the configurations outside of git.

The output from the remote hooks, should look like this:

Git remote output