Raw File
vip_manager.py
#!/usr/bin/env python
# -*- coding: iso8859-1 -*-

# 
# Copyright (c) 2004-2006 Stian Søiland, Magnus Nordseth
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Authors: Stian Soiland <stian@soiland.no>
#          Magnus Nordseth <magnus@nordseth.net>
#          Lasse Karstensen <lkarsten@samfundet.no>
#
# License: MIT
#

"""Manages VIP interfaces according to keepalived.conf.
Parses keepalived.conf to find active VIP (virtual_server) addresses
that should be active for a real_server (RIP).

Activates current VIPs on dummy interface, and disactivates
no longer active IPs.

See http://soiland.no/software/vip_manager/ or README.txt for
documentation.

USAGE: vip_manager.py [-d] [real_ip]
    -d       Show debug messages
    real_ip  Manually set real_server IP
             (default: resolve from hostname)
"""

__version__ = "0.2.2006-04-10"

(True, False) = (1==1, 0==1)

# Revision history
# ================
#
# 0.1-2004-05-04
#    First release
#
# 0.2-2006-04-10
#    Added support for Linux 2.6 by Lasse Karstensen
#    Default keepalived.conf path is now /etc/keepalived/keepalived.conf


import re
import socket
import sys
import os
from sets import Set
from systemArgs import systemArgs, ErrorcodeException

# Which dummy device to add/remove IP addresses to
DUMMY="dummy0"
# Our keepalived-conf ready for parsing
CONF="/etc/keepalived/keepalived.conf"
# Whether to print debug
DEBUG = False
# Our own IP to match for as real_server
REAL_IP = None


class VipManagerException(Exception):
    pass

def _message(args):
    """Stringify a list of arguments, join with space"""
    args = [str(arg) for arg in args]
    return " ".join(args)

def _debug(*args):
    if not DEBUG:
        return    
    """Prints debug"""
    print _message(args)
    

def _warning(*args):
    """Prints warning"""
    print >>sys.stderr, _message(args)
    

def _error(errorcode, *args):
    """Prints error exits with errorcode"""
    if __name__ == "__main__":
        # Only abort if we're run as a program
        _warning(*args)
        _warning("Aborting.")
        sys.exit(errorcode)
    else:
        raise VipManagerException, (errorcode, _message(args))


def get_all_servers(conf=None):
    """Reads keepalived.conf and finds defined virtual_servers and their
       matching real_servers. 
       Returns a dictionary, the keys are virtual_server IPs, and
       the values are sets of 0 or more real_server IPs.
    """   
    conf = conf or CONF
    try:
        config = open(conf)
    except IOError, e:
        _error(4, "Could not read config file", conf, e)
    virtualservers = {}
    realservers = None
    line_nr = 0
    for line in config.readlines():
        line_nr += 1
        vmatch = re.search(r"^\s*virtual_server\s+([\d.]+)", line)
        if vmatch:
            vserver = vmatch.group(1)
            # Add new list to virtualservers  if non-existing
            realservers = virtualservers.setdefault(vserver, Set())
        rmatch = re.search(r"^\s*real_server\s+([\d.]+)", line)            
        if rmatch:
            rserver = rmatch.group(1)
            if realservers is None:
                _error(1, "Parse error: found real_server", rserver, "for",
                      "unknown virtual_server on line", line_nr)
            realservers.add(rserver)
    sanity_check(virtualservers)            
    return virtualservers                


def sanity_check(virtualservers):
    """Make sure the virtualservers list seems reasonable"""
    if not virtualservers:
        _error(2, "No virtual servers defined, probably misconfiguration")
    # Check if any of the virtual servers actually has a real_server    
    any_real = [realservers for realservers in 
                     virtualservers.values() if realservers]
    if not any_real:
        _error(3, "No real servers defined, probably misconfiguration")


def get_my_virtualservers(real_ip=None, conf=None):
    """Gets a list over VIP addresses served by real_ip.
    If real_ip is not supplied, current hostname is used
    to resolve it.
    conf may specify the path to keepalived.conf. 
    """
    virtualservers = get_all_servers(conf)
    # get my real address 
    my_real = real_ip or REAL_IP
    if not my_real:
        try:
            my_real = socket.gethostbyname(socket.getfqdn())
        except socket.error, e:
            _error(5, "Could not resolve real_server IP", e)    
    if not my_real[:1].isdigit():
        # this might happen with /store/bin/python (!) 
        # (gethostbyname simply returns the same name) 
        _error(6, "Invalid IP, got", my_real)
    
    my_servers = [vserv for (vserv, realservers) in virtualservers.items()
                  if my_real in realservers]
    # my_servers.sort()                  
    return Set(my_servers)


def get_current_interfaces(dummy=None):
    """Retrieves current IPs active for given/default dummy interface"""
    dummy = dummy or DUMMY
    try:
        ip_output = systemArgs('ip', 'addr') 
    except ErrorcodeException, e:
        _error(7, "Could not run 'ip addr', error", *e)
    interfaces = Set()
    for line in ip_output.split("\n"):
        inetmatch = re.search(r"^\s+inet ([\d.]+)/32.*%s$" % dummy, line)
        if inetmatch:
            interfaces.add(inetmatch.group(1))
    return interfaces          

def update_interfaces(dummy=None, real_ip=None, conf=None):
    """Removes inactive IPs and adds new IPs to dummy interface"""
    dummy = dummy or DUMMY
    should_have = get_my_virtualservers(real_ip=real_ip, conf=conf)
    has_already = get_current_interfaces(dummy)
    # must_delete = filter(lambda x:x not in should_have, has_already)
    # must_delete = [ip for ip in has_already if ip not in should_have]
    # Using sets is a lot more sexy.... :)
    must_delete = has_already - should_have
    must_add = should_have - has_already
    _debug("Deleting", must_delete)
    for ip in must_delete:
        try:
            systemArgs('ip', 'addr', 'del', ip, 'dev', dummy)
        except ErrorcodeException, e:
            # The error code if 'ip addr del' says 
            # "RTNETLINK answers: Cannot assign requested address"
            if e[0] == 2: 
                _warning("Ignoring already removed ip", ip)
            else:    
                _error(10, "Could not remove", ip)
    _debug("Adding", must_add)
    for ip in must_add:
        try:
            systemArgs('ip', 'addr', 'add', ip, 'dev', dummy)
        except ErrorcodeException, e:
            # The error code if 'ip addr add' says 
            # "RTNETLINK answers: File exists"
            if e[0] == 2: 
                _warning("Ignoring already added ip", ip)
            else:    
                _error(11, "Could not add", ip)
    if should_have:            
        # make interface hidden if we have any active IPs 
        # (if no IPv4-addresses are on interface, hide_interface()
        # won't work) 
        hide_interface(dummy)

def activate_interface(dummy=None):
    """Turns the dummy interface 'up'"""
    dummy = dummy or DUMMY
    try:    
        systemArgs('ifconfig', dummy, 'up')
    except ErrorcodeException, e:
        _error(9, "Could not activate interface", dummy, *e)

def hide_interface(dummy=None): 
    """Makes the dummy interface 'hidden' to avoid ARP responses and
       related problems."""
    dummy = dummy or DUMMY
    try:
        if os.uname()[2].startswith('2.4'):
            open("/proc/sys/net/ipv4/conf/%s/hidden" % dummy, "w").write("1\n")
            # This global setting is also needed
            open("/proc/sys/net/ipv4/conf/all/hidden", "w").write("1\n")
        else:
            # Assume 2.6 or later
            # Linux 2.6 does things slightly more verbose
            open("/proc/sys/net/ipv4/conf/%s/arp_announce" % dummy, "w").write("2\n")
            open("/proc/sys/net/ipv4/conf/%s/arp_ignore"   % dummy, "w").write("1\n")
            # This global setting is also needed
            open("/proc/sys/net/ipv4/conf/all/arp_announce", "w").write("2\n")
            open("/proc/sys/net/ipv4/conf/all/arp_ignore"  , "w").write("1\n")
    except IOError, e:
        # Turning off interface since we can't hide it
        try: 
            systemArgs('ifconfig', dummy, 'down')
        except ErrorcodeException: 
            pass
        _error(8, "Could not set interface", dummy, "hidden", *e)   

def main():
    activate_interface()
    update_interfaces()

if __name__ == "__main__":
    # This is dirty, but small code
    args = sys.argv[1:]
    if "-h" in args:
        print __doc__
        sys.exit(0)
    if "-d" in args:
        DEBUG=True
        args.remove("-d")
    if args:
        REAL_IP = args[0]        
    main()
back to top