#!/usr/bin/env python

import json
import os
import sys

from boto.vpc import connect_to_region
from boto.utils import get_instance_metadata
from subprocess import call


NAT_CONFIG = '/etc/nat.conf'


class Rerouter(object):
    def __init__(self, config):
        self.metadata = get_instance_metadata()
        self.config = config

    @property
    def current_instance_id(self):
        return self.metadata['instance-id']

    @property
    def current_az(self):
        return self.metadata['placement']['availability-zone']

    @property
    def current_region(self):
        return self.current_az[:-1]

    def take_route(self, az=None):
        az = az or self.current_az
        route_table_id = self.config[az]
        vpc = connect_to_region(self.current_region)
        vpc.replace_route(route_table_id, '0.0.0.0/0', instance_id=self.current_instance_id)

    __call__ = take_route


class Quorum(object):
    def __init__(self, config):
        self.config = config

    def quorum(self):
        ''' Returns True if the current nat belongs to the quorum. '''
        t = len(self.config)
        n = t - t / 2
        return self.alive(n)

    def alive(self, n):
        ''' Returns True if at least n nats are alive. '''
        cmd = "serf members -tag role=nat -status=alive -format json | jq '.members | length >= %s' | grep true" % n
        res = call(cmd, shell=True)
        return res == 0

    __call__ = quorum


class Member(object):
    def __init__(self, hostname, ip, role, tags):
        self.hostname = hostname
        self.ip = ip
        self.role = role
        self.tags = tags

    @property
    def az(self):
        return self.tags['az']

    @classmethod
    def parse_member(cls, row):
        ''' Parses serf row and returns (hostname, ip, role, tags). '''
        hostname, ip, role, tagstr  = row.strip().split('\t')
        tags = cls.parse_tags(tagstr)
        return cls(hostname, ip, role, tags)

    @classmethod
    def parse_tags(cls, tagstr):
        ''' Parses tag strings into a dicts: a=b,c=d -> {a: b, c: d} '''
        pairs = tagstr.split(',')
        return dict([x.split('=') for x in pairs])

    parse = parse_member


def main():
    with open(NAT_CONFIG) as f:
        config = json.load(f)

    event = os.environ['SERF_EVENT']
    quorum = Quorum(config)
    reroute = Rerouter(config)

    if quorum():
        if event == 'member-join':
            reroute()
        elif event in ['member-leave', 'member-failed']:
            members = map(Member.parse, sys.stdin.readlines())
            nats = [x for x in members if x.role == 'nat']
            for nat in nats:
                reroute(nat.az)


if __name__ == '__main__':
    main()

@@ -16,16 +16,956 @@
 python%0A%0A
+''' Serf handler for managing EC2 routes%0A%0AThis handler should be installed on NAT instances. It relies on Serf for%0Amembership events from other NATs. When other NATs disappear from the cluster%0Athis script will attempt to reroute their traffic onto the current instance. It%0Awill only do this if it has quorum, that is, if a majority of the instances are%0Astill available.%0A%0AThis script makes certain assumptions:%0A%0A    * There is a json file called /etc/nat.conf that maps the default route%0A    table ID for each availability zone, as follows:%0A%0A        %7B%0A            %22eu-west-1a%22: %22rtb-0e0ed06b%22,%0A            %22eu-west-1b%22: %22rtb-090ed06c%22,%0A            %22eu-west-1c%22: %22rtb-080ed06d%22%0A        %7D%0A%0A    * All NAT instances expose the role=nat tag in Serf.%0A    * All NAT instances expose the az tag in Serf, containing the availability%0A    zone in which it is running. For example, az=eu-west-1a.%0A    * The jq json processor program is available.%0A%0A'''%0A%0A
 import j

Add some documentation to nat-handler.py