neighbors.py 5.28 KB
Newer Older
1
#!/usr/bin/env python
2
from __future__ import absolute_import, division, print_function, unicode_literals
3
import logging
4 5
import argparse
from daemonize import Daemonize
6 7 8 9 10 11 12 13 14

log = logging.getLogger("neighbors")
# arp
# neigbour discovery
#
# events seen during ping:
# RTM_NEWNEIGH, RTM_DELNEIGH
#
from pyroute2.netlink.iproute import IPRoute
15
import etcd
16
from nova_router import neighbors
17 18


19 20 21 22
APP = "nova_neighbor_monitor"
DESCRIPTION = "Monitor neighbors and keep updated set of mac/ip bindings in etcd"


23 24 25 26 27 28 29 30 31 32
def byteswap(a):
    return ((a & 0xff000000) >> 24) | ((a & 0xff0000) >> 8) | \
           ((a & 0xff00) << 8) | ((a & 0xff) << 24)


def ifindex(interface):
    return int(open("/sys/class/net/{}/ifindex".format(interface)).read().rstrip())


class NeighborCache(object):
33 34 35
    def __init__(self, ssl_cert, ssl_key, cacert, interface, routers):
        self.etcd_client = etcd.Etcd(ssl_key=ssl_key, ssl_cert=ssl_cert,
                                     verify=cacert)
36
        self.caches = ({}, {}, {})
37 38 39
        self.ifindex = ifindex(interface)
        self.ir = IPRoute()
        self.blacklist_mac = set()
40
        neighbor_events = self.ir.get_neighbors()
41 42 43 44
        addresses = self.ir.get_addr()
        addresses = [a.get_attr("IFA_ADDRESS") for a in addresses]
        for router in routers:
            if router not in addresses:
45
                for n in neighbor_events:
46
                    if n.get_attr('NDA_DST') == router:
47
                        self.blacklist_mac.add(n.get_attr('NDA_LLADDR').lower())
48
        for np in neighbor_events:
49 50 51
            self.process_event(np)

    def new(self, mac, ipaddr, addrtype):
52
        mac = mac.lower()
53
        old_address = neighbors.get_ipaddress_from_mac(self.etcd_client, mac, addrtype)
54 55 56 57 58

        if not old_address is None:
            if old_address == ipaddr:
                return
            else:
59 60
                log.debug("%s changed address from %s to %s",
                          mac, old_address, ipaddr)
61
                neighbors.remove_pair(self.etcd_client, mac, old_address, addrtype)
62

63
        orig_mac = neighbors.get_mac_from_ipaddress(self.etcd_client, ipaddr, addrtype)
64 65 66 67 68 69 70
        if orig_mac is not None:
            log.warning("Duplicate address detected."
                        " %s tries to take %s from %s",
                        mac, ipaddr, orig_mac)
            return
        else:
            log.debug("New host %s: %s", mac, ipaddr)
71
            neighbors.add_pair(self.etcd_client, mac, ipaddr, addrtype)
72 73 74 75 76 77 78 79 80 81 82 83 84

    def process_event(self, np):
        if np['event'] == 'RTM_NEWNEIGH':
            mac = np.get_attr('NDA_LLADDR')
            ip = np.get_attr('NDA_DST')
            if byteswap(np['ifindex']) != self.ifindex:
                return
            if mac is None:
                return
            mac = mac.lower()
            if mac in self.blacklist_mac:
                return
            if "." in ip:
85
                addrtype = neighbors.V4
86
            elif ip.startswith("fe80"):
87
                addrtype = neighbors.V6_LL
88
            else:
89
                addrtype = neighbors.V6_PUB
90 91 92 93 94 95 96 97 98 99 100 101
            self.new(mac, ip, addrtype)

    def run(self):
        self.ir.monitor()
        while True:
            nps = self.ir.get()
            for np in nps:
                if np['event'] == 'RTM_NEWNEIGH':
                    self.process_event(np)
        self.ir.release()


102
def main(args):
103
    logging.basicConfig(level=logging.DEBUG)
104
    logging.getLogger("requests").setLevel(logging.INFO)
105 106 107
    if args.daemonize:
        handler = logging.handlers.RotatingFileHandler(args.logfile, maxBytes=10*1024**3,
                                                       backupCount=5)
108
        handler.setFormatter(logging.getLogger("").handlers[0].formatter)
109
        logging.getLogger("").addHandler(handler)
110

111
    cache = NeighborCache(args.cert, args.key, args.cacert, args.interface, args.routers)
112 113
    cache.run()

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133

def parse_args():
    parser = argparse.ArgumentParser(description=DESCRIPTION)
    parser.add_argument('-d', '--daemonize', default=False, action='store_true',
                        help="Run as daemon")
    parser.add_argument('--pidfile', type=str, default="/var/run/{}.pid".format(APP),
                        help="pidfile when run as daemon")
    parser.add_argument('--cert', default="client.crt", help="client certificate to use")
    parser.add_argument('--key', default="client.key",
                        help="private key to use for client certificate")
    parser.add_argument('--cacert', default="etcd_ca.crt", help="ca certificate to use")
    parser.add_argument('--logfile', default='/var/log/{}.log'.format(APP),
                        help="logfile to use")
    parser.add_argument('--interface', help="Interface to monitor", required=True)
    parser.add_argument('--routers',
                        help="Comma-separated list of routers to ignore when monitoring",
                        nargs="*", required=True)

    return parser.parse_args()

134
if __name__ == '__main__':
Sigmund Augdal's avatar
Sigmund Augdal committed
135 136
    logging.basicConfig(level=logging.DEBUG,
                        format='%(asctime)s %(name)s %(levelname)s %(message)s')
137 138 139 140 141 142 143 144
    logging.getLogger("requests").setLevel(logging.WARNING)
    args = parse_args()
    if args.daemonize:
        daemon = Daemonize(app=APP, pid=args.pidfile,
                           action=lambda: main(args))
        daemon.start()
    else:
        main(args)