def __init__(self, vswitch_instance, ns_info): self.__ns_info = ns_info self.name = ns_info['name'] self.ip = IPRoute() # self.ipdb = IPDB(nl=NetNS(self.name)) self.main_ipdb = IPDB() self.__vswitch = vswitch_instance
def address_exists(config, name, value): ip = IPRoute() if value.version == 4: family = socket.AF_INET elif value.version == 6: family = socket.AF_INET6 else: raise AssertionError("Unknown version {}".format(value.version)) if ip.get_addr(family=family, address=value.ip, prefixlen=value.prefixlen): raise ConfigError(name, "No such address {}".format(value))
def initdb(self): # flush all the DB objects with self.exclusive: # explicitly cleanup object references for event in tuple(self._event_map): del self._event_map[event] self._flush_db() # if the command socket is not provided, create it if self._nl_own: if self.nl is not None: self.nl.close() self.nl = IPRoute(sndbuf=self._sndbuf, rcvbuf=self._rcvbuf, async_qsize=0) # OBS: legacy design # setup monitoring socket if self.mnl is not None: self._flush_mnl() self.mnl.close() self.mnl = self.nl.clone() try: self.mnl.bind(groups=self.nl_bind_groups, async_cache=self._nl_async) except: self.mnl.close() if self._nl_own is None: self.nl.close() raise # explicitly cleanup references for key in tuple(self._deferred): del self._deferred[key] for module in self._plugins: if (module.groups & self.nl_bind_groups) != module.groups: continue for plugin in module.spec: self._deferred[plugin['name']] = module.spec if plugin['name'] in self._loaded: delattr(self, plugin['name']) self._loaded.remove(plugin['name']) # start service threads for tspec in (('_mthread', '_serve_main', 'IPDB main event loop'), ('_cthread', '_serve_cb', 'IPDB cb event loop')): tg = getattr(self, tspec[0], None) if not getattr(tg, 'is_alive', lambda: False)(): tx = threading.Thread(name=tspec[2], target=getattr(self, tspec[1])) setattr(self, tspec[0], tx) tx.setDaemon(True) tx.start()
def run(self): nat = {} clients = [] srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) srv.bind((self.addr, self.port)) ipr = IPRoute() ipr.bind() poll = select.poll() poll.register(ipr, select.POLLIN | select.POLLPRI) poll.register(srv, select.POLLIN | select.POLLPRI) while True: events = poll.poll() for (fd, event) in events: if fd == ipr.fileno(): bufsize = ipr.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) // 2 data = ipr.recv(bufsize) cookie = struct.unpack('I', data[8:12])[0] if cookie == 0: for address in clients: srv.sendto(data, address) else: srv.sendto(data, nat[cookie]) else: data, address = srv.recvfrom(16384) if data is None: clients.remove(address) continue cookie = struct.unpack('I', data[8:12])[0] nat[cookie] = address ipr.sendto(data, (0, 0))
def address_exists(cls, config, value): ip = IPRoute() if value.version == 4: family = socket.AF_INET elif value.version == 6: family = socket.AF_INET6 else: raise AssertionError("Unknown version {}".format(value.version)) if ip.get_addr(family=family, address=value.ip, prefixlen=value.prefixlen): raise OptionCheckError("No such address {}".format(value), option=cls.__name__)
def address_exists(option, config, value): ip = IPRoute() if value.version == 4: family = socket.AF_INET elif value.version == 6: family = socket.AF_INET6 else: raise AssertionError("Unknown version {}".format(value.version)) if ip.get_addr(family=family, address=value.ip, prefixlen=value.prefixlen): raise OptionCheckError("No such address {}".format(value), option=option.__name__)
def initdb(self, nl=None): ''' Restart IPRoute channel, and create all the DB from scratch. Can be used when sync is lost. ''' self.nl = nl or IPRoute() self.nl.monitor = True self.nl.bind(async=True) # resolvers self.interfaces = Dotkeys() self.routes = RoutingTables(ipdb=self) self.by_name = Dotkeys() self.by_index = Dotkeys() # caches self.ipaddr = {} self.neighbors = {} # load information links = self.nl.get_links() for link in links: self.device_put(link, skip_slaves=True) for link in links: self.update_slaves(link) self.update_addr(self.nl.get_addr()) self.update_neighbors(self.nl.get_neighbors()) routes = self.nl.get_routes() self.update_routes(routes)
def run(self) -> int: try: with IPRoute() as ipr: links = ipr.get_links("all") routes = ipr.get_default_routes(family=AF_INET6) except Exception as e: LOG.exception(f"Failed to get list of valid interfaces: {e}") return -1 for link in links: if link.get_attr("IFLA_IFNAME") != self.interface: continue self.interface_id = link["index"] break if not self.interface_id: LOG.error(f"Unable to find {self.interface} index") return 1 for route in routes: multi_routes = route.get_attr("RTA_MULTIPATH") for mr in multi_routes: if mr["oif"] == self.interface_id: return 0 return 2
def __init__(self, ns_info): self.__ns_info = ns_info self.name = ns_info['name'] self.ip = IPRoute() self.__interfaces = {} self.__bridges = {} self.logger_topo = None
def create_interface(self): ifname = self.__intf_info["ifname"] IP_ROUTE = IPRoute() if len(IP_ROUTE.link_lookup(ifname=ifname)) > 0: self.logger_topo.warning( "ip link {} exists so not create it.".format(ifname)) IP_ROUTE.close() return if self.__peer: if len(IP_ROUTE.link_lookup(ifname=self.__peer)) > 0: self.logger_topo.warning( "ip link {} exists so not create it.".format(ifname)) IP_ROUTE.close() return else: # Close IP_ROUTE first anyway IP_ROUTE.close() ps_intf = r"^\d+: (?P<intf>[\w-]+): " p_intf = re.compile(ps_intf, re.MULTILINE) _, out, _ = exec_cmd_in_namespace(self.__namespace, ["ip", "link"]) m_intf = p_intf.findall(out) if ifname in m_intf: self.logger_topo.warning( "ip link {} exists in namespace {} so not create it.". format(ifname, self.__namespace)) return MAIN_IPDB = IPDB() MAIN_IPDB.create(ifname=ifname, kind="veth" if self.__peer else "dummy", peer=self.__peer).commit() with MAIN_IPDB.interfaces[ifname] as veth: try: veth.net_ns_fd = self.__namespace except netlink.exceptions.NetlinkError as e: MAIN_IPDB.release() if e.code == 17: # "File exists" pass else: raise e MAIN_IPDB.release() self.logger_topo.info( "interface {} in namespace {} is created, peer: {}.".format( ifname, self.__namespace, self.__peer))
def create(self): """ Main function to build all infrasim virtual network referring to resolved topology """ self.__load() self.logger_topo.info("[Define openvswitches]") for _, ovs in self.__openvswitch.items(): ovs.add_vswitch() ovs.add_all_ports() # FIXME: substitute for below code # self.__vswitch_ex.set_interface("phy-br-ex", "int-br-ex") # self.__vswitch_int.set_interface("int-br-ex", "phy-br-ex") ovs.add_interface_d() self.logger_topo.info("[Define namespaces]") for _, ns in self.__namespace.items(): ns.create_namespace() ns.create_all_interfaces(ref=self.__connection) self.logger_topo.info("[Set openvswitch ports up]") IP_ROUTE = IPRoute() for _, ovs in self.__openvswitch.items(): idx = IP_ROUTE.link_lookup(ifname=ovs.name)[0] IP_ROUTE.link("set", index=idx, state="up") self.logger_topo.info("set openvswitch {} up.".format(ovs.name)) for _, ovs_port in self.__connection.items(): idx = IP_ROUTE.link_lookup(ifname=ovs_port)[0] IP_ROUTE.link("set", index=idx, state="up") self.logger_topo.info("set port {} up.".format(ovs_port)) self.logger_topo.info("[Set namespace interfaces up]") for _, ns in self.__namespace.items(): ns.create_interface_d() ns.link_up_all() ns.create_routes() self.logger_topo.info("[Setup portforward]") self.__port_forward.build() IP_ROUTE.close()
def initdb(self): # common event map, empty by default, so all the # events aer just ignored self.release(complete=False) self._stop = False # explicitly cleanup object references for event in tuple(self._event_map): del self._event_map[event] # if the command socket is not provided, create it if self._nl_own: self.nl = IPRoute() # setup monitoring socket self.mnl = self.nl.clone() try: self.mnl.bind(groups=self.nl_bind_groups, async=self._nl_async) except: self.mnl.close() if self._nl_own is None: self.nl.close() raise # explicitly cleanup references for key in tuple(self._deferred): del self._deferred[key] for module in self._plugins: if (module.groups & self.nl_bind_groups) != module.groups: continue for plugin in module.spec: self._deferred[plugin['name']] = module.spec if plugin['name'] in self._loaded: delattr(self, plugin['name']) self._loaded.remove(plugin['name']) # start the monitoring thread self._mthread = threading.Thread(name="IPDB event loop", target=self.serve_forever) self._mthread.setDaemon(True) self._mthread.start()
def test_isinstance(self): from pyroute2 import IPRSocket from pyroute2 import IPRoute from pyroute2.iproute import IPRoute as IPRoute_real from pyroute2.netlink.rtnl.iprsocket import IPRSocket as IPRSocket_real ipr1 = IPRoute() ipr2 = IPRoute_real() ips1 = IPRSocket() ips2 = IPRSocket_real() # positive assert isinstance(ips1, IPRSocket) assert isinstance(ips2, IPRSocket) assert isinstance(ips1, IPRSocket_real) assert isinstance(ips2, IPRSocket_real) assert isinstance(ipr1, IPRoute) assert isinstance(ipr2, IPRoute) assert isinstance(ipr1, IPRoute_real) assert isinstance(ipr2, IPRoute_real) # negative assert not isinstance(ips1, IPRoute) assert not isinstance(ips2, IPRoute) assert not isinstance(ips1, IPRoute_real) assert not isinstance(ips2, IPRoute_real) # this must succeed -- IPRoute is a subclass of IPRSocket assert isinstance(ipr1, IPRSocket) assert isinstance(ipr2, IPRSocket) assert isinstance(ipr1, IPRSocket_real) assert isinstance(ipr2, IPRSocket_real) ips1.close() ips2.close() ipr1.close() ipr2.close()
def connection_down(parsed_args): ipsec_connection = load_ipsec_connection(parsed_args) connection_name = ipsec_connection['name'] ip_route = IPRoute() if is_connection_up(ip_route, ipsec_connection): ipsec_result = ipsec('down', connection_name) if ipsec_result.status != 0: raise DockerIPSecError('Failed to disconnect VPN: {0}\n{1}'.format( connection_name, ipsec_result.output)) filter_func = functools.partial(comment_matches_ipsec_connection, connection_name) remove_iptables_rules(filter_func)
def __init__(self, addr): IPRoute.__init__(self) self.proxy = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.proxy.bind(('0.0.0.0', 3547)) self.proxy_addr = addr self.proxy_queue = Queue() def recv(): while True: (data, addr) = self.proxy.recvfrom(16384) self.proxy_queue.put(data) self.pthread = threading.Thread(target=recv) self.pthread.setDaemon(True) self.pthread.start() def sendto(buf, *argv, **kwarg): return self.proxy.sendto(buf, (self.proxy_addr, 3546)) def recv(*argv, **kwarg): return self.proxy_queue.get() self._sendto = sendto self._recv = recv
def add_docker_networks(parsed_args): ipsec_connection = load_ipsec_connection(parsed_args) connection_name = ipsec_connection['name'] docker_networks = parsed_args.dockerNetworks docker_client = docker.DockerClient() docker_network_to_ip_network = functools.partial( ip_network_for_docker_network, docker_client) docker_ip_networks = tuple( map(docker_network_to_ip_network, docker_networks)) ip_route = IPRoute() if not is_connection_up(ip_route, ipsec_connection): raise DockerIPSecError( 'IPSec connection {0} is not connected!'.format(connection_name)) add_ip_networks(ip_route, docker_ip_networks, connection_name)
def __init__(self, interface): '''Manipulations the configuration of a given interface Args: interface - name of the interface to manipulate (i.e. 'eth0') Raises: InvalidNetworkDevice - if the interface does not exist ''' self.interface = interface self.iproute_api = IPRoute() # Confirm this is a valid interface # This will chuck a IndexError exception if it doesn't exist self.interface_index = self._get_interface_index()
def initdb(self, nl=None): ''' Restart IPRoute channel, and create all the DB from scratch. Can be used when sync is lost. ''' self.nl = nl or IPRoute() # resolvers self.interfaces = Dotkeys() self.routes = RoutingTableSet(ipdb=self, ignore_rtables=self._ignore_rtables) self.by_name = View(src=self.interfaces, constraint=lambda k, v: isinstance(k, basestring)) self.by_index = View(src=self.interfaces, constraint=lambda k, v: isinstance(k, int)) # caches self.ipaddr = {} self.neighbours = {} try: self.nl.bind(async=self._nl_async) # load information links = self.nl.get_links() for link in links: self.device_put(link, skip_slaves=True) for link in links: self.update_slaves(link) # bridge info links = self.nl.get_vlans() for link in links: self.update_dev(link) # self.update_addr(self.nl.get_addr()) self.update_neighbours(self.nl.get_neighbours()) routes4 = self.nl.get_routes(family=AF_INET) routes6 = self.nl.get_routes(family=AF_INET6) self.update_routes(routes4) self.update_routes(routes6) except Exception as e: try: self.nl.close() except: pass raise e
def connection_up(parsed_args): ipsec_connection = load_ipsec_connection(parsed_args) connection_name = ipsec_connection['name'] docker_networks = parsed_args.dockerNetworks if len(docker_networks) > 0: docker_client = docker.DockerClient() docker_network_to_ip_network = functools.partial( ip_network_for_docker_network, docker_client) docker_ip_networks = tuple( map(docker_network_to_ip_network, docker_networks)) else: docker_ip_networks = tuple() ip_route = IPRoute() if not is_connection_up(ip_route, ipsec_connection): ipsec_result = ipsec('up', connection_name) if ipsec_result.status != 0: raise DockerIPSecError('Failed to connect VPN: {0}\n{1}'.format( connection_name, ipsec_result.output)) add_ip_networks(ip_route, docker_ip_networks, connection_name)
def enable_static_routes(static_routes): """ 启用默认路由 :param static_routes: list 默认路由列表 """ with IPRoute() as route: for r in static_routes: if_id = route.link_lookup( ifname=r['interface'])[0] if 'interface' in r else None args = { 'dst': r['net'], 'oif': if_id, 'gateway': r.get('gateway'), 'metric': 1 } args = {k: v for k, v in args.iteritems() if v} try: route.route('add', **args) except NetlinkError as e: if e.code == 17: continue else: raise
class InterfaceConfigTest(unittest.TestCase): '''Tests all aspects of the NetworkInterfaceConfig API A dummy interface (dummy0) is used to test APIs and read back configuration. Testing may report a false positive if this interface exists for whatever reason.''' def __init__(self, *args, **kwargs): super(InterfaceConfigTest, self).__init__(*args, **kwargs) self.iproute_api = IPRoute() def setUp(self): self.iproute_api.link_create(name='dummy0', kind='dummy') def tearDown(self): idx = self.iproute_api.link_lookup(ifname='dummy0')[0] self.iproute_api.link_remove(idx) self.iproute_api.close() @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_nonexistent_interface(self): '''Tests that InvalidNetworkDevice is raised if an interface is non-existent''' with self.assertRaises(InvalidNetworkDevice): NetworkInterfaceConfig('nonexistent0') @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_empty_configuration(self): '''Confirms that we properly handle no configuration data on an interface''' # The interface has just been created as part of setup, there shouldn't be any IPs interface_cfg = NetworkInterfaceConfig('dummy0') if interface_cfg.get_ips(): self.fail("dummy configuration returned an IP!") @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_add_ipv4(self): '''Adds an IPv4 address, and then confirms it via get_ips()''' interface_cfg = NetworkInterfaceConfig('dummy0') interface_cfg.add_v4_ip(ip_address='10.0.241.123', prefix_length=24) # Retrieve the IPs on the interface and make sure its the only one # plus that it is the correct IP ips = interface_cfg.get_ips() self.assertEqual(len(ips), 1, "dummy interface either didn't get the IP or has multiple!") self.assertEqual(ips[0]['ip_address'], '10.0.241.123', "IP assignment failure!") self.assertEqual(ips[0]['family'], AF_INET) self.assertEqual(ips[0]['broadcast'], '10.0.241.255', "IP assignment failure!") self.assertEqual(ips[0]['prefix_length'], 24, "IP assignment failure!") @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_add_ipv6(self): '''Adds an IPv6 address and then confirms it''' interface_cfg = NetworkInterfaceConfig('dummy0') interface_cfg.add_v6_ip('fd00:a3b1:78a2::1', 64) ips = interface_cfg.get_ips() self.assertEqual(len(ips), 1, "dummy interface either didn't get the IP or has multiple!") self.assertEqual(ips[0]['ip_address'], 'fd00:a3b1:78a2::1', "IP assignment failure!") self.assertEqual(ips[0]['family'], AF_INET6, "IP assignment failure!") self.assertEqual(ips[0]['prefix_length'], 64, "IP assignment failure!") # FIXME: Write tests using different IPv6 notations @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_remove_ipv6(self): '''Removes an IPv6 address and confirms it''' interface_cfg = NetworkInterfaceConfig('dummy0') interface_cfg.add_v6_ip('fd00:a3b1:78a2::1', 64) interface_cfg.remove_ip('fd00:a3b1:78a2::1') if interface_cfg.get_ips(): self.fail("dummy configuration returned an IP!") @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_duplicate_ip_failure(self): '''Tests duplicate address protection''' interface_cfg = NetworkInterfaceConfig('dummy0') interface_cfg.add_v4_ip(ip_address='10.0.241.123', prefix_length=24) with self.assertRaises(DuplicateIPError): interface_cfg.add_v4_ip(ip_address='10.0.241.123', prefix_length=24) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_remove_ipv4(self): '''Tests interface deconfiguration of v4 addresses''' interface_cfg = NetworkInterfaceConfig('dummy0') interface_cfg.add_v4_ip(ip_address='10.0.241.123', prefix_length=24) # Now remove the IP interface_cfg.remove_ip('10.0.241.123') # And make sure its gone if interface_cfg.get_ips(): self.fail("dummy configuration returned an IP!") @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_invalid_ip_check(self): '''Makes sure we raise ValueError on if we pass in an invalid IP''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v4_ip(ip_address='ImNotAnIP!', prefix_length=1337) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_network_address_rejection(self): '''Prefixes >/24 require a dedicated network address that can't be used as an IP''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v4_ip(ip_address='192.168.1.192', prefix_length=26) # pylint: disable=invalid-name @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_broadcast_address_rejection(self): '''Rejects if we try using a broadcast address of a prefix''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v4_ip(ip_address='192.168.1.191', prefix_length=26) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_ipv4_loopback_address_rejection(self): '''Rejects if we try using a loopback address''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v4_ip(ip_address='127.0.1.2', prefix_length=24) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_ipv4_multicast_rejection(self): '''Reject if we try to assign a multicast address''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v4_ip(ip_address='224.0.0.1', prefix_length=24) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_ipv4_class_e_rejection(self): '''Reject if we try to use a class E address''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v4_ip(ip_address='240.0.0.1', prefix_length=24) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_ipv4_link_local_rejection(self): '''Reject if we try to use a link-local address''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v4_ip(ip_address='169.254.1.1', prefix_length=24) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_ipv6_loopback_address_reject(self): '''Rejects if we try to assign loopback''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v6_ip(ip_address='::1', prefix_length=128) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_ipv6_multicast_reject(self): '''Rejects if address is IPv6 multicast''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v6_ip(ip_address="ff05::1:3", prefix_length=128) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_ipv6_reserved_reject(self): '''Rejects if the IP is in reserved address space''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(ValueError): interface_cfg.add_v6_ip(ip_address='dead:beef::', prefix_length=64) @unittest.skipIf(os.getuid() != 0, 'must be run as root') def test_check_for_nonexistent_ip(self): '''Tests IPNotFound response when getting information for a specific IP''' interface_cfg = NetworkInterfaceConfig('dummy0') with self.assertRaises(IPNotFound): interface_cfg.get_full_ip_info("10.0.21.123")
def main(): """ Program entry point when run interactively. """ # prepare option parser parser = ArgumentParser(usage="usage: %(prog)s [options]", description="Listens to UDP broadcasts to test for " "connectivity") parser.add_argument("-i", "--interface", dest="interface", default="poprow0", action="store", metavar="ifname", help="Interface to use [default: %(default)s]") parser.add_argument("-p", "--port", dest="port", type=int, default=12345, action="store", help="UDP port [default: %(default)s]", metavar="PORT") parser.add_argument("-f", "--filename", dest="filename", default="", action="store", help="Output file where to store results [default: " "stdout]", metavar="FILENAME") parser.add_argument("-v", "--verbose", dest="verbose", action="store_true") parser.add_argument("-d", "--daemon", dest="daemon", action="store_true", help="Daemonize the process") # parse options args = parser.parse_args() # copy options interface = args.interface port = args.port filename = args.filename verbose = args.verbose daemon = args.daemon ip = IPRoute() if_index = ip.link_lookup(ifname=interface) if len(if_index) == 0: print >> sys.stderr, "Can't find interface %s" % interface sys.exit(1) # from the given interface find unicast and broadcast addresses if_info = ip.get_addr(index=if_index[0])[0] address = if_info.get_attr("IFA_ADDRESS") netmask = if_info["prefixlen"] network = ipaddr.IPv4Network("%s/%d" % (address, netmask)) broadcast = str(network.broadcast) # get hostname as additional information for the log file hostname = socket.gethostname() if not daemon: signal.signal(signal.SIGTERM, signal_handler) else: daemonize() # setup output file if filename == "": outfile = sys.stdout else: outfile = open(filename, "w") if verbose: print >> sys.stderr, "Host with IP %s listening for packets on " \ "broadcast IP %s" % (address, broadcast) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((broadcast, port)) run = True while run: try: datagram = sock.recvfrom(1024) payload = datagram[0].split(' ') src = datagram[1][0] src_port = datagram[1][1] # check that this is really our app message if payload[0] != "POPROWPING": continue count = int(payload[1]) remote_host = payload[2] if verbose: print >> sys.stderr, "Received packet #%d from %s:%d" % \ (count, src, src_port) outfile.write("%s,%s,%s,%s,%d\n" % (hostname, address, remote_host, src, count)) outfile.flush() except SystemExit: run = False if verbose: print >> sys.stderr, "Catching SystemExit. Quitting" except KeyboardInterrupt: run = False if verbose: print >> sys.stderr, "Keyboard Interrupt. Quitting" except: run = False if verbose: print >> sys.stderr, "Error while receiving packet. Quitting" sock.close() outfile.flush() outfile.close()
def NetNServer(netns, rcvch, cmdch, flags=os.O_CREAT): ''' The netns server supposed to be started automatically by NetNS. It has two communication channels: one simplex to forward incoming netlink packets, `rcvch`, and other synchronous duplex to get commands and send back responses, `cmdch`. Channels should support standard socket API, should be compatible with poll/select and should be able to transparently pickle objects. NetNS uses `multiprocessing.Pipe` for this purpose, but it can be any other implementation with compatible API. The first parameter, `netns`, is a netns name. Depending on the `flags`, the netns can be created automatically. The `flags` semantics is exactly the same as for `open(2)` system call. ... The server workflow is simple. The startup sequence:: 1. Create or open a netns. 2. Start `IPRoute` instance. It will be used only on the low level, the `IPRoute` will not parse any packet. 3. Start poll/select loop on `cmdch` and `IPRoute`. On the startup, the server sends via `cmdch` the status packet. It can be `None` if all is OK, or some exception. Further data handling, depending on the channel, server side:: 1. `IPRoute`: read an incoming netlink packet and send it unmodified to the peer via `rcvch`. The peer, polling `rcvch`, can handle the packet on its side. 2. `cmdch`: read tuple (cmd, argv, kwarg). If the `cmd` starts with "send", then take `argv[0]` as a packet buffer, treat it as one netlink packet and substitute PID field (offset 12, uint32) with its own. Strictly speaking, it is not mandatory for modern netlink implementations, but it is required by the protocol standard. ''' signal.signal(signal.SIGINT, signal.SIG_IGN) try: nsfd = setns(netns, flags) except OSError as e: cmdch.send(e) return e.errno except Exception as e: cmdch.send(OSError(errno.ECOMM, str(e), netns)) return 255 # try: ipr = IPRoute() rcvch_lock = ipr._sproxy.lock ipr._s_channel = rcvch poll = select.poll() poll.register(ipr, select.POLLIN | select.POLLPRI) poll.register(cmdch, select.POLLIN | select.POLLPRI) except Exception as e: cmdch.send(e) return 255 # all is OK so far cmdch.send(None) # 8<------------------------------------------------------------- while True: events = poll.poll() for (fd, event) in events: if fd == ipr.fileno(): bufsize = ipr.getsockopt(SOL_SOCKET, SO_RCVBUF) // 2 with rcvch_lock: rcvch.send(ipr.recv(bufsize)) elif fd == cmdch.fileno(): try: cmdline = cmdch.recv() if cmdline is None: poll.unregister(ipr) poll.unregister(cmdch) ipr.close() os.close(nsfd) return (cmd, argv, kwarg) = cmdline if cmd[:4] == 'send': # Achtung # # It's a hack, but we just have to do it: one # must use actual pid in netlink messages # # FIXME: there can be several messages in one # call buffer; but right now we can ignore it msg = argv[0][:12] msg += struct.pack("I", os.getpid()) msg += argv[0][16:] argv = list(argv) argv[0] = msg cmdch.send(getattr(ipr, cmd)(*argv, **kwarg)) except Exception as e: e.tb = traceback.format_exc() cmdch.send(e)
class IPDB(object): ''' The class that maintains information about network setup of the host. Monitoring netlink events allows it to react immediately. It uses no polling. ''' def __init__(self, nl=None, mode='implicit', restart_on_error=None, nl_async=None, sndbuf=1048576, rcvbuf=1048576, nl_bind_groups=RTMGRP_DEFAULTS, ignore_rtables=None, callbacks=None, sort_addresses=False, plugins=None): plugins = plugins or ['interfaces', 'routes', 'rules'] pmap = {'interfaces': interfaces, 'routes': routes, 'rules': rules} self.mode = mode self.txdrop = False self._stdout = sys.stdout self._ipaddr_set = SortedIPaddrSet if sort_addresses else IPaddrSet self._event_map = {} self._deferred = {} self._ensure = [] self._loaded = set() self._mthread = None self._nl_own = nl is None self._nl_async = config.ipdb_nl_async if nl_async is None else True self.mnl = None self.nl = nl self._sndbuf = sndbuf self._rcvbuf = rcvbuf self.nl_bind_groups = nl_bind_groups self._plugins = [pmap[x] for x in plugins if x in pmap] if isinstance(ignore_rtables, int): self._ignore_rtables = [ignore_rtables, ] elif isinstance(ignore_rtables, (list, tuple, set)): self._ignore_rtables = ignore_rtables else: self._ignore_rtables = [] self._stop = False # see also 'register_callback' self._post_callbacks = {} self._pre_callbacks = {} # local event queues # - callbacks event queue self._cbq = queue.Queue(maxsize=8192) self._cbq_drop = 0 # - users event queue self._evq = None self._evq_lock = threading.Lock() self._evq_drop = 0 # locks and events self.exclusive = threading.RLock() self._shutdown_lock = threading.Lock() # register callbacks # # examples:: # def cb1(ipdb, msg, event): # print(event, msg) # def cb2(...): # ... # # # default mode: post # IPDB(callbacks=[cb1, cb2]) # # specify the mode explicitly # IPDB(callbacks=[(cb1, 'pre'), (cb2, 'post')]) # for cba in callbacks or []: if not isinstance(cba, (tuple, list, set)): cba = (cba, ) self.register_callback(*cba) # load information self.restart_on_error = restart_on_error if \ restart_on_error is not None else nl is None # init the database self.initdb() # init the dir() cache self.__dir_cache__ = [i for i in self.__class__.__dict__.keys() if i[0] != '_'] self.__dir_cache__.extend(list(self._deferred.keys())) def cleanup(ref): ipdb_obj = ref() if ipdb_obj is not None: ipdb_obj.release() atexit.register(cleanup, weakref.ref(self)) def __dir__(self): return self.__dir_cache__ def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.release() def _flush_db(self): def flush(idx): for key in tuple(idx.keys()): try: del idx[key] except KeyError: pass idx_list = [] if 'interfaces' in self._loaded: for (key, dev) in self.by_name.items(): try: # FIXME self.interfaces._detach(key, dev['index'], dev.nlmsg) except KeyError: pass idx_list.append(self.ipaddr) idx_list.append(self.neighbours) if 'routes' in self._loaded: idx_list.extend([self.routes.tables[x] for x in self.routes.tables.keys()]) if 'rules' in self._loaded: idx_list.append(self.rules) for idx in idx_list: flush(idx) def initdb(self): # flush all the DB objects with self.exclusive: # explicitly cleanup object references for event in tuple(self._event_map): del self._event_map[event] self._flush_db() # if the command socket is not provided, create it if self._nl_own: if self.nl is not None: self.nl.close() self.nl = IPRoute(sndbuf=self._sndbuf, rcvbuf=self._rcvbuf) # setup monitoring socket if self.mnl is not None: self._flush_mnl() self.mnl.close() self.mnl = self.nl.clone() try: self.mnl.bind(groups=self.nl_bind_groups, async_cache=self._nl_async) except: self.mnl.close() if self._nl_own is None: self.nl.close() raise # explicitly cleanup references for key in tuple(self._deferred): del self._deferred[key] for module in self._plugins: if (module.groups & self.nl_bind_groups) != module.groups: continue for plugin in module.spec: self._deferred[plugin['name']] = module.spec if plugin['name'] in self._loaded: delattr(self, plugin['name']) self._loaded.remove(plugin['name']) # start service threads for tspec in (('_mthread', '_serve_main', 'IPDB main event loop'), ('_cthread', '_serve_cb', 'IPDB cb event loop')): tg = getattr(self, tspec[0], None) if not getattr(tg, 'is_alive', lambda: False)(): tx = threading.Thread(name=tspec[2], target=getattr(self, tspec[1])) setattr(self, tspec[0], tx) tx.setDaemon(True) tx.start() def __getattribute__(self, name): deferred = super(IPDB, self).__getattribute__('_deferred') if name in deferred: register = [] spec = deferred[name] for plugin in spec: obj = plugin['class'](self, **plugin['kwarg']) setattr(self, plugin['name'], obj) register.append(obj) self._loaded.add(plugin['name']) del deferred[plugin['name']] for obj in register: if hasattr(obj, '_register'): obj._register() if hasattr(obj, '_event_map'): for event in obj._event_map: if event not in self._event_map: self._event_map[event] = [] self._event_map[event].append(obj._event_map[event]) return super(IPDB, self).__getattribute__(name) def register_callback(self, callback, mode='post'): ''' IPDB callbacks are routines executed on a RT netlink message arrival. There are two types of callbacks: "post" and "pre" callbacks. ... "Post" callbacks are executed after the message is processed by IPDB and all corresponding objects are created or deleted. Using ipdb reference in "post" callbacks you will access the most up-to-date state of the IP database. "Post" callbacks are executed asynchronously in separate threads. These threads can work as long as you want them to. Callback threads are joined occasionally, so for a short time there can exist stopped threads. ... "Pre" callbacks are synchronous routines, executed before the message gets processed by IPDB. It gives you the way to patch arriving messages, but also places a restriction: until the callback exits, the main event IPDB loop is blocked. Normally, only "post" callbacks are required. But in some specific cases "pre" also can be useful. ... The routine, `register_callback()`, takes two arguments: - callback function - mode (optional, default="post") The callback should be a routine, that accepts three arguments:: cb(ipdb, msg, action) Arguments are: - **ipdb** is a reference to IPDB instance, that invokes the callback. - **msg** is a message arrived - **action** is just a msg['event'] field E.g., to work on a new interface, you should catch action == 'RTM_NEWLINK' and with the interface index (arrived in msg['index']) get it from IPDB:: index = msg['index'] interface = ipdb.interfaces[index] ''' lock = threading.Lock() def safe(*argv, **kwarg): with lock: callback(*argv, **kwarg) safe.hook = callback safe.lock = lock safe.uuid = uuid32() if mode == 'post': self._post_callbacks[safe.uuid] = safe elif mode == 'pre': self._pre_callbacks[safe.uuid] = safe else: raise KeyError('Unknown callback mode') return safe.uuid def unregister_callback(self, cuid, mode='post'): if mode == 'post': cbchain = self._post_callbacks elif mode == 'pre': cbchain = self._pre_callbacks else: raise KeyError('Unknown callback mode') safe = cbchain[cuid] with safe.lock: ret = cbchain.pop(cuid) return ret def eventqueue(self, qsize=8192, block=True, timeout=None): ''' Initializes event queue and returns event queue context manager. Once the context manager is initialized, events start to be collected, so it is possible to read initial state from the system witout losing last moment changes, and once that is done, start processing events. Example: ipdb = IPDB() with ipdb.eventqueue() as evq: my_state = ipdb.<needed_attribute>... for msg in evq: update_state_by_msg(my_state, msg) ''' return _evq_context(self, qsize, block, timeout) def eventloop(self, qsize=8192, block=True, timeout=None): """ Event generator for simple cases when there is no need for initial state setup. Initialize event queue and yield events as they happen. """ with self.eventqueue(qsize=qsize, block=block, timeout=timeout) as evq: for msg in evq: yield msg def release(self): ''' Shutdown IPDB instance and sync the state. Since IPDB is asyncronous, some operations continue in the background, e.g. callbacks. So, prior to exit the script, it is required to properly shutdown IPDB. The shutdown sequence is not forced in an interactive python session, since it is easier for users and there is enough time to sync the state. But for the scripts the `release()` call is required. ''' with self._shutdown_lock: if self._stop: log.warning("shutdown in progress") return self._stop = True self._cbq.put(ShutdownException("shutdown")) if self._mthread is not None: self._flush_mnl() self._mthread.join() if self.mnl is not None: self.mnl.close() self.mnl = None if self._nl_own: self.nl.close() self.nl = None self._flush_db() def _flush_mnl(self): if self.mnl is not None: # terminate the main loop for t in range(3): try: msg = ifinfmsg() msg['index'] = 1 msg.reset() self.mnl.put(msg, RTM_GETLINK) except Exception as e: log.error("shutdown error: %s", e) # Just give up. # We can not handle this case def create(self, kind, ifname, reuse=False, **kwarg): return self.interfaces.add(kind, ifname, reuse, **kwarg) def ensure(self, cmd='add', reachable=None, condition=None): if cmd == 'reset': self._ensure = [] elif cmd == 'run': for f in self._ensure: f() elif cmd == 'add': if isinstance(reachable, basestring): reachable = reachable.split(':') if len(reachable) == 1: f = partial(test_reachable_icmp, reachable[0]) else: raise NotImplementedError() self._ensure.append(f) else: if sys.stdin.isatty(): pprint(self._ensure, stream=self._stdout) elif cmd == 'print': pprint(self._ensure, stream=self._stdout) elif cmd == 'get': return self._ensure else: raise NotImplementedError() def items(self): # TODO: add support for filters? # iterate interfaces for ifname in getattr(self, 'by_name', {}): yield (('interfaces', ifname), self.interfaces[ifname]) # iterate routes for table in getattr(getattr(self, 'routes', None), 'tables', {}): for key, route in self.routes.tables[table].items(): yield (('routes', table, key), route) def dump(self): ret = {} for key, obj in self.items(): ptr = ret for step in key[:-1]: if step not in ptr: ptr[step] = {} ptr = ptr[step] ptr[key[-1]] = obj return ret def load(self, config, ptr=None): if ptr is None: ptr = self for key in config: obj = getattr(ptr, key, None) if obj is not None: if hasattr(obj, 'load'): obj.load(config[key]) else: self.load(config[key], ptr=obj) elif hasattr(ptr, 'add'): ptr.add(**config[key]) return self def review(self): ret = {} for key, obj in self.items(): ptr = ret try: rev = obj.review() except TypeError: continue for step in key[:-1]: if step not in ptr: ptr[step] = {} ptr = ptr[step] ptr[key[-1]] = rev if not ret: raise TypeError('no transaction started') return ret def drop(self): ok = False for key, obj in self.items(): try: obj.drop() except TypeError: continue ok = True if not ok: raise TypeError('no transaction started') def commit(self, transactions=None, phase=1): # what to commit: either from transactions argument, or from # started transactions on existing objects if transactions is None: # collect interface transactions txlist = [(x, x.current_tx) for x in getattr(self, 'by_name', {}).values() if x.local_tx.values()] # collect route transactions for table in getattr(getattr(self, 'routes', None), 'tables', {}).keys(): txlist.extend([(x, x.current_tx) for x in self.routes.tables[table] if x.local_tx.values()]) transactions = txlist snapshots = [] removed = [] tx_ipdb_prio = [] tx_main = [] tx_prio1 = [] tx_prio2 = [] tx_prio3 = [] for (target, tx) in transactions: # 8<------------------------------ # first -- explicit priorities if tx['ipdb_priority']: tx_ipdb_prio.append((target, tx)) continue # 8<------------------------------ # routes if isinstance(target, BaseRoute): tx_prio3.append((target, tx)) continue # 8<------------------------------ # intefaces kind = target.get('kind', None) if kind in ('vlan', 'vxlan', 'gre', 'tuntap', 'vti', 'vti6', 'vrf'): tx_prio1.append((target, tx)) elif kind in ('bridge', 'bond'): tx_prio2.append((target, tx)) else: tx_main.append((target, tx)) # 8<------------------------------ # explicitly sorted transactions tx_ipdb_prio = sorted(tx_ipdb_prio, key=lambda x: x[1]['ipdb_priority'], reverse=True) # FIXME: this should be documented # # The final transactions order: # 1. any txs with ipdb_priority (sorted by that field) # # Then come default priorities (no ipdb_priority specified): # 2. all the rest # 3. vlan, vxlan, gre, tuntap, vti, vrf # 4. bridge, bond # 5. routes transactions = tx_ipdb_prio + tx_main + tx_prio1 + tx_prio2 + tx_prio3 try: for (target, tx) in transactions: if target['ipdb_scope'] == 'detached': continue if tx['ipdb_scope'] == 'remove': tx['ipdb_scope'] = 'shadow' removed.append((target, tx)) if phase == 1: s = (target, target.pick(detached=True)) snapshots.append(s) # apply the changes, but NO rollback -- only phase 1 target.commit(transaction=tx, commit_phase=phase, commit_mask=phase) # if the commit above fails, the next code # branch will run rollbacks except Exception: if phase == 1: # run rollbacks for ALL the collected transactions, # even successful ones self.fallen = transactions txs = filter(lambda x: not ('create' == x[0]['ipdb_scope'] == x[1]['ipdb_scope']), snapshots) self.commit(transactions=txs, phase=2) raise else: if phase == 1: for (target, tx) in removed: target['ipdb_scope'] = 'detached' target.detach() finally: if phase == 1: for (target, tx) in transactions: target.drop(tx.uid) return self def watchdog(self, wdops='RTM_NEWLINK', **kwarg): return Watchdog(self, wdops, kwarg) def _serve_cb(self): ### # Callbacks thread working on a dedicated event queue. ### while not self._stop: msg = self._cbq.get() self._cbq.task_done() if isinstance(msg, ShutdownException): return elif isinstance(msg, Exception): raise msg for cb in tuple(self._post_callbacks.values()): try: cb(self, msg, msg['event']) except: pass def _serve_main(self): ### # Main monitoring cycle. It gets messages from the # default iproute queue and updates objects in the # database. ### while not self._stop: try: messages = self.mnl.get() ## # Check it again # # NOTE: one should not run callbacks or # anything like that after setting the # _stop flag, since IPDB is not valid # anymore if self._stop: break except Exception as e: with self.exclusive: if self._evq: self._evq.put(e) return if self.restart_on_error: log.error('Restarting IPDB instance after ' 'error:\n%s', traceback.format_exc()) try: self.initdb() except: log.error('Error restarting DB:\n%s', traceback.format_exc()) return continue else: log.error('Emergency shutdown, cleanup manually') raise RuntimeError('Emergency shutdown') for msg in messages: # Run pre-callbacks # NOTE: pre-callbacks are synchronous for (cuid, cb) in tuple(self._pre_callbacks.items()): try: cb(self, msg, msg['event']) except: pass with self.exclusive: event = msg.get('event', None) if event in self._event_map: for func in self._event_map[event]: func(msg) # Post-callbacks try: self._cbq.put_nowait(msg) if self._cbq_drop: log.warning('dropped %d events', self._cbq_drop) self._cbq_drop = 0 except queue.Full: self._cbq_drop += 1 except Exception: log.error('Emergency shutdown, cleanup manually') raise RuntimeError('Emergency shutdown') # # Why not to put these two pieces of the code # it in a routine? # # TODO: run performance tests with routines # Users event queue if self._evq: try: self._evq.put_nowait(msg) if self._evq_drop: log.warning("dropped %d events", self._evq_drop) self._evq_drop = 0 except queue.Full: self._evq_drop += 1 except Exception as e: log.error('Emergency shutdown, cleanup manually') raise RuntimeError('Emergency shutdown')
# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals import os import ipaddress import threading #import pyroute2 # for pyinstaller # again for nuitka from pyroute2.iproute import IPRoute IP = IPRoute() def _transform_attrs_inplace(result): for item in result: item['attrs'] = dict(item['attrs']) return result def is_physical_iface(iface): device_path = os.readlink(os.path.join('/sys/class/net', iface)) return not device_path.startswith('../../devices/virtual/') def get_physical_ifaces(): ifaces = os.listdir('/sys/class/net') return [iface for iface in ifaces if is_physical_iface(iface)]
class InfrasimNamespace(object): def __init__(self, vswitch_instance, ns_info): self.__ns_info = ns_info self.name = ns_info['name'] self.ip = IPRoute() # self.ipdb = IPDB(nl=NetNS(self.name)) self.main_ipdb = IPDB() self.__vswitch = vswitch_instance @staticmethod def get_namespaces_list(): return netns.listnetns() def build_one_namespace(self): self._create_namespace() for intf in self.__ns_info["interfaces"]: # get name ifname = intf["ifname"] if intf.get("pair") is False: self.create_single_virtual_intf_in_ns(intf) else: global interface_index self.create_ip_link_in_ns(ifname, "veth{}".format(interface_index)) if 'bridge' in intf: self.create_bridge(intf=ifname, br_name=intf['bridge']['ifname']) self.__vswitch.add_port("veth{}".format(interface_index)) idx = self.ip.link_lookup( ifname="veth{}".format(interface_index))[0] self.ip.link("set", index=idx, state="up") interface_index += 1 def _create_namespace(self): if self.name in self.get_namespaces_list(): print "name space {} exists.".format(self.name) return netns.create(self.name) def del_namespace(self): if self.name in netns.listnetns(): netns.remove(self.name) def create_single_virtual_intf_in_ns(self, intf): ifname = intf['ifname'] if len(self.ip.link_lookup(ifname=ifname)) > 0: print "ip link {} exists so not create it.".format(ifname) return self.main_ipdb.create(ifname=ifname, kind="dummy").commit() with self.main_ipdb.interfaces[ifname] as veth: veth.net_ns_fd = self.name if 'bridge' in intf: self.create_bridge(intf=ifname, br_name=intf['bridge']['ifname']) def create_ip_link_in_ns(self, ifname, peername): if len(self.ip.link_lookup(ifname=ifname)) > 0: print "ip link {} exists so not create it.".format(ifname) return if len(self.ip.link_lookup(ifname=peername)) > 0: print "ip link {} exists so not create it.".format(peername) return # create link peer self.main_ipdb.create(ifname=ifname, kind="veth", peer=peername).commit() with self.main_ipdb.interfaces[ifname] as veth: veth.net_ns_fd = self.name def exec_cmd_in_namespace(self, cmd): start_process(["ip", "netns", "exec", self.name] + cmd) def link_up_all(self): # setup lo # self.exec_cmd_in_namespace(["ifdown", "lo"]) # self.exec_cmd_in_namespace(["ifup", "lo"]) self.exec_cmd_in_namespace(["ip", "link", "set", "dev", "lo", "up"]) for intf_info in self.__ns_info["interfaces"]: if "bridge" in intf_info: self.exec_cmd_in_namespace( ["ip", "link", "set", "dev", intf_info["ifname"], "up"]) self.exec_cmd_in_namespace( ["ifdown", intf_info["bridge"]["ifname"]]) self.exec_cmd_in_namespace( ["ifup", intf_info["bridge"]["ifname"]]) else: self.exec_cmd_in_namespace(["ifdown", intf_info["ifname"]]) self.exec_cmd_in_namespace(["ifup", intf_info["ifname"]]) def create_bridge(self, intf="einf0", br_name="br0"): self.exec_cmd_in_namespace(["brctl", "addbr", "{}".format(br_name)]) self.exec_cmd_in_namespace( ["brctl", "addif", "{}".format(br_name), intf]) self.exec_cmd_in_namespace( ["brctl", "setfd", "{}".format(br_name), "0"]) self.exec_cmd_in_namespace( ["brctl", "sethello", "{}".format(br_name), "1"]) self.exec_cmd_in_namespace( ["brctl", "stp", "{}".format(br_name), "no"]) self.exec_cmd_in_namespace(["ifconfig", intf, "promisc"]) def build_ns_configuration(self): netns_path = "/etc/netns" ns_network_dir = os.path.join(netns_path, self.name, "network") if_down_dir = os.path.join(ns_network_dir, "if-down.d") if not os.path.exists(if_down_dir): os.makedirs(if_down_dir) if_post_down_dir = os.path.join(ns_network_dir, "if-post-down.d") if not os.path.exists(if_post_down_dir): os.makedirs(if_post_down_dir) if_pre_up_dir = os.path.join(ns_network_dir, "if-pre-up.d") if not os.path.exists(if_pre_up_dir): os.makedirs(if_pre_up_dir) if_up_dir = os.path.join(ns_network_dir, "if-up.d") if not os.path.exists(if_up_dir): os.makedirs(if_up_dir) content = "" content += "auto lo\n" content += "iface lo inet loopback\n" content += "\n" intf_list = [] for intf_info in self.__ns_info["interfaces"]: intf_obj = Interface(intf_info) intf_list.append(intf_obj) for iobj in intf_list: content += iobj.compose() with open(os.path.join(ns_network_dir, "interfaces"), "w") as f: f.write(content)
class NetworkInterfaceConfig(object): '''High-level abstraction of a network interface's configuration NetworkInterfaceConfig is designed to abstract most of the pain of work with (rt)netlink directly, and use pythonic methods for manipulating network configuration. At a high level, this interface is protocol-neutral, though only support for IPv4 and IPv6 has been added. Each function call does sanity checks to try and prevent undefined behavior, such as defining a link-local address by accident. For ease of use, this interface does not use the kernel netlink bind() functionality, and instead works in an async fashion. Manipulation of interfaces is rechecked at the end of each function to make sure that the state changed properly, and to make sure there wasn't some sort of silent failure. A single IPRoute() socket is open per instance of this class for performance reasons. This class is thread-safe. ''' def __init__(self, interface): '''Manipulations the configuration of a given interface Args: interface - name of the interface to manipulate (i.e. 'eth0') Raises: InvalidNetworkDevice - if the interface does not exist ''' self.interface = interface self.iproute_api = IPRoute() # Confirm this is a valid interface # This will chuck a IndexError exception if it doesn't exist self.interface_index = self._get_interface_index() def __del__(self): self.iproute_api.close() def _get_interface_index(self): '''Private API to get the interface index number Raises: InvalidNetworkDevice - if an interface isn't found by pyroute2 ''' try: idx = self.iproute_api.link_lookup(ifname=self.interface)[0] except IndexError: # IndexError means the interface wasn't found. Send a cleaner message up the pipe raise InvalidNetworkDevice("Interface not found") return idx def get_ips(self): '''Returns all a list IPs for a given interface. None if no IPs are configures IPs are returned in a list of dictionaries for easy enumeration. Keys that are always available: ip_address - assigned address family - protocol family of the returned IP. Either AF_INET, or AF_INET6 prefix_length - Length of the network prefix assigned to this interface This is the CIDR representation of the netmask. if family == AF_INET broadcast - broadcast address for an interface ''' # I'd like to use label= here, but IPv6 information is not returned # when doing so. Possibly a bug in pyroute2 ip_cfgs = self.iproute_api.get_addr(index=self.interface_index) # get_addr returns IPs in a nasty to praise format. We need to look at the attrs # section of each item we get, and find IFA_ADDRESS, and check the second element # of the tuple to get the IP address # Here's an example of what we're praising # # [{'attrs': [['IFA_ADDRESS', '10.0.241.123'], # ['IFA_LOCAL', '10.0.241.123'], # ['IFA_BROADCAST', '10.0.241.255'], # ['IFA_LABEL', 'dummy0'], # ['IFA_CACHEINFO', # {'cstamp': 181814615, # 'ifa_prefered': 4294967295, # 'ifa_valid': 4294967295, # 'tstamp': 181814615}]], # 'event': 'RTM_NEWADDR', # 'family': 2, # 'flags': 128, # 'header': {'error': None, # 'flags': 2, # 'length': 80, # 'pid': 4294395048, # 'sequence_number': 255, # 'type': 20}, # 'index': 121, # 'prefixlen': 24, # 'scope': 0}]''' # Walk the list ips = [] for ip_cfg in ip_cfgs: ip_address = None family = None broadcast = None prefixlen = None ip_attrs = ip_cfg['attrs'] for attribute in ip_attrs: if attribute[0] == "IFA_ADDRESS": ip_address = attribute[1] if attribute[0] == "IFA_BROADCAST": broadcast = attribute[1] prefixlen = ip_cfg['prefixlen'] family = ip_cfg['family'] # 2 for AF_INET, 10 for AF_INET6 # Build IP dictionary ip_dict = {'ip_address': ip_address, 'prefix_length': prefixlen, 'family' : family} # Handle v4-only information if broadcast: ip_dict['broadcast'] = broadcast # Push it onto the list to be returned ips.append(ip_dict) # And break! return ips def get_full_ip_info(self, wanted_address): '''Returns an ip_dict for an individual IP address. Format identical to get_ips() Args: wanted_address - a valid v4 or v6 address to search for. Value is normalized by ipaddress.ip_address when returning Raises: ValueError - wanted_address is not a valid IP address IPNotFound - this interface doesn't have this IP address ''' wanted_address = check.validate_and_normalize_ip(wanted_address) ips = self.get_ips() # Walk the IP table and find the specific IP we want for ip_address in ips: if ip_address['ip_address'] == wanted_address: return ip_address # If we get here, the IP wasn't found raise IPNotFound("IP not found on interface") def add_v4_ip(self, ip_address, prefix_length): '''Wrapper for add_ip - adds an IPv4 address to this interface Args: ip_address = IPv4 address to add prefix_length - Network prefix size; netmask and broadcast addresses Raises: - see add_ip() ''' ip_dict = {'ip_address': ip_address, 'family': AF_INET, 'prefix_length': prefix_length} self.add_ip(ip_dict) def add_v6_ip(self, ip_address, prefix_length): '''Wrapper for add_ip - adds an IPv6 address to this interface Args: ip_address - IPv6 address to add prefix_length - Network prefix size; netmask and broadcast addresses Raises: - see add_ip() ''' ip_dict = {'ip_address': ip_address, 'family': AF_INET6, 'prefix_length': prefix_length} self.add_ip(ip_dict) def add_ip(self, ip_dict): '''Adds an IP to an interface. Lower-level function to add an IP address Args: ip_dict - takes an IP dictionary (see get_ips) and adds it to an interface directorly Raises: ValueError IP address invalid. See message for more info DuplicateIPError This IP is already configured InterfaceConfigurationError The ip_dict was valid, but the IP failed add ''' check.validate_and_normalize_ip_dict(ip_dict) # Throw an error if we try to add an existing address. existing_ip_check = None try: existing_ip_check = self.get_full_ip_info(ip_dict['ip_address']) except IPNotFound: pass if existing_ip_check: raise DuplicateIPError("This IP has already been assigned!") # We call add slightly differently based on socket family if ip_dict['family'] == AF_INET: self.iproute_api.addr('add', index=self.interface_index, family=AF_INET, address=ip_dict['ip_address'], broadcast=ip_dict['broadcast'], prefixlen=ip_dict['prefix_length']) if ip_dict['family'] == AF_INET6: self.iproute_api.addr('add', index=self.interface_index, family=AF_INET6, address=ip_dict['ip_address'], prefixlen=ip_dict['prefix_length']) # Do a sanity check and make sure the IP actually got added ip_check = self.get_full_ip_info(ip_dict['ip_address']) if not (ip_check['ip_address'] == ip_dict['ip_address'] and ip_check['prefix_length'] == ip_dict['prefix_length']): raise InterfaceConfigurationError("IP failed to add!") def remove_ip(self, ip_address): '''Removes an IP from an interface. Full details are looked up via get_full_ip_info for removal Args: ip_address - IP address to remove Raises: ValueError The IP address provided was invalid IPNotFound The IP is not configured on this interface InterfaceConfigurationError The IP address was valid, but the IP was not successfully removed ''' # San check ip_address = check.validate_and_normalize_ip(ip_address) # Get the full set of IP information ip_info = self.get_full_ip_info(ip_address) # Attempt to delete if ip_info['family'] == AF_INET: self.iproute_api.addr('delete', index=self.interface_index, address=ip_info['ip_address'], broadcast=ip_info['broadcast'], prefixlen=ip_info['prefix_length']) if ip_info['family'] == AF_INET6: self.iproute_api.addr('delete', index=self.interface_index, address=ip_info['ip_address'], prefixlen=ip_info['prefix_length']) # Confirm the delete. get_full_ip_info will throw an exception if it can't find it try: self.get_full_ip_info(ip_address) except IPNotFound: # We got it! return # Didn't get it. Throw an exception and bail raise InterfaceConfigurationError("IP deletion failure") def add_default_gateway(self, gateway, prefix_length): '''Adds a default gateway for a given prefix length''' raise NotImplementedError def add_static_route(self, source, destination): '''Sets a static route for an interface''' raise NotImplementedError def add_route(self, route_info): '''Adds a route for a given interface''' raise NotImplementedError def remove_route(self, route_info): '''Removes a route from an interface''' raise NotImplementedError def get_routes(self): '''Gets routes for an interface''' # The only way to get routes for an interface is to pull the entire routing table, and # filter entries for this interface. Miserable interfaces are miserable. Furthermore, # I can only get the v4 and v6 routes as a separate transaction # In theory, we can apply a filter to returned routes, but I can't figure out if # we can exclude things like cache entries, so we'll just grab stuff on a per family # level, and filter for ourselves # Pull the v4 and v6 global routing table v4_routing_table = self.iproute_api.get_routes(family=AF_INET) v6_routing_table = self.iproute_api.get_routes(family=AF_INET6) kernel_routing_table = self._filter_routing_table(v4_routing_table + v6_routing_table) # _filter_routing_table got rid of most of the junk we don't care about # so now we need to walk the table and make it something far similar to # praise without ripping our hair out. We also filter out link-local # addresses in this step because we need the full prefix to know if its # a link-local network routing_table = [] for route in kernel_routing_table: route_dict = {} # Let's get the easy stuff first for attribute in route['attrs']: if attribute[0] == 'RTA_PREFSRC': route_dict['source'] = attribute[1] if attribute[0] == 'RTA_DST': route_dict['destination'] = attribute[1] if attribute[0] == 'RTA_GATEWAY': route_dict['gateway'] = attribute[1] # Family is mapped straight through so AF_INET and AF_INET6 just match route_dict['family'] = route['family'] # Attach prefixes if they're non-zero if route['src_len'] != 0 and 'source' in route_dict: route_dict['source'] += ("/%s" % route['src_len']) if route['dst_len'] != 0 and 'destination' in route_dict: route_dict['destination'] += ("/%s" % route['dst_len']) # Check for link-local here if ipaddress.ip_network(route_dict['destination']).is_link_local: continue # skip the route if route['dst_len'] != 0 and 'gateway' in route_dict: route_dict['gateway'] += ("/%s" % route['dst_len']) # Map the protocol to something human-readable route_dict['protocol'] = map_protocol_number_to_string(route['proto']) route_dict['type'] = determine_route_type(route_dict) routing_table.append(route_dict) return routing_table def determine_if_route_exists(self): '''Checks if a route exists''' raise NotImplementedError def validate_route_dict(self): '''Validates a routing information dict''' raise NotImplementedError def _filter_routing_table(self, routing_table): '''Internal API. Takes output of get_routes, and filters down to what we care about''' # For every configured IP address, a couple of automatic routes are # generated that we're not interested in. # # Understanding this code requires an understanding of how the Linux kernel # handles routing and routing tables. For each interface, the kernel has a possible # 255 tables to hold routes. These exist to allow for specific routing rules and # preferences as the system goes from table 255->1 in determining which route # will be used. # # On a default configuration, only three routing tables are defined: # 255 - local # 254 - main # 253 - default # # Table names are controlled in /etc/iproutes.d/rt_tables # # The local table is special as it can only be added to by the kernel, and removing # entries from it is explicitly done "at your own risk". It defines which IPs this # machine owns so any attempt to communicate on it comes back to itself. As such # we can simply filter out 255 to make our lives considerably easier # # Unfortunately, filtering 255 doesn't get rid of all the "line noise" so to speak. # # From this point forward, I've had to work from kernel source, and the source of # iproute2 to understand what's going on from netlayer. But basically, here's the # rundown of what we need to do # # Case 1 - Cached Entries # This is a surprising complicated case. Cached entries are used by the kernel for # automatically configured routes. I haven't seen the kernel v4 table populated, but # that may just be because of my local usage # # IPv6 is a different story. Routing information for IPv6 can come in the form of # routing announcements, static configuration, and so forth. It seems all IPv6 info is # is marked as a cached entry. This is made more complicated that the kernel stores # various IPv6 routing information in the table for "random" hosts accessed. For example. # on my home system ... # # mcasadevall@perdition:~/workspace/mcdynipd$ route -6 -C # Kernel IPv6 routing cache # Destination Next Hop Flag Met Ref Use If # 2001:4860:4860::8888/128 fe80::4216:7eff:fe6c:492 UGDAC 0 0 47 eth0 # 2607:f8b0:4001:c09::bc/128 fe80::4216:7eff:fe6c:492 UGDAC 0 1 17 eth0 # 2607:f8b0:4004:808::1013/128 fe80::4216:7eff:fe6c:492 UGDAC 0 1 21 eth0 # 2607:f8b0:4009:805::1005/128 fe80::4216:7eff:fe6c:492 UGDAC 0 0 3 eth0 # 2607:f8b0:4009:80a::200e/128 fe80::4216:7eff:fe6c:492 UGDAC 0 0 85 eth0 # 2607:f8b0:400d:c04::bd/128 fe80::4216:7eff:fe6c:492 UGDAC 0 1 76 eth0 # # 2607:f8b0::/32 is owned by Google, and these were connections my system made to Google # systems. Looking at my router (which runs a 6to4 HE tunnel), it appears 6to4 is handled # via static routing and should show up sans CACHEINFO (untested - needs confirmation). # # Digging into iproute2, the proto field defines what defined a route. Here's the list # defined in the source # # ---------------------------------------------------------------------------- # #define RTPROT_UNSPEC 0 # #define RTPROT_REDIRECT 1 /* Route installed by ICMP redirects; # not used by current IPv4 */ # #define RTPROT_KERNEL 2 /* Route installed by kernel */ # #define RTPROT_BOOT 3 /* Route installed during boot */ # #define RTPROT_STATIC 4 /* Route installed by administrator */ # # /* Values of protocol >= RTPROT_STATIC are not interpreted by kernel; # they are just passed from user and back as is. # It will be used by hypothetical multiple routing daemons. # Note that protocol values should be standardized in order to # avoid conflicts. # */ # # #define RTPROT_GATED 8 /* Apparently, GateD */ # #define RTPROT_RA 9 /* RDISC/ND router advertisements */ # #define RTPROT_MRT 10 /* Merit MRT */ # #define RTPROT_ZEBRA 11 /* Zebra */ # #define RTPROT_BIRD 12 /* BIRD */ # #define RTPROT_DNROUTED 13 /* DECnet routing daemon */ # #define RTPROT_XORP 14 /* XORP */ # #define RTPROT_NTK 15 /* Netsukuku */ # #define RTPROT_DHCP 16 /* DHCP client */ # #define RTPROT_MROUTED 17 /* Multicast daemon */ # ---------------------------------------------------------------------------- # # Looking at the behavior of the kernel, if a given prefix has a route, it will # place a proto 2 entry for it with no routing address. Cached table entries # exist to the next point: # # 2001:4860:4860::8888/128 fe80::4216:7eff:fe6c:492 UGDAC 0 0 47 eth0 # # (this shows up as proto 9 in the routing table) # # To get the default route of a device in IPv6, we need entries that ONLY have RTA_GATEWAY # and not RTA_DEST, regardless of protocol. Otherwise, we filter out proto 9. This gets # output identical to route aside from the multicast address (ff00::/8) # # As a final note to this saga, after I coded this, I found rtnetlink is documented on # Linux systems. Run man 7 rtnetlink to save yourself a source dive :( filtered_table = [] non_cached_table = [] # Pass 1. Exclude table 255, and any entries that have RTA_CACHEINFO for route in routing_table: # If this is a 255 entry, ignore it, we don't care if route['table'] == 255: continue # Now the table cache cached_entry = False destination_address = None gateway_address = None routing_attributes = route['attrs'] for attribute in routing_attributes: if attribute[0] == 'RTA_CACHEINFO': cached_entry = True if attribute[0] == 'RTA_DST': destination_address = attribute[1] if attribute[0] == 'RTA_GATEWAY': gateway_address = attribute[1] # If its not a cached entry, always keep it if not cached_entry: non_cached_table.append(route) continue # Keep it if proto != 9 if route['proto'] != 9: non_cached_table.append(route) continue # If it only has DST or GATEWAY, its a default route, keep it if ((gateway_address and not destination_address) or (destination_address and not gateway_address)): non_cached_table.append(route) continue for route in non_cached_table: # Like IP addresses, most of the route information is stored # in attributes. RTA_OIF (OIF = Outbound Interface), contains # the index number of the interface this route is assigned to routing_attributes = route['attrs'] for attribute in routing_attributes: if attribute[0] == 'RTA_OIF' and attribute[1] == self.interface_index: filtered_table.append(route) # filtered_table should just contain our OIFs, pass this back up for # processing return filtered_table
class IPDB(object): ''' The class that maintains information about network setup of the host. Monitoring netlink events allows it to react immediately. It uses no polling. ''' def __init__(self, nl=None, mode='implicit', restart_on_error=None, nl_async=None, nl_bind_groups=RTNL_GROUPS, ignore_rtables=None, callbacks=None, sort_addresses=False): self.mode = mode self.sort_addresses = sort_addresses self._event_map = {} self._deferred = {} self._loaded = set() self._mthread = None self._nl_own = nl is None self._nl_async = config.ipdb_nl_async if nl_async is None else True self.mnl = None self.nl = nl self.nl_bind_groups = nl_bind_groups self._plugins = [interface, route, rule] if isinstance(ignore_rtables, int): self._ignore_rtables = [ignore_rtables, ] elif isinstance(ignore_rtables, (list, tuple, set)): self._ignore_rtables = ignore_rtables else: self._ignore_rtables = [] self._stop = False # see also 'register_callback' self._post_callbacks = {} self._pre_callbacks = {} self._cb_threads = {} # locks and events self.exclusive = threading.RLock() self._shutdown_lock = threading.Lock() # register callbacks # # examples:: # def cb1(ipdb, msg, event): # print(event, msg) # def cb2(...): # ... # # # default mode: post # IPDB(callbacks=[cb1, cb2]) # # specify the mode explicitly # IPDB(callbacks=[(cb1, 'pre'), (cb2, 'post')]) # for cba in callbacks or []: if not isinstance(cba, (tuple, list, set)): cba = (cba, ) self.register_callback(*cba) # load information self.restart_on_error = restart_on_error if \ restart_on_error is not None else nl is None # init the database self.initdb() # atexit.register(self.release) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.release() def initdb(self): # common event map, empty by default, so all the # events aer just ignored self.release(complete=False) self._stop = False # explicitly cleanup object references for event in tuple(self._event_map): del self._event_map[event] # if the command socket is not provided, create it if self._nl_own: self.nl = IPRoute() # setup monitoring socket self.mnl = self.nl.clone() try: self.mnl.bind(groups=self.nl_bind_groups, async=self._nl_async) except: self.mnl.close() if self._nl_own is None: self.nl.close() raise # explicitly cleanup references for key in tuple(self._deferred): del self._deferred[key] for module in self._plugins: if (module.groups & self.nl_bind_groups) != module.groups: continue for plugin in module.spec: self._deferred[plugin['name']] = module.spec if plugin['name'] in self._loaded: delattr(self, plugin['name']) self._loaded.remove(plugin['name']) # start the monitoring thread self._mthread = threading.Thread(name="IPDB event loop", target=self.serve_forever) self._mthread.setDaemon(True) self._mthread.start() def __getattribute__(self, name): deferred = super(IPDB, self).__getattribute__('_deferred') if name in deferred: register = [] spec = deferred[name] for plugin in spec: obj = plugin['class'](self, **plugin['kwarg']) setattr(self, plugin['name'], obj) register.append(obj) self._loaded.add(plugin['name']) del deferred[plugin['name']] for obj in register: if hasattr(obj, '_register'): obj._register() if hasattr(obj, '_event_map'): for event in obj._event_map: if event not in self._event_map: self._event_map[event] = [] self._event_map[event].append(obj._event_map[event]) return super(IPDB, self).__getattribute__(name) def register_callback(self, callback, mode='post'): ''' IPDB callbacks are routines executed on a RT netlink message arrival. There are two types of callbacks: "post" and "pre" callbacks. ... "Post" callbacks are executed after the message is processed by IPDB and all corresponding objects are created or deleted. Using ipdb reference in "post" callbacks you will access the most up-to-date state of the IP database. "Post" callbacks are executed asynchronously in separate threads. These threads can work as long as you want them to. Callback threads are joined occasionally, so for a short time there can exist stopped threads. ... "Pre" callbacks are synchronous routines, executed before the message gets processed by IPDB. It gives you the way to patch arriving messages, but also places a restriction: until the callback exits, the main event IPDB loop is blocked. Normally, only "post" callbacks are required. But in some specific cases "pre" also can be useful. ... The routine, `register_callback()`, takes two arguments: - callback function - mode (optional, default="post") The callback should be a routine, that accepts three arguments:: cb(ipdb, msg, action) Arguments are: - **ipdb** is a reference to IPDB instance, that invokes the callback. - **msg** is a message arrived - **action** is just a msg['event'] field E.g., to work on a new interface, you should catch action == 'RTM_NEWLINK' and with the interface index (arrived in msg['index']) get it from IPDB:: index = msg['index'] interface = ipdb.interfaces[index] ''' lock = threading.Lock() def safe(*argv, **kwarg): with lock: callback(*argv, **kwarg) safe.hook = callback safe.lock = lock safe.uuid = uuid32() if mode == 'post': self._post_callbacks[safe.uuid] = safe elif mode == 'pre': self._pre_callbacks[safe.uuid] = safe return safe.uuid def unregister_callback(self, cuid, mode='post'): if mode == 'post': cbchain = self._post_callbacks elif mode == 'pre': cbchain = self._pre_callbacks else: raise KeyError('Unknown callback mode') safe = cbchain[cuid] with safe.lock: cbchain.pop(cuid) for t in tuple(self._cb_threads.get(cuid, ())): t.join(3) ret = self._cb_threads.get(cuid, ()) return ret def release(self, complete=True): ''' Shutdown IPDB instance and sync the state. Since IPDB is asyncronous, some operations continue in the background, e.g. callbacks. So, prior to exit the script, it is required to properly shutdown IPDB. The shutdown sequence is not forced in an interactive python session, since it is easier for users and there is enough time to sync the state. But for the scripts the `release()` call is required. ''' with self._shutdown_lock: if self._stop: return self._stop = True if self.mnl is not None: # terminate the main loop for t in range(3): try: msg = ifinfmsg() msg['index'] = 1 msg.reset() self.mnl.put(msg, RTM_GETLINK) except Exception as e: logging.warning("shutdown error: %s", e) # Just give up. # We can not handle this case if self._mthread is not None: self._mthread.join() if self.mnl is not None: self.mnl.close() self.mnl = None if complete or self._nl_own: self.nl.close() self.nl = None with self.exclusive: # collect all the callbacks for cuid in tuple(self._cb_threads): for t in tuple(self._cb_threads[cuid]): t.join() # flush all the objects def flush(idx): for key in tuple(idx.keys()): try: del idx[key] except KeyError: pass idx_list = [] if 'interfaces' in self._loaded: for (key, dev) in self.by_name.items(): try: # FIXME self.interfaces._detach(key, dev['index'], dev.nlmsg) except KeyError: pass idx_list.append(self.ipaddr) idx_list.append(self.neighbours) if 'routes' in self._loaded: idx_list.extend([self.routes.tables[x] for x in self.routes.tables.keys()]) if 'rules' in self._loaded: idx_list.append(self.rules) for idx in idx_list: flush(idx) def create(self, kind, ifname, reuse=False, **kwarg): return self.interfaces.add(kind, ifname, reuse, **kwarg) def commit(self, transactions=None, phase=1): # what to commit: either from transactions argument, or from # started transactions on existing objects if transactions is None: # collect interface transactions txlist = [(x, x.current_tx) for x in self.by_name.values() if x.local_tx.values()] # collect route transactions for table in self.routes.tables.keys(): txlist.extend([(x, x.current_tx) for x in self.routes.tables[table] if x.local_tx.values()]) txlist = sorted(txlist, key=lambda x: x[1]['ipdb_priority'], reverse=True) transactions = txlist snapshots = [] removed = [] try: for (target, tx) in transactions: if target['ipdb_scope'] == 'detached': continue if tx['ipdb_scope'] == 'remove': tx['ipdb_scope'] = 'shadow' removed.append((target, tx)) if phase == 1: s = (target, target.pick(detached=True)) snapshots.append(s) target.commit(transaction=tx, commit_phase=phase, commit_mask=phase) except Exception: if phase == 1: self.fallen = transactions self.commit(transactions=snapshots, phase=2) raise else: if phase == 1: for (target, tx) in removed: target['ipdb_scope'] = 'detached' target.detach() finally: if phase == 1: for (target, tx) in transactions: target.drop(tx.uid) def watchdog(self, action='RTM_NEWLINK', **kwarg): return Watchdog(self, action, kwarg) def serve_forever(self): ### # Main monitoring cycle. It gets messages from the # default iproute queue and updates objects in the # database. # # Should not be called manually. ### while not self._stop: try: messages = self.mnl.get() ## # Check it again # # NOTE: one should not run callbacks or # anything like that after setting the # _stop flag, since IPDB is not valid # anymore if self._stop: break except: log.error('Restarting IPDB instance after ' 'error:\n%s', traceback.format_exc()) if self.restart_on_error: try: self.initdb() except: log.error('Error restarting DB:\n%s', traceback.format_exc()) return continue else: raise RuntimeError('Emergency shutdown') for msg in messages: # Run pre-callbacks # NOTE: pre-callbacks are synchronous for (cuid, cb) in tuple(self._pre_callbacks.items()): try: cb(self, msg, msg['event']) except: pass with self.exclusive: event = msg.get('event', None) if event in self._event_map: for func in self._event_map[event]: func(msg) # run post-callbacks # NOTE: post-callbacks are asynchronous for (cuid, cb) in tuple(self._post_callbacks.items()): t = threading.Thread(name="IPDB callback %s" % (id(cb)), target=cb, args=(self, msg, msg['event'])) t.start() if cuid not in self._cb_threads: self._cb_threads[cuid] = set() self._cb_threads[cuid].add(t) # occasionally join cb threads for cuid in tuple(self._cb_threads): for t in tuple(self._cb_threads.get(cuid, ())): t.join(0) if not t.is_alive(): try: self._cb_threads[cuid].remove(t) except KeyError: pass if len(self._cb_threads.get(cuid, ())) == 0: del self._cb_threads[cuid] def init_ipaddr_set(self): if self.sort_addresses: return SortedIPaddrSet() else: return IPaddrSet()
def __init__(self, *args, **kwargs): super(InterfaceConfigTest, self).__init__(*args, **kwargs) self.iproute_api = IPRoute()
def main(): """ Program entry point when run interactively. """ # prepare option parser parser = ArgumentParser(usage="usage: %(prog)s [options]", description="Executes a UDP broadcast to test for " "connectivity") parser.add_argument("-i", "--interface", dest="interface", default="poprow0", action="store", metavar="ifname", help="Interface to use [default: %(default)s]") parser.add_argument("-p", "--port", dest="port", type=int, default=12345, action="store", help="UDP port [default: %(default)s]", metavar="PORT") parser.add_argument( "-n", "--count", dest="count", type=int, default=100, action="store", help="Number of packets to send [default: %(default)s]", metavar="COUNT") parser.add_argument("-t", "--interval", dest="interval", type=float, default=0.2, action="store", help="Packet interval [default: %(default)s]") parser.add_argument("-v", "--verbose", dest="verbose", action="store_true") # parse options args = parser.parse_args() # copy options interface = args.interface port = args.port count = args.count interval = args.interval verbose = args.verbose ip = IPRoute() if_index = ip.link_lookup(ifname=interface) if len(if_index) == 0: print >> sys.stderr, "Can't find interface %s" % interface sys.exit(1) # from the given interface find unicast and broadcast addresses if_info = ip.get_addr(index=if_index[0])[0] address = if_info.get_attr("IFA_ADDRESS") netmask = if_info["prefixlen"] network = ipaddr.IPv4Network("%s/%d" % (address, netmask)) broadcast = str(network.broadcast) # get hostname as additional information to send in the packets hostname = socket.gethostname() sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) i = 0 run = True while i < count and run: try: payload = "POPROWPING %d %s" % (i, hostname) if verbose: print >> sys.stderr, "Sending packet #%d to %s:%d" % \ (i+1, broadcast, port) sock.sendto(payload, (broadcast, port)) i += 1 time.sleep(interval) except SystemExit: run = False if verbose: print >> sys.stderr, "Catching SystemExit. Quitting" except KeyboardInterrupt: run = False if verbose: print >> sys.stderr, "Keyboard Interrupt. Quitting" except: run = False if verbose: print >> sys.stderr, "Error while sending packet. Quitting" sock.close()
import os import shutil import json import re import subprocess import yaml from pyroute2 import netlink from pyroute2 import IPDB from pyroute2 import netns from pyroute2.iproute import IPRoute IP_ROUTE = IPRoute() MAIN_IPDB = IPDB() def start_process(args): """ Shell command agent """ try: p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() return (p.returncode, out, err) except OSError: return (-1, None, None) def exec_cmd_in_namespace(ns, cmd):
def NetNServer(netns, rcvch, cmdch, flags=os.O_CREAT): ''' The netns server supposed to be started automatically by NetNS. It has two communication channels: one simplex to forward incoming netlink packets, `rcvch`, and other synchronous duplex to get commands and send back responses, `cmdch`. Channels should support standard socket API, should be compatible with poll/select and should be able to transparently pickle objects. NetNS uses `multiprocessing.Pipe` for this purpose, but it can be any other implementation with compatible API. The first parameter, `netns`, is a netns name. Depending on the `flags`, the netns can be created automatically. The `flags` semantics is exactly the same as for `open(2)` system call. ... The server workflow is simple. The startup sequence:: 1. Create or open a netns. 2. Start `IPRoute` instance. It will be used only on the low level, the `IPRoute` will not parse any packet. 3. Start poll/select loop on `cmdch` and `IPRoute`. On the startup, the server sends via `cmdch` the status packet. It can be `None` if all is OK, or some exception. Further data handling, depending on the channel, server side:: 1. `IPRoute`: read an incoming netlink packet and send it unmodified to the peer via `rcvch`. The peer, polling `rcvch`, can handle the packet on its side. 2. `cmdch`: read tuple (cmd, argv, kwarg). If the `cmd` starts with "send", then take `argv[0]` as a packet buffer, treat it as one netlink packet and substitute PID field (offset 12, uint32) with its own. Strictly speaking, it is not mandatory for modern netlink implementations, but it is required by the protocol standard. ''' try: nsfd = setns(netns, flags) except OSError as e: cmdch.send(e) return e.errno except Exception as e: cmdch.send(OSError(errno.ECOMM, str(e), netns)) return 255 # try: ipr = IPRoute() rcvch_lock = ipr._sproxy.lock ipr._s_channel = rcvch poll = select.poll() poll.register(ipr, select.POLLIN | select.POLLPRI) poll.register(cmdch, select.POLLIN | select.POLLPRI) except Exception as e: cmdch.send(e) return 255 # all is OK so far cmdch.send(None) # 8<------------------------------------------------------------- while True: events = poll.poll() for (fd, event) in events: if fd == ipr.fileno(): bufsize = ipr.getsockopt(SOL_SOCKET, SO_RCVBUF) // 2 with rcvch_lock: rcvch.send(ipr.recv(bufsize)) elif fd == cmdch.fileno(): try: cmdline = cmdch.recv() if cmdline is None: poll.unregister(ipr) poll.unregister(cmdch) ipr.close() os.close(nsfd) return (cmd, argv, kwarg) = cmdline if cmd[:4] == 'send': # Achtung # # It's a hack, but we just have to do it: one # must use actual pid in netlink messages # # FIXME: there can be several messages in one # call buffer; but right now we can ignore it msg = argv[0][:12] msg += struct.pack("I", os.getpid()) msg += argv[0][16:] argv = list(argv) argv[0] = msg cmdch.send(getattr(ipr, cmd)(*argv, **kwarg)) except Exception as e: e.tb = traceback.format_exc() cmdch.send(e)
class IPDB(object): ''' The class that maintains information about network setup of the host. Monitoring netlink events allows it to react immediately. It uses no polling. ''' def __init__(self, nl=None, mode='implicit', restart_on_error=None, nl_async=None, nl_bind_groups=RTNL_GROUPS, ignore_rtables=None, callbacks=None, sort_addresses=False, plugins=None): plugins = plugins or ['interfaces', 'routes', 'rules'] pmap = {'interfaces': interfaces, 'routes': routes, 'rules': rules} self.mode = mode self.sort_addresses = sort_addresses self._event_map = {} self._deferred = {} self._loaded = set() self._mthread = None self._nl_own = nl is None self._nl_async = config.ipdb_nl_async if nl_async is None else True self.mnl = None self.nl = nl self.nl_bind_groups = nl_bind_groups self._plugins = [pmap[x] for x in plugins if x in pmap] if isinstance(ignore_rtables, int): self._ignore_rtables = [ ignore_rtables, ] elif isinstance(ignore_rtables, (list, tuple, set)): self._ignore_rtables = ignore_rtables else: self._ignore_rtables = [] self._stop = False # see also 'register_callback' self._post_callbacks = {} self._pre_callbacks = {} self._cb_threads = {} # locks and events self.exclusive = threading.RLock() self._shutdown_lock = threading.Lock() # register callbacks # # examples:: # def cb1(ipdb, msg, event): # print(event, msg) # def cb2(...): # ... # # # default mode: post # IPDB(callbacks=[cb1, cb2]) # # specify the mode explicitly # IPDB(callbacks=[(cb1, 'pre'), (cb2, 'post')]) # for cba in callbacks or []: if not isinstance(cba, (tuple, list, set)): cba = (cba, ) self.register_callback(*cba) # load information self.restart_on_error = restart_on_error if \ restart_on_error is not None else nl is None # init the database self.initdb() # atexit.register(self.release) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.release() def initdb(self): # common event map, empty by default, so all the # events aer just ignored self.release(complete=False) self._stop = False # explicitly cleanup object references for event in tuple(self._event_map): del self._event_map[event] # if the command socket is not provided, create it if self._nl_own: self.nl = IPRoute() # setup monitoring socket self.mnl = self.nl.clone() try: self.mnl.bind(groups=self.nl_bind_groups, async=self._nl_async) except: self.mnl.close() if self._nl_own is None: self.nl.close() raise # explicitly cleanup references for key in tuple(self._deferred): del self._deferred[key] for module in self._plugins: if (module.groups & self.nl_bind_groups) != module.groups: continue for plugin in module.spec: self._deferred[plugin['name']] = module.spec if plugin['name'] in self._loaded: delattr(self, plugin['name']) self._loaded.remove(plugin['name']) # start the monitoring thread self._mthread = threading.Thread(name="IPDB event loop", target=self.serve_forever) self._mthread.setDaemon(True) self._mthread.start() def __getattribute__(self, name): deferred = super(IPDB, self).__getattribute__('_deferred') if name in deferred: register = [] spec = deferred[name] for plugin in spec: obj = plugin['class'](self, **plugin['kwarg']) setattr(self, plugin['name'], obj) register.append(obj) self._loaded.add(plugin['name']) del deferred[plugin['name']] for obj in register: if hasattr(obj, '_register'): obj._register() if hasattr(obj, '_event_map'): for event in obj._event_map: if event not in self._event_map: self._event_map[event] = [] self._event_map[event].append(obj._event_map[event]) return super(IPDB, self).__getattribute__(name) def register_callback(self, callback, mode='post'): ''' IPDB callbacks are routines executed on a RT netlink message arrival. There are two types of callbacks: "post" and "pre" callbacks. ... "Post" callbacks are executed after the message is processed by IPDB and all corresponding objects are created or deleted. Using ipdb reference in "post" callbacks you will access the most up-to-date state of the IP database. "Post" callbacks are executed asynchronously in separate threads. These threads can work as long as you want them to. Callback threads are joined occasionally, so for a short time there can exist stopped threads. ... "Pre" callbacks are synchronous routines, executed before the message gets processed by IPDB. It gives you the way to patch arriving messages, but also places a restriction: until the callback exits, the main event IPDB loop is blocked. Normally, only "post" callbacks are required. But in some specific cases "pre" also can be useful. ... The routine, `register_callback()`, takes two arguments: - callback function - mode (optional, default="post") The callback should be a routine, that accepts three arguments:: cb(ipdb, msg, action) Arguments are: - **ipdb** is a reference to IPDB instance, that invokes the callback. - **msg** is a message arrived - **action** is just a msg['event'] field E.g., to work on a new interface, you should catch action == 'RTM_NEWLINK' and with the interface index (arrived in msg['index']) get it from IPDB:: index = msg['index'] interface = ipdb.interfaces[index] ''' lock = threading.Lock() def safe(*argv, **kwarg): with lock: callback(*argv, **kwarg) safe.hook = callback safe.lock = lock safe.uuid = uuid32() if mode == 'post': self._post_callbacks[safe.uuid] = safe elif mode == 'pre': self._pre_callbacks[safe.uuid] = safe return safe.uuid def unregister_callback(self, cuid, mode='post'): if mode == 'post': cbchain = self._post_callbacks elif mode == 'pre': cbchain = self._pre_callbacks else: raise KeyError('Unknown callback mode') safe = cbchain[cuid] with safe.lock: cbchain.pop(cuid) for t in tuple(self._cb_threads.get(cuid, ())): t.join(3) ret = self._cb_threads.get(cuid, ()) return ret def release(self, complete=True): ''' Shutdown IPDB instance and sync the state. Since IPDB is asyncronous, some operations continue in the background, e.g. callbacks. So, prior to exit the script, it is required to properly shutdown IPDB. The shutdown sequence is not forced in an interactive python session, since it is easier for users and there is enough time to sync the state. But for the scripts the `release()` call is required. ''' with self._shutdown_lock: if self._stop: return self._stop = True if self.mnl is not None: # terminate the main loop for t in range(3): try: msg = ifinfmsg() msg['index'] = 1 msg.reset() self.mnl.put(msg, RTM_GETLINK) except Exception as e: logging.warning("shutdown error: %s", e) # Just give up. # We can not handle this case if self._mthread is not None: self._mthread.join() if self.mnl is not None: self.mnl.close() self.mnl = None if complete or self._nl_own: self.nl.close() self.nl = None with self.exclusive: # collect all the callbacks for cuid in tuple(self._cb_threads): for t in tuple(self._cb_threads[cuid]): t.join() # flush all the objects def flush(idx): for key in tuple(idx.keys()): try: del idx[key] except KeyError: pass idx_list = [] if 'interfaces' in self._loaded: for (key, dev) in self.by_name.items(): try: # FIXME self.interfaces._detach(key, dev['index'], dev.nlmsg) except KeyError: pass idx_list.append(self.ipaddr) idx_list.append(self.neighbours) if 'routes' in self._loaded: idx_list.extend( [self.routes.tables[x] for x in self.routes.tables.keys()]) if 'rules' in self._loaded: idx_list.append(self.rules) for idx in idx_list: flush(idx) def create(self, kind, ifname, reuse=False, **kwarg): return self.interfaces.add(kind, ifname, reuse, **kwarg) def commit(self, transactions=None, phase=1): # what to commit: either from transactions argument, or from # started transactions on existing objects if transactions is None: # collect interface transactions txlist = [(x, x.current_tx) for x in self.by_name.values() if x.local_tx.values()] # collect route transactions for table in self.routes.tables.keys(): txlist.extend([(x, x.current_tx) for x in self.routes.tables[table] if x.local_tx.values()]) txlist = sorted(txlist, key=lambda x: x[1]['ipdb_priority'], reverse=True) transactions = txlist snapshots = [] removed = [] try: for (target, tx) in transactions: if target['ipdb_scope'] == 'detached': continue if tx['ipdb_scope'] == 'remove': tx['ipdb_scope'] = 'shadow' removed.append((target, tx)) if phase == 1: s = (target, target.pick(detached=True)) snapshots.append(s) target.commit(transaction=tx, commit_phase=phase, commit_mask=phase) except Exception: if phase == 1: self.fallen = transactions self.commit(transactions=snapshots, phase=2) raise else: if phase == 1: for (target, tx) in removed: target['ipdb_scope'] = 'detached' target.detach() finally: if phase == 1: for (target, tx) in transactions: target.drop(tx.uid) def watchdog(self, action='RTM_NEWLINK', **kwarg): return Watchdog(self, action, kwarg) def serve_forever(self): ### # Main monitoring cycle. It gets messages from the # default iproute queue and updates objects in the # database. # # Should not be called manually. ### while not self._stop: try: messages = self.mnl.get() ## # Check it again # # NOTE: one should not run callbacks or # anything like that after setting the # _stop flag, since IPDB is not valid # anymore if self._stop: break except: log.error('Restarting IPDB instance after ' 'error:\n%s', traceback.format_exc()) if self.restart_on_error: try: self.initdb() except: log.error('Error restarting DB:\n%s', traceback.format_exc()) return continue else: raise RuntimeError('Emergency shutdown') for msg in messages: # Run pre-callbacks # NOTE: pre-callbacks are synchronous for (cuid, cb) in tuple(self._pre_callbacks.items()): try: cb(self, msg, msg['event']) except: pass with self.exclusive: event = msg.get('event', None) if event in self._event_map: for func in self._event_map[event]: func(msg) # run post-callbacks # NOTE: post-callbacks are asynchronous for (cuid, cb) in tuple(self._post_callbacks.items()): t = threading.Thread(name="IPDB callback %s" % (id(cb)), target=cb, args=(self, msg, msg['event'])) t.start() if cuid not in self._cb_threads: self._cb_threads[cuid] = set() self._cb_threads[cuid].add(t) # occasionally join cb threads for cuid in tuple(self._cb_threads): for t in tuple(self._cb_threads.get(cuid, ())): t.join(0) if not t.is_alive(): try: self._cb_threads[cuid].remove(t) except KeyError: pass if len(self._cb_threads.get(cuid, ())) == 0: del self._cb_threads[cuid] def init_ipaddr_set(self): if self.sort_addresses: return SortedIPaddrSet() else: return IPaddrSet()