Monday, April 22, 2013

Dynamic /etc/hosts with a simple template engine

This is a pretty niche script that probably won't do much good to anybody else.. but just in case I'm pushing it down the intertubes. I find myself editing blocks of domains in my hosts file regularly so I finally decided to save a few seconds everyday and create a template engine for my hosts file where I can now use a script to change the IP for blocks of virtual hosts all in a single command. The syntax is short and simple:
# <pool_a>
127.0.0.1 vhost-1.service_a.com service_a
127.0.0.1 vhost-2.service_a.com
# </pool_a>

# <pool_b>
192.168.0.1 vhost-1.service_b.com service_b
192.168.0.1 vhost-2.service_b.com
# </pool_b>
It uses an html-like tag inside of a bash comment for opening and closing the "blocks" of virtual hosts. Then comes the script which now reads and modifies my hosts files using these "templates":
#!/bin/bash

HOSTS_FILE='/etc/hosts';

function update_dyn_block() {
  local block_name="${1}";
  local ip_address="${2}";

  # sanity checks, only one named template block allowed:
  if [ $(cat ${HOSTS_FILE} | grep "# <${block_name}>" 2>/dev/null | wc -l) -ne 1 ]; then
    echo -n "missing or duplicate named template opening-tag found in ";
    echo "${HOSTS_FILE}: <${block_name}>";
    return 1;
  fi

  if [ $(cat ${HOSTS_FILE} | grep "# </${block_name}>" 2>/dev/null | wc -l) -ne 1 ]; then
    echo -n "missing or duplicate named template closing-tag found in ";
    echo "${HOSTS_FILE}: </${block_name}>";
    return 1;
  fi

  # get the line numbers of the line numbers of the template opening and closing tags:
  local opening_tag_at=$(cat ${HOSTS_FILE} | grep -n "# <${block_name}>" | cut -d: -f1);
  local closing_tag_at=$(cat ${HOSTS_FILE} | grep -n "# </${block_name}>" | cut -d: -f1);

  # echo "template block found between lines: ${opening_tag_at} and ${closing_tag_at}";

  local temp_file=$(mktemp);

  # ...
  cat ${HOSTS_FILE} | awk "
  {
    if ( NR > ${opening_tag_at} && NR < ${closing_tag_at} && NF >= 2 ) {
      printf \"%s\", \"${ip_address}\";
      for ( i = 2; i <= NF; i++ ) {
        printf \" %s\", \$i;
      }
      printf \"\n\";
    } else {
      print;
    }
  }" > ${temp_file};

  diff ${HOSTS_FILE} ${temp_file};

  mv ${temp_file} ${HOSTS_FILE};
}

if [ "${1}" = "" -o "${2}" = "" ]; then
  echo "usage: $0 <block_name> <new_ip_address>";
  echo;
  echo "inside of your hosts file, use 'tags' to define a block of entries with this syntax:";
  echo;
  echo "# <tag-name>";
  echo "127.0.0.1 host-1.hostname.com";
  echo "127.0.0.1 host-2.hostname.com";
  echo "# </tag-name>";
  echo;
  echo -n "note: the script is picky about the space between the hashtag and the '<' ";
  echo "character of the tag -- dont forget the space";
  exit 1;
else
  update_dyn_block "${1}" "${2}";
fi

And voila, now I can just call the script with the parameters "block name" and "new ip address" and my hosts file will be updated using the same template and print a diff showing the changes that were made to the hosts file. Here's a complete demo with the HOSTS_FILE=/tmp/hosts set in the change_hosts script:
[root@localhost requester]# cat /tmp/hosts
# <pool_a>
127.0.0.1 vhost-1.service_a.com service_a
127.0.0.1 vhost-2.service_a.com
# </pool_a>

# <pool_b>
192.168.0.1 vhost-1.service_b.com service_b
192.168.0.1 vhost-2.service_b.com
# </pool_b>
[root@localhost requester]# ~/bin/change_hosts pool_a 127.0.0.2
2,3c2,3
< 127.0.0.1 vhost-1.service_a.com service_a
< 127.0.0.1 vhost-2.service_a.com
---
> 127.0.0.2 vhost-1.service_a.com service_a
> 127.0.0.2 vhost-2.service_a.com
[root@localhost requester]# ~/bin/change_hosts pool_b 198.168.0.2
7,8c7,8
< 192.168.0.1 vhost-1.service_b.com service_b
< 192.168.0.1 vhost-2.service_b.com
---
> 198.168.0.2 vhost-1.service_b.com service_b
> 198.168.0.2 vhost-2.service_b.com
[root@localhost requester]# cat /tmp/hosts
# <pool_a>
127.0.0.2 vhost-1.service_a.com service_a
127.0.0.2 vhost-2.service_a.com
# </pool_a>

# <pool_b>
198.168.0.2 vhost-1.service_b.com service_b
198.168.0.2 vhost-2.service_b.com
# </pool_b>
Cheers