neighbors.py 5.77 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, setupLogfile
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
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())


32 33 34 35
def ifmac(interface):
    return open("/sys/class/net/{}/address".format(interface)).read().rstrip().lower()


36 37 38 39 40
def unicast_mac(mac):
    first_byte = int(mac.split(":")[0], 16)
    return (first_byte & 1) == 0


41
class NeighborCache(object):
42
    def __init__(self, ssl_cert, ssl_key, cacert, interface, routers):
Morten Knutsen's avatar
Morten Knutsen committed
43
        self.etcd_client = etcd.Client(host="localhost", cert=(ssl_cert, ssl_key),
44
                                       ca_cert=cacert, protocol='https')
45
        self.caches = ({}, {}, {})
46 47 48
        self.ifindex = ifindex(interface)
        self.ir = IPRoute()
        self.blacklist_mac = set()
49
        self.blacklist_mac.add(ifmac(interface))
50
        neighbor_events = self.ir.get_neighbors()
51 52 53 54
        addresses = self.ir.get_addr()
        addresses = [a.get_attr("IFA_ADDRESS") for a in addresses]
        for router in routers:
            if router not in addresses:
55
                for n in neighbor_events:
56
                    if n.get_attr('NDA_DST') == router:
57 58 59 60 61
                        mac = n.get_attr('NDA_LLADDR')
                        if mac:
                            self.blacklist_mac.add(mac.lower())
                        else:
                            logging.debug("Missing macaddress in neighbor message for router")
62
        for np in neighbor_events:
63 64 65
            self.process_event(np)

    def new(self, mac, ipaddr, addrtype):
66
        mac = mac.lower()
67 68 69 70
        old_address = neighbors.get_ipaddress_from_mac(self.etcd_client, mac, addrtype)

        if not old_address is None:
            if old_address == ipaddr:
71 72
                return
            else:
73 74 75 76 77 78 79 80 81 82 83 84 85
                log.info("%s changed address from %s to %s",
                         mac, old_address, ipaddr)
                neighbors.remove_pair(self.etcd_client, mac, old_address, addrtype)

        orig_mac = neighbors.get_mac_from_ipaddress(self.etcd_client, ipaddr, addrtype)
        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.info("New host %s: %s", mac, ipaddr)
            neighbors.add_pair(self.etcd_client, mac, ipaddr, addrtype)
86 87 88 89 90 91 92 93 94 95 96

    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:
97
                log.debug("Got event from blacklisted mac %s", mac)
98
                return
99 100 101
            if not unicast_mac(mac):
                log.debug("Got event from multicast mac %s", mac)
                return
102
            if "." in ip:
103
                addrtype = neighbors.V4
104
            elif ip.startswith("fe80"):
105
                addrtype = neighbors.V6_LL
106
            else:
107
                addrtype = neighbors.V6_PUB
108 109 110 111 112 113 114 115 116 117 118 119
            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()


120
def main(args):
Sigmund Augdal's avatar
Sigmund Augdal committed
121
    logging.getLogger("urllib3").setLevel(logging.INFO)
122
    if args.daemonize:
123
        setupLogfile(args.logfile)
124

125
    logging.info("Process starting")
Sigmund Augdal's avatar
Sigmund Augdal committed
126

127
    cache = NeighborCache(args.cert, args.key, args.cacert, args.interface, args.routers)
128 129
    cache.run()

130

131
def daemon_main(args):
132 133 134
    try:
        main(args)
    except Exception:
135
        logging.exception("Unhandled exception")
136 137


138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
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()

157
if __name__ == '__main__':
158
    logging.basicConfig(level=logging.INFO,
Sigmund Augdal's avatar
Sigmund Augdal committed
159
                        format='%(asctime)s %(name)s %(levelname)s %(message)s')
160 161 162
    args = parse_args()
    if args.daemonize:
        daemon = Daemonize(app=APP, pid=args.pidfile,
163
                           action=lambda: daemon_main(args))
164 165 166
        daemon.start()
    else:
        main(args)