Simple Config Templating

While cloud-based servers are quickly eliminating many organizations' needs for clusters it's not yet a viable option in China where the organization this code was written for is based.

Here, in the deployment of a small cluster, simple templating of config files was used to keep the process automated and simple.

This sample of code shows how this templating was used with nginx files. In order to add a server to the cluster all that is required is to add it's public and private IPs to the IP lists in cluster_management. The next standard deployment will begin to use the new server.

cluster_managment.py

- Server IP data & helper functions.
 '''
Created on Nov 19, 2013

@author: jivan
@brief Provides functions and settings specifically for our Ubuntu-12.04 cluster.
'''
from __future__ import unicode_literals
import subprocess
import re

# --- List all production django server IPs here.
# External IPs for each django server
django_server_ips = ['-removed-', '-removed-', '-removed-']
# Private IPs (assigned to 2nd network interface which is connected to our private switch)
django_server_private_ips = ['192.168.0.101', '192.168.0.102', '192.168.0.103']
# Weights to use in round-robin distribution of requests from nginx.
django_server_weights = [3, 5, 5]

# --- List all production postgres servers here.  The first one should be the writable server.
# External IPs for each postgres server
postgres_server_ips = ['-removed-', '-removed-']
# Private IPs (assigned to 2nd network interface which is connected to our private switch)
postgres_server_private_ips = ['192.168.0.201', '192.168.0.202']


def current_machine_ips():
    """ @brief: Returns all IPv4 addresses listed by 'ifconfig' command except for 127.0.0.1.
        @warn: This is very Linux-specific, making use of the 'ifconfig' command.
    """
    try:
        ifconfig_output = subprocess.check_output(['ifconfig'])
    except subprocess.CalledProcessError as ex:
        raise ex

    ips = re.findall(r'inet addr:(\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3})', ifconfig_output)
    while '127.0.0.1' in ips:
        ips.remove('127.0.0.1')

    return ips


def get_server_hostname(test_ips=None):
    """ @brief: Returns the name to use for the host this is run on.
        @author: Jivan
        @since: 2014-03-03
        @return: String name for server or None.

        If the server's IP is found in django_server_ips or postgres_server_ips,
        the name is returned as django- or postgres- where  is the position
        in the list.
        For example, if one of the current server's ip addresses matches django_server_ips[2]
        the name returned would be django-3.
        If none of the server's IP addresses are found in either list, None is returned.
    """
    if test_ips:
        this_machines_ips = test_ips
    else:
        this_machines_ips = current_machine_ips()

    hostname = None
    if set(this_machines_ips).intersection(django_server_ips):
        for tmip in this_machines_ips:
            try:
                index_of_server = django_server_ips.index(tmip)
                hostname = 'django-{}'.format(index_of_server + 1)
                break
            except ValueError:
                pass
    elif set(this_machines_ips).intersection(postgres_server_ips):
        for tmip in this_machines_ips:
            try:
                index_of_server = postgres_server_ips.index(tmip)
                hostname = 'postgres-{}'.format(index_of_server + 1)
                break
            except ValueError:
                pass

    return hostname


def get_server_definitions():
    """ @brief: Returns the nginx upstream server definitions for gunicorn instances on each server.
        @author: Jivan
        @since: 2014-02-04
    """
    # Server definitions
    sds = []
    # Template for each server definition
    sdt = '    server {ip}:8000    weight={weight};'

    # Private ip & weight
    for pip, w in zip(django_server_private_ips, django_server_weights):
        sd = sdt.format(ip=pip, weight=w)
        sds.append(sd)

    sd_text = '\n'.join(sds)
    return sd_text

fabfile_snippet.py

- Piece of fabric file where nginx templates are used to generate config files.
    with hide('output'):
        start_step('generating nginx config files from templates', step_number)
        # Nginx config templates located at dowant/sysadmin/nginx/*.template
        nginx_config_template_names = glob.glob(os.path.join(
            os.path.dirname(__file__),
            '../nginx/*.template'
        ))
        remote_nginx_config_folder = os.path.join(
            server_tree_location,
            'sysadmin/nginx/'
        )
        for nctn in nginx_config_template_names:
            with open(nctn, 'r') as template_file:
                # Template content
                tc = template_file.read()
            sds = get_server_definitions()

            # Generate config content
            try:
                # Config content
                cc = tc.format(server_definitions=sds)
            except KeyError as ke:
                fastprint("Error: formatting value {} not passed to template '{}'\n".format(ke, nctn))
                continue

            # Nginx config name
            ncn = nctn.replace('.template', '')
            ncn_name = ntpath.basename(ncn)
            destination_path = os.path.join(remote_nginx_config_folder, ncn_name)
            with NamedTemporaryFile() as config_file:
                config_file.write(cc)
                config_file.flush()
                put(config_file.name, destination_path, use_sudo=True)
        step_number = stop_step(step_number)

nginx.conf.template

- Stripped-down nginx config template.
upstream deliveryhero_gunicorn {{
{server_definitions}
}}

server {{
    listen 80;
    server_name servername.com;
    charset utf-8;
    root    /opt/site_location/;

    location / {{
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 180;
        proxy_pass http://deliveryhero_gunicorn;
    }}
}}