#!/usr/bin/env python from __future__ import absolute_import, division, print_function, unicode_literals import logging import argparse from daemonize import Daemonize log = logging.getLogger("neighbors") # arp # neigbour discovery # # events seen during ping: # RTM_NEWNEIGH, RTM_DELNEIGH # from pyroute2.netlink.iproute import IPRoute import etcd from nova_router import neighbors APP = "nova_neighbor_monitor" DESCRIPTION = "Monitor neighbors and keep updated set of mac/ip bindings in etcd" 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): 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) self.caches = ({}, {}, {}) self.ifindex = ifindex(interface) self.ir = IPRoute() self.blacklist_mac = set() neighbor_events = self.ir.get_neighbors() addresses = self.ir.get_addr() addresses = [a.get_attr("IFA_ADDRESS") for a in addresses] for router in routers: if router not in addresses: for n in neighbor_events: if n.get_attr('NDA_DST') == router: self.blacklist_mac.add(n.get_attr('NDA_LLADDR').lower()) for np in neighbor_events: self.process_event(np) def new(self, mac, ipaddr, addrtype): mac = mac.lower() old_address = neighbors.get_ipaddress_from_mac(self.etcd_client, mac, addrtype) if not old_address is None: if old_address == ipaddr: return else: log.debug("%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.debug("New host %s: %s", mac, ipaddr) neighbors.add_pair(self.etcd_client, mac, ipaddr, addrtype) 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: addrtype = neighbors.V4 elif ip.startswith("fe80"): addrtype = neighbors.V6_LL else: addrtype = neighbors.V6_PUB 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() def main(args): logging.basicConfig(level=logging.DEBUG) logging.getLogger("requests").setLevel(logging.INFO) if args.daemonize: handler = logging.handlers.RotatingFileHandler(args.logfile, maxBytes=10*1024**3, backupCount=5) handler.setFormatter(logging.getLogger("").handlers[0].formatter) logging.getLogger("").addHandler(handler) cache = NeighborCache(args.cert, args.key, args.cacert, args.interface, args.routers) cache.run() 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() if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)s %(levelname)s %(message)s') 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)