#!/usr/bin/env python3 # # Copyright (C) 2017, 2018 Cumulus Networks, Inc. all rights reserved # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; version 2. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. # # https://www.gnu.org/licenses/gpl-2.0-standalone.html # # Author: # Julien Fortin, julien@cumulusnetworks.com # # Netlink cache -- # import os import socket import struct import signal import inspect import logging import ipaddress import threading import traceback from logging import DEBUG, WARNING from collections import OrderedDict try: from ifupdown2.lib.sysfs import Sysfs from ifupdown2.lib.base_objects import BaseObject from ifupdown2.lib.exceptions import RetryCMD from ifupdown2.nlmanager.nlpacket import \ Address, \ Netconf, \ Link, \ Route, \ AF_MPLS, \ NetlinkPacket, \ NLM_F_REQUEST, \ RTM_NEWLINK, \ RTM_SETLINK, \ RTM_DELLINK, \ RTM_NEWADDR, \ RTM_DELADDR, \ RTMGRP_ALL, \ NLMSG_DONE, \ NLM_F_REQUEST, \ NLM_F_CREATE, \ NLM_F_ACK, \ RT_SCOPES, \ INFINITY_LIFE_TIME import ifupdown2.nlmanager.ipnetwork as ipnetwork import ifupdown2.nlmanager.nlpacket as nlpacket import ifupdown2.nlmanager.nllistener as nllistener import ifupdown2.nlmanager.nlmanager as nlmanager import ifupdown2.ifupdown.statemanager as statemanager except (ImportError, ModuleNotFoundError): from lib.sysfs import Sysfs from lib.base_objects import BaseObject from lib.exceptions import RetryCMD from nlmanager.nlpacket import \ Address, \ Netconf, \ Link, \ Route, \ AF_MPLS, \ NetlinkPacket, \ NLM_F_REQUEST, \ RTM_NEWLINK, \ RTM_SETLINK, \ RTM_DELLINK, \ RTM_NEWADDR, \ RTM_DELADDR, \ RTMGRP_ALL, \ NLMSG_DONE, \ NLM_F_REQUEST, \ NLM_F_CREATE, \ NLM_F_ACK, \ RT_SCOPES, \ INFINITY_LIFE_TIME import nlmanager.ipnetwork as ipnetwork import nlmanager.nlpacket as nlpacket import nlmanager.nllistener as nllistener import nlmanager.nlmanager as nlmanager import ifupdown.statemanager as statemanager log = logging.getLogger() class NetlinkListenerWithCacheErrorNotInitialized(Exception): """ If NetlinkListenerWithCache fails on __init__() or / start() we need to raise this custom exception. """ pass class NetlinkError(Exception): def __init__(self, exception, prefix=None, ifname=None): netlink_exception_message = ['netlink'] if ifname: netlink_exception_message.append(ifname) if prefix: netlink_exception_message.append(prefix) netlink_exception_message.append(str(exception)) super(NetlinkError, self).__init__(": ".join(netlink_exception_message)) class NetlinkCacheError(Exception): pass class NetlinkCacheIfnameNotFoundError(NetlinkCacheError): pass class NetlinkCacheIfindexNotFoundError(NetlinkCacheError): pass class _NetlinkCache: """ Netlink Cache Class """ # we need to store these attributes in a static list to be able to iterate # through it when comparing Address objects in add_address() # we ignore IFA_CACHEINFO and IFA_FLAGS _ifa_attributes = ( Address.IFA_ADDRESS, Address.IFA_LOCAL, Address.IFA_LABEL, Address.IFA_BROADCAST, Address.IFA_ANYCAST, # Address.IFA_CACHEINFO, Address.IFA_MULTICAST, # Address.IFA_FLAGS ) def __init__(self): # sysfs API self.__sysfs = Sysfs self.__sysfs.cache = self self._link_cache = {} self._addr_cache = {} self._bridge_vlan_cache = {} self._bridge_vlan_vni_cache = {} # helper dictionaries # ifindex: ifname # ifname: ifindex self._ifname_by_ifindex = {} self._ifindex_by_ifname = {} self._ifname_by_ifindex_sysfs = {} self._ifindex_by_ifname_sysfs = {} # master/slave(s) dictionary # master_ifname: [slave_ifname, slave_ifname] self._masters_and_slaves = {} # slave/master dictionary # slave_ifname: master_ifname self._slaves_master = {} # netconf cache data-structure schema: # { # family: { # ifindex: obj # } # } self._netconf_cache = { socket.AF_INET: {}, socket.AF_INET6: {}, AF_MPLS: {} } # custom lock mechanism for netconf cache self._netconf_cache_lock = threading.Lock() # RLock is needed because we don't want to have separate handling in # get_ifname, get_ifindex and all the API function self._cache_lock = threading.RLock() # After sending a RTM_DELLINK request (ip link del DEV) we don't # automatically receive an RTM_DELLINK notification but instead we # have 3 to 5 RTM_NEWLINK notifications (first the device goes # admin-down then, goes through other steps that send notifications... # Because of this behavior the cache is out of sync and may cause # issues. To work-around this behavior we can ignore RTM_NEWLINK for a # given ifname until we receive the RTM_DELLINK. That way our cache is # not stale. When deleting a link, ifupdown2 uses: # - NetlinkListenerWithCache:link_del(ifname) # Before sending the RTM_DELLINK netlink packet we: # - register the ifname in the _ignore_rtm_newlinkq # - force purge the cache because we are not notified right away # - for every RTM_NEWLINK notification we check _ignore_rtm_newlinkq # to see if we need to ignore that packet # - for every RTM_DELLINK notification we check if we have a # corresponding entry in _ignore_rtm_newlinkq and remove it self._ignore_rtm_newlinkq = list() self._ignore_rtm_newlinkq_lock = threading.Lock() # After sending a no master request (IFLA_MASTER=0) the kernels send # 2 or 3 notifications (with IFLA_MASTER) before sending the final # notification where IFLA_MASTER is removed. For performance purposes # we don't wait for those notifications, we simply update the cache # to reflect the change (if we got an ACK on the nomaster request). # Those extra notification re-add the former slave to it's master # (in our internal data-structures at least). ifupdown2 relies on # the cache to get accurate information, this puts the cache in an # unreliable state. We can detected this bad state and avoid it. Afer # a nomaster request we "register" the device as "nomaster", meaning # that we will manually remove the IFLA_MASTER attribute from any # subsequent packet, until the final packet arrives - then unregister # the device from the nomasterq. # We need an extra data-structure and lock mechanism for this: self._rtm_newlink_nomasterq = list() self._rtm_newlink_nomasterq_lock = threading.Lock() # In the scenario of NetlinkListenerWithCache, the listener thread # decode netlink packets and perform caching operation based on their # respective msgtype add_link for RTM_NEWLINK, remove_link for DELLINK # In some cases the main thread is creating a new device with: # NetlinkListenerWithCache.link_add() # the request is sent and the cache won't have any knowledge of this # new link until we receive a NEWLINK notification on the listener # socket meanwhile the main thread keeps going. The main thread may # query the cache for the newly created device but the cache might not # know about it yet thus creating unexpected situation in the main # thread operations. We need to provide a mechanism to block the main # thread until the desired notification is processed. The main thread # can call: # register_wait_event(ifname, netlink_msgtype) # to register an event for device name 'ifname' and netlink msgtype. # The main thread should then call wait_event to sleep until the # notification is received the NetlinkListenerWithCache provides the # following API: # tx_nlpacket_get_response_with_error_and_wait_for_cache(ifname, nl_packet) # to handle both packet transmission, error handling and cache event self._wait_event = None self._wait_event_alarm = threading.Event() def __handle_type_error(self, func_name, data, exception, return_value): """ TypeError shouldn't happen but if it does, we are prepared to log and recover """ log.debug('nlcache: %s: %s: TypeError: %s' % (func_name, data, str(exception))) return return_value def __unslave_nolock(self, slave, master=None): """ WARNING: LOCK SHOULD BE ACQUIRED BEFORE CALLING THIS FUNCTION When unslaving a device we need to manually clear and update our internal data structures to avoid keeping stale information before receiving a proper netlink notification. Dictionaries: - master_and_slaves - slaves_master - bridge_vlan_cache :param master: :param slave: :return: """ try: del self._link_cache[slave].attributes[Link.IFLA_MASTER] except Exception: pass try: if not master: master = self._slaves_master[slave] self._masters_and_slaves[master].remove(slave) except (KeyError, ValueError): for master, slaves_list in self._masters_and_slaves.items(): if slave in slaves_list: slaves_list.remove(slave) break try: del self._slaves_master[slave] except KeyError: pass try: del self._bridge_vlan_cache[slave] except KeyError: pass try: del self._bridge_vlan_vni_cache[slave] except KeyError: pass def append_to_ignore_rtm_newlinkq(self, ifname): """ Register device 'ifname' to the ignore_rtm_newlinkq list pending RTM_DELLINK (see comments above _ignore_rtm_newlinkq declaration) """ with self._ignore_rtm_newlinkq_lock: self._ignore_rtm_newlinkq.append(ifname) def remove_from_ignore_rtm_newlinkq(self, ifname): """ Unregister ifname from ignore_newlinkq list """ try: with self._ignore_rtm_newlinkq_lock: self._ignore_rtm_newlinkq.remove(ifname) except ValueError: pass def append_to_rtm_newlink_nomasterq(self, ifname): """ Register device 'ifname' to the _ignore_rtm_newlink_nomasterq """ with self._rtm_newlink_nomasterq_lock: self._rtm_newlink_nomasterq.append(ifname) def remove_from_rtm_newlink_nomasterq(self, ifname): """ Unregister ifname from _ignore_rtm_newlink_nomasterq list """ try: with self._rtm_newlink_nomasterq_lock: self._rtm_newlink_nomasterq.remove(ifname) except ValueError: pass def register_wait_event(self, ifname, msgtype): """ Register a cache "wait event" for device named 'ifname' and packet type msgtype We only one wait_event to be registered. Currently we don't support multi-threaded application so we need to had a strict check. In the future we could have a wait_event queue for multiple thread could register wait event. :param ifname: target device :param msgtype: netlink message type (RTM_NEWLINK, RTM_DELLINK etc.) :return: boolean: did we successfully register a wait_event? """ with self._cache_lock: if self._wait_event: return False self._wait_event = (ifname, msgtype) return True def wait_event(self): """ Sleep until cache event happened in netlinkq thread or timeout expired :return: None We set an arbitrary timeout at 1sec in case the kernel doesn't send out a notification for the event we want to wait for. """ if not self._wait_event_alarm.wait(1): log.debug('nlcache: wait event alarm timeout expired for device "%s" and netlink packet type: %s' % (self._wait_event[0], NetlinkPacket.type_to_string.get(self._wait_event[1], str(self._wait_event[1])))) with self._cache_lock: self._wait_event = None self._wait_event_alarm.clear() def unregister_wait_event(self): """ Clear current wait event (cache can only handle one at once) :return: """ with self._cache_lock: self._wait_event = None self._wait_event_alarm.clear() def override_link_flag(self, ifname, flags): # TODO: dont override all the flags just turn on/off IFF_UP try: with self._cache_lock: self._link_cache[ifname].flags = flags except Exception: pass def override_link_mtu(self, ifname, mtu): """ Manually override link mtu and ignore any failures :param ifname: :param mtu: :return: """ try: with self._cache_lock: self._link_cache[ifname].attributes[Link.IFLA_MTU].value = mtu except Exception: pass def override_cache_unslave_link(self, slave, master): """ Manually update the cache unslaving SLAVE from MASTER When calling link_set_nomaster, we don't want to wait for the RTM_GETLINK notification - if the operation return with NL_SUCCESS we can manually update our cache and move on. :param master: :param slave: :return: """ with self._cache_lock: self.__unslave_nolock(slave, master) def DEBUG_IFNAME(self, ifname, with_addresses=False): """ A very useful function to use while debugging, it dumps the netlink packet with debug and color output. """ import logging root = logging.getLogger() level = root.level try: root.setLevel(DEBUG) for handler in root.handlers: handler.setLevel(DEBUG) nllistener.log.setLevel(DEBUG) nlpacket.log.setLevel(DEBUG) nlmanager.log.setLevel(DEBUG) with self._cache_lock: obj = self._link_cache[ifname] save_debug = obj.debug obj.debug = True obj.dump() obj.debug = save_debug #if with_addresses: # addrs = self._addr_cache.get(ifname, []) # log.error('ADDRESSES=%s' % addrs) # for addr in addrs: # save_debug = addr.debug # addr.debug = True # addr.dump() # addr.debug = save_debug # log.error('-----------') # log.error('-----------') # log.error('-----------') except Exception: traceback.print_exc() # TODO: save log_level at entry and re-apply it after the dump nllistener.log.setLevel(WARNING) nlpacket.log.setLevel(WARNING) nlmanager.log.setLevel(WARNING) root.setLevel(level) for handler in root.handlers: handler.setLevel(level) def DEBUG_MSG(self, msg): import logging root = logging.getLogger() level = root.level try: root.setLevel(DEBUG) for handler in root.handlers: handler.setLevel(DEBUG) nllistener.log.setLevel(DEBUG) nlpacket.log.setLevel(DEBUG) nlmanager.log.setLevel(DEBUG) save_debug = msg.debug msg.debug = True msg.dump() msg.debug = save_debug except Exception: traceback.print_exc() # TODO: save log_level at entry and re-apply it after the dump nllistener.log.setLevel(WARNING) nlpacket.log.setLevel(WARNING) nlmanager.log.setLevel(WARNING) root.setLevel(level) for handler in root.handlers: handler.setLevel(level) def _populate_sysfs_ifname_ifindex_dicts(self): ifname_by_ifindex_dict = {} ifindex_by_ifname_dict = {} try: for dir_name in os.listdir('/sys/class/net/'): try: with open('/sys/class/net/%s/ifindex' % dir_name) as f: ifindex = int(f.readline()) ifname_by_ifindex_dict[ifindex] = dir_name ifindex_by_ifname_dict[dir_name] = ifindex except (IOError, ValueError): pass except OSError: pass with self._cache_lock: self._ifname_by_ifindex_sysfs = ifname_by_ifindex_dict self._ifindex_by_ifname_sysfs = ifindex_by_ifname_dict def get_ifindex(self, ifname): """ Return device index or raise NetlinkCacheIfnameNotFoundError :param ifname: :return: int :raise: NetlinkCacheIfnameNotFoundError(NetlinkCacheError) """ try: with self._cache_lock: return self._ifindex_by_ifname[ifname] except KeyError: # We assume that if the user requested a valid device ifindex but # for some reason we don't find any trace of it in our cache, we # then use sysfs to make sure that this device exists and fill our # internal help dictionaries. with self._cache_lock: ifindex = self._ifindex_by_ifname_sysfs.get(ifname) if ifindex: return ifindex self._populate_sysfs_ifname_ifindex_dicts() try: return self._ifindex_by_ifname_sysfs[ifname] except KeyError: # if we still haven't found any trace of the requested device # we raise a custom exception raise NetlinkCacheIfnameNotFoundError('ifname %s not present in cache' % ifname) def get_ifname(self, ifindex): """ Return device name or raise NetlinkCacheIfindexNotFoundError :param ifindex: :return: str :raise: NetlinkCacheIfindexNotFoundError (NetlinkCacheError) """ try: with self._cache_lock: return self._ifname_by_ifindex[ifindex] except KeyError: # We assume that if the user requested a valid device ifname but # for some reason we don't find any trace of it in our cache, we # then use sysfs to make sure that this device exists and fill our # internal help dictionaries. with self._cache_lock: ifname = self._ifname_by_ifindex_sysfs.get(ifindex) if ifname: return ifname self._populate_sysfs_ifname_ifindex_dicts() try: return self._ifname_by_ifindex_sysfs[ifindex] except KeyError: # if we still haven't found any trace of the requested device # we raise a custom exception raise NetlinkCacheIfindexNotFoundError('ifindex %s not present in cache' % ifindex) def link_exists(self, ifname): """ Check if we have a cache entry for device 'ifname' :param ifname: device name :return: boolean """ with self._cache_lock: return ifname in self._link_cache def link_is_up(self, ifname): """ Check if device 'ifname' has IFF_UP flag :param ifname: :return: boolean """ try: with self._cache_lock: return self._link_cache[ifname].flags & Link.IFF_UP except (KeyError, TypeError): # ifname is not present in the cache return False except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=False) def link_is_loopback(self, ifname): """ Check if device has IFF_LOOPBACK flag :param ifname: :return: boolean """ try: with self._cache_lock: return self._link_cache[ifname].flags & Link.IFF_LOOPBACK # IFF_LOOPBACK should be enough, otherwise we can also check for # link.device_type & Link.ARPHRD_LOOPBACK except (KeyError, AttributeError): return False except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=False) def link_exists_and_up(self, ifname): """ Check if device exists and has IFF_UP flag set :param ifname: :return: tuple (boolean, boolean) -> (link_exists, link_is_up) """ try: with self._cache_lock: return True, self._link_cache[ifname].flags & Link.IFF_UP except KeyError: # ifname is not present in the cache return False, False except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=(False, False)) def link_is_bridge(self, ifname): return self.get_link_kind(ifname) == 'bridge' def get_link_kind(self, ifname): """ Return link IFLA_INFO_KIND :param ifname: :return: string """ try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_KIND] except (KeyError, AttributeError): return None except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=None) def get_link_mtu(self, ifname): """ Return link IFLA_MTU :param ifname: :return: int """ return self.get_link_attribute(ifname, Link.IFLA_MTU, default=0) def get_link_mtu_str(self, ifname): """ Return link IFLA_MTU as string :param ifname: :return: str """ return str(self.get_link_mtu(ifname)) def get_link_address(self, ifname): """ Return link IFLA_ADDRESS :param ifname: :return: str """ packet = None default_value = "" try: with self._cache_lock: packet = self._link_cache[ifname] return packet.attributes[Link.IFLA_ADDRESS].value.lower() except (KeyError, AttributeError): # KeyError will be raised if: # - ifname is missing from the cache (but link_exists should be called prior this call) # - IFLA_ADDRESS is missing # AttributeError can also be raised if attributes[IFLA_ADDRESS] returns None # If the packet is tagged as a REQUEST packet (priv_flags) we should query sysfs # otherwise default_value is returned. if packet and packet.priv_flags & NLM_F_REQUEST: return self.__sysfs.get_link_address(ifname) else: return default_value except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), default_value) def get_link_address_raw(self, ifname): """ Return link IFLA_ADDRESS as integer :param ifname: :return: int """ return self.get_link_attribute_raw(ifname, Link.IFLA_ADDRESS, default=0) def get_link_alias(self, ifname): """ Return link IFLA_IFALIAS :param ifname: :return: str """ return self.get_link_attribute(ifname, Link.IFLA_IFALIAS) def get_link_protodown(self, ifname): """ Return link IFLA_PROTO_DOWN :param ifname: :return: int """ return self.get_link_attribute(ifname, Link.IFLA_PROTO_DOWN) def get_link_attribute(self, ifname, attr, default=None): """ Return link attribute 'attr'.value :param ifname: :param attr: :param default: :return: """ try: with self._cache_lock: return self._link_cache[ifname].attributes[attr].value except (KeyError, AttributeError): return default except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=default) def get_link_attribute_raw(self, ifname, attr, default=None): """ Return link attribute 'attr'.raw :param ifname: :param attr: :param default: :return: """ try: with self._cache_lock: return self._link_cache[ifname].attributes[attr].raw except (KeyError, AttributeError): return default except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=default) def get_link_slave_kind(self, ifname): """ Return device slave kind :param ifname: :return: str """ try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_SLAVE_KIND] except (KeyError, AttributeError): return None except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=None) def get_link_info_data_attribute(self, ifname, info_data_attribute, default=None): """ Return device linkinfo:info_data attribute or default value :param ifname: :param info_data_attribute: :param default: :return: """ try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][info_data_attribute] except (KeyError, AttributeError): return default except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=default) def get_link_info_data(self, ifname): """ Return device linkinfo:info_data attribute or default value :param ifname: :param info_data_attribute: :param default: :return: """ try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA] except (KeyError, AttributeError): return {} except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value={}) def get_link_info_slave_data_attribute(self, ifname, info_slave_data_attribute, default=None): """ Return device linkinfo:info_slave_data attribute or default value :param ifname: :param info_data_attribute: :param default: :return: """ try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_SLAVE_DATA][info_slave_data_attribute] except (KeyError, AttributeError): return default except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=default) ################ # MASTER & SLAVE ################ def get_master(self, ifname): """ Return device master's ifname :param ifname: :return: str """ try: with self._cache_lock: return self._slaves_master[ifname] except (KeyError, AttributeError): return None except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=None) def get_slaves(self, master): """ Return all devices ifname enslaved to master device :param master: :return: list of string """ try: with self._cache_lock: return list(self._masters_and_slaves[master]) except KeyError: return [] def is_link_enslaved_to(self, slave, master): """ Return bool if SLAVE is enslaved to MASTER :param slave: :param master: :return: """ try: with self._cache_lock: return self._slaves_master[slave] == master except KeyError: return False def get_lower_device_ifname(self, ifname): """ Return the lower-device (IFLA_LINK) name or raise KeyError :param ifname: :return: string """ try: with self._cache_lock: return self.get_ifname(self._link_cache[ifname].attributes[Link.IFLA_LINK].value) except (NetlinkCacheIfnameNotFoundError, AttributeError, KeyError): return None except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=None) ########################################################################## # VRF #################################################################### ########################################################################## def get_vrf_table_map(self): vrf_table_map = {} try: with self._cache_lock: for ifname, obj in self._link_cache.items(): linkinfo = obj.attributes.get(Link.IFLA_LINKINFO) if linkinfo and linkinfo.value.get(Link.IFLA_INFO_KIND) == "vrf": vrf_table_map[linkinfo.value[Link.IFLA_INFO_DATA][Link.IFLA_VRF_TABLE]] = ifname except Exception as e: log.debug("get_vrf_table_map: %s" % str(e)) return vrf_table_map def get_vrf_table(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_VRF_TABLE] except (KeyError, AttributeError): return 0 ########################################################################## # BOND ################################################################### ########################################################################## def bond_exists(self, ifname): """ Check if bond 'ifname' exists :param ifname: bond name :return: boolean """ try: with self._cache_lock: return self._link_cache[ifname].attributes[nlpacket.Link.IFLA_LINKINFO].value[nlpacket.Link.IFLA_INFO_KIND] == 'bond' except (KeyError, AttributeError): return False except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=False) ########################################################################## # BRIDGE PORT ############################################################ ########################################################################## def get_bridge_port_multicast_router(self, ifname): """ Get bridge port multicast_router value - defaults to 1 :param ifname: :return: """ default_value = 1 try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_SLAVE_DATA][Link.IFLA_BRPORT_MULTICAST_ROUTER] except (KeyError, AttributeError): # KeyError will be raised if: # - ifname is missing from the cache (but link_exists should be called prior this call) # - IFLA_BRPORT_MULTICAST_ROUTER is missing # AttributeError can also be raised if IFLA_LINKINFO is missing (None.value) # default_value is returned. return default_value except TypeError as e: return self.__handle_type_error( inspect.currentframe().f_code.co_name, ifname, str(e), return_value=default_value ) ########################################################################## # BRIDGE ################################################################# ########################################################################## def get_bridge_multicast_snooping(self, ifname): """ Get bridge multicast_snooping value - defaults to 1 :param ifname: :return: """ default_value = 1 try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_BR_MCAST_SNOOPING] except (KeyError, AttributeError): # KeyError will be raised if: # - ifname is missing from the cache (but link_exists should be called prior this call) # - IFLA_BR_MCAST_SNOOPING is missing # AttributeError can also be raised if IFLA_LINKINFO is missing (None.value) # default_value is returned. return default_value except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=default_value) def get_bridge_stp(self, ifname): """ WARNING: ifname should be a bridge """ try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_BR_STP_STATE] except (KeyError, AttributeError): return 0 except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=0) def get_brport_cost(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_BRPORT_COST] except (KeyError, AttributeError): return None except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=None) def get_brport_priority(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_BRPORT_PRIORITY] except (KeyError, AttributeError): return None except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=None) def get_brport_unicast_flood(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_BRPORT_UNICAST_FLOOD] except (KeyError, AttributeError): return 0 except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=0) def get_brport_multicast_flood(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_BRPORT_MCAST_FLOOD] except (KeyError, AttributeError): return 0 except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=0) def get_brport_broadcast_flood(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_BRPORT_BCAST_FLOOD] except (KeyError, AttributeError): return 0 except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=0) def get_brport_neigh_suppress(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_BRPORT_NEIGH_SUPPRESS] except (KeyError, AttributeError): return 0 except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=0) def get_brport_learning(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_SLAVE_DATA][Link.IFLA_BRPORT_LEARNING] except (KeyError, AttributeError): return 0 def get_vlan_vni(self, ifname): with self._cache_lock: return self._bridge_vlan_vni_cache.get(ifname) def get_pvid_and_vids(self, ifname): """ vlan-identifiers are stored in: self._bridge_vlan_cache = { ifname: [(vlan, flag), (vlan, flag), ...] } Those vlans are stored in compressed format (RTEXT_FILTER_BRVLAN_COMPRESSED) We only uncompress the vlan when the user request it. :param ifname: :return tuple: pvid, vids = int, [int, ] """ pvid = None vlans = [] try: range_begin_vlan_id = None range_flag = None with self._cache_lock: bridge_vlans_tuples = self._bridge_vlan_cache.get(ifname) if bridge_vlans_tuples: for (vlan_id, vlan_flag) in sorted(bridge_vlans_tuples): if vlan_flag & Link.BRIDGE_VLAN_INFO_PVID: pvid = vlan_id if vlan_flag & Link.BRIDGE_VLAN_INFO_RANGE_BEGIN: range_begin_vlan_id = vlan_id range_flag = vlan_flag elif vlan_flag & Link.BRIDGE_VLAN_INFO_RANGE_END: range_flag |= vlan_flag if not range_begin_vlan_id: log.warning("BRIDGE_VLAN_INFO_RANGE_END is %d but we never " "saw a BRIDGE_VLAN_INFO_RANGE_BEGIN" % vlan_id) range_begin_vlan_id = vlan_id for x in range(range_begin_vlan_id, vlan_id + 1): vlans.append(x) range_begin_vlan_id = None range_flag = None else: vlans.append(vlan_id) except Exception: log.exception("get_bridge_vids") return pvid, vlans def get_pvid(self, ifname): """ Get Port VLAN ID for device 'ifname' :param ifname: :return: """ pvid = None try: with self._cache_lock: bridge_vlans_tuples = self._bridge_vlan_cache.get(ifname) if bridge_vlans_tuples: for (vlan_id, vlan_flag) in sorted(bridge_vlans_tuples): if vlan_flag & Link.BRIDGE_VLAN_INFO_PVID: return vlan_id except Exception: log.exception("get_pvid") return pvid def bridge_exists(self, ifname): """ Check if cached device is a bridge """ try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_KIND] == "bridge" except (KeyError, AttributeError): return False except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=False) def bridge_is_vlan_aware(self, ifname): """ Return IFLA_BR_VLAN_FILTERING value :param ifname: :return: boolean """ try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA][Link.IFLA_BR_VLAN_FILTERING] except (KeyError, AttributeError): return False except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=False) def link_is_bridge_port(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_SLAVE_KIND] == "bridge" except (KeyError, AttributeError): return False except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=False) def bridge_port_exists(self, bridge_name, brport_name): try: with self._cache_lock: # we are assuming that bridge_name is a valid bridge? return self._slaves_master[brport_name] == bridge_name except (KeyError, AttributeError): return False except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, bridge_name, str(e), return_value=False) def get_bridge_name_from_port(self, bridge_port_name): bridge_name = self.get_master(bridge_port_name) # now that we have the master's name we just need to double check # if the master is really a bridge return bridge_name if self.link_is_bridge(bridge_name) else None #def is_link_slave_kind(self, ifname, _type): # try: # with self._cache_lock: # return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_SLAVE_KIND] == _type # except (KeyError, AttributeError): # return False ######################################## def get_link_ipv6_addrgen_mode(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_AF_SPEC].value[socket.AF_INET6][Link.IFLA_INET6_ADDR_GEN_MODE] except (KeyError, AttributeError): # default to 0 (eui64) return 0 except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=0) ##################################################### ##################################################### ##################################################### ##################################################### ##################################################### def add_link(self, link): """ Cache RTM_NEWLINK packet :param link: :return: """ ifindex = link.ifindex try: ifname = link.get_attribute_value(Link.IFLA_IFNAME).decode() except AttributeError: # ifname is already a string we don't need to decode it ifname = link.get_attribute_value(Link.IFLA_IFNAME) # check if this device is registered in the ignore list with self._ignore_rtm_newlinkq_lock: if ifname in self._ignore_rtm_newlinkq: return # check if this device is registered in the nomaster list: # if so we need to remove IFLA_MASTER attribute (if it fails # it means we've received the final notification and we should # unregister the device from our list. with self._rtm_newlink_nomasterq_lock: if ifname in self._rtm_newlink_nomasterq: try: del link.attributes[Link.IFLA_MASTER] except Exception: self._rtm_newlink_nomasterq.remove(ifname) # we need to check if the device was previously enslaved # so we can update the _masters_and_slaves and _slaves_master # dictionaries if the master has changed or was un-enslaved. old_ifla_master = None with self._cache_lock: # do we have a wait event registered for RTM_NEWLINK this ifname if self._wait_event and self._wait_event == (ifname, RTM_NEWLINK): self._wait_event_alarm.set() try: ifla_master_attr = self._link_cache[ifname].attributes.get(Link.IFLA_MASTER) if ifla_master_attr: old_ifla_master = ifla_master_attr.get_pretty_value() except KeyError: # link is not present in the cache pass except AttributeError: # if this code is ever reached, this is very concerning and # should never happen as _link_cache should always contains # nlpacket.NetlinkPacket... maybe have some extra handling # here just in case? pass self._link_cache[ifname] = link ###################################################### # update helper dictionaries and handle link renamed # ###################################################### if ifindex: # ifindex can be None for packet added on ACK, it means # that we are caching the request packet and not the # notification coming from the kernel. We can leave # those data-strctures empty and rely on our try/excepts # in get_ifname/get_ifindex/get_master to do the work. self._ifindex_by_ifname[ifname] = ifindex rename_detected = False old_ifname_entry_for_ifindex = self._ifname_by_ifindex.get(ifindex) if old_ifname_entry_for_ifindex and old_ifname_entry_for_ifindex != ifname: # The ifindex was reused for a new interface before we got the # RTM_DELLINK notification or the device using that ifindex was # renamed. We need to update the cache accordingly. rename_detected = True self._ifname_by_ifindex[ifindex] = ifname if rename_detected: # in this case we detected a rename... It should'nt happen has we should get a RTM_DELLINK before that. # if we still detect a rename the opti is to get rid of the stale value directly try: del self._ifindex_by_ifname[old_ifname_entry_for_ifindex] except KeyError: log.debug('update_helper_dicts: del _ifindex_by_ifname[%s]: KeyError ifname: %s' % (old_ifname_entry_for_ifindex, old_ifname_entry_for_ifindex)) try: del self._link_cache[old_ifname_entry_for_ifindex] except KeyError: log.debug('update_helper_dicts: del _link_cache[%s]: KeyError ifname: %s' % (old_ifname_entry_for_ifindex, old_ifname_entry_for_ifindex)) ###################################################### ###################################################### link_ifla_master_attr = link.attributes.get(Link.IFLA_MASTER) if link_ifla_master_attr: link_ifla_master = link_ifla_master_attr.get_pretty_value() else: link_ifla_master = None # if the link has a master we need to store it in an helper dictionary, where # the key is the master ifla_ifname and the value is a list of slaves, example: # _masters_slaves_dict = { # 'bond0': ['swp21', 'swp42'] # } # this will be useful in the case we need to iterate on all slaves of a specific link if old_ifla_master: if old_ifla_master != link_ifla_master: # the link was previously enslaved but master is now unset on this device # we need to reflect that on the _masters_and_slaves and _slaves_master dictionaries try: self.__unslave_nolock(slave=ifname) except NetlinkCacheIfindexNotFoundError: pass else: # the master status didn't change we can assume that our internal # masters_slaves dictionary is up to date and return here return if not link_ifla_master: return master_ifname = self._ifname_by_ifindex.get(link_ifla_master) if not master_ifname: # in this case we have a link object with IFLA_MASTER set to a ifindex # but this ifindex is not in our _ifname_by_ifindex dictionary thus it's # not in the _link_cache yet. This situation may happen when getting the # very first link dump. The kernel dumps device in the "ifindex" order. # # So let's say you have a box with 4 ports (lo, eth0, swp1, swp2), then # manually create a bond (bond0) and enslave swp1 and swp2, bond0 will # have ifindex 5 but when getting the link dump swp1 will be handled first # so at this point the cache has no idea if ifindex 5 is valid or not. # But since we've made it this far we can assume that this is probably a # valid device and will use sysfs to confirm. master_device_path = '/sys/class/net/%s/master' % ifname if os.path.exists(master_device_path): # this check is necessary because realpath doesn't return None on error # it returns it's input argument... # >>> os.path.realpath('/sys/class/net/device_not_found') # '/sys/class/net/device_not_found' # >>> master_ifname = os.path.basename(os.path.realpath(master_device_path)) if master_ifname in self._masters_and_slaves: slave_list = self._masters_and_slaves[master_ifname] if ifname not in slave_list: slave_list.append(ifname) else: self._masters_and_slaves[master_ifname] = [ifname] self._slaves_master[ifname] = master_ifname def update_link_info_data(self, ifname, ifla_info_data): """ Update specific IFLA_INFO_DATA attributes of an existing cached device ignore all errors """ try: with self._cache_lock: self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_DATA].update(ifla_info_data) except Exception: pass def update_link_ifla_address(self, ifname, ifla_address_str, ifla_address_int): try: with self._cache_lock: self._link_cache[ifname].attributes[Link.IFLA_ADDRESS].value = ifla_address_str self._link_cache[ifname].attributes[Link.IFLA_ADDRESS].raw = ifla_address_int except Exception: pass def add_bridge_vlan(self, msg): """ Process AF_BRIDGE family packets (AF_BRIDGE family should be check before calling this function). Extract VLAN_INFO (vlan id and flag) and store it in cache. :param link: :return: """ vlans_list = [] # Todo: acquire the lock only when really needed with self._cache_lock: ifla_af_spec = msg.get_attribute_value(Link.IFLA_AF_SPEC) ifname = msg.get_attribute_value(Link.IFLA_IFNAME) if not ifla_af_spec: return try: # We need to check if this object is still in cache, after a bridge # is removed we still receive AF_BRIDGE notifications for it's slave # those notifications should be ignored. ifla_master = msg.get_attribute_value(Link.IFLA_MASTER) if not ifla_master or not ifla_master in self._ifname_by_ifindex: return except Exception: pass # Example IFLA_AF_SPEC # 20: 0x1c001a00 .... Length 0x001c (28), Type 0x001a (26) IFLA_AF_SPEC # 21: 0x08000200 .... Nested Attribute - Length 0x0008 (8), Type 0x0002 (2) IFLA_BRIDGE_VLAN_INFO # 22: 0x00000a00 .... # 23: 0x08000200 .... Nested Attribute - Length 0x0008 (8), Type 0x0002 (2) IFLA_BRIDGE_VLAN_INFO # 24: 0x00001000 .... # 25: 0x08000200 .... Nested Attribute - Length 0x0008 (8), Type 0x0002 (2) IFLA_BRIDGE_VLAN_INFO # 26: 0x00001400 .... for x_type, x_value in ifla_af_spec.items(): if x_type == Link.IFLA_BRIDGE_VLAN_INFO: for vlan_flag, vlan_id in x_value: # We store these in the tuple as (vlan, flag) instead # (flag, vlan) so that we can sort the list of tuples vlans_list.append((vlan_id, vlan_flag)) elif x_type == Link.IFLA_BRIDGE_VLAN_TUNNEL_INFO: self._bridge_vlan_vni_cache.update({ifname: x_value}) self._bridge_vlan_cache.update({ifname: vlans_list}) def force_add_slave(self, master, slave): """ When calling link_set_master, we don't want to wait for the RTM_GETLINK notification - if the operation return with NL_SUCCESS we can manually update our cache and move on :param master: :param slave: :return: """ try: with self._cache_lock: master_slaves = self._masters_and_slaves.get(master) if not master_slaves: self._masters_and_slaves[master] = [slave] else: if slave not in master_slaves: master_slaves.append(slave) # if the slave is already enslaved to another device we should # make sure to remove it from the _masters_and_slaves data # structure as well. old_master = self._slaves_master.get(slave) if old_master and old_master != master: try: self._masters_and_slaves.get(old_master, []).remove(slave) except Exception: pass self._slaves_master[slave] = master except Exception: # since this is an optimization function we can ignore all error pass def force_add_slave_list(self, master, slave_list): """ When calling link_set_master, we don't want to wait for the RTM_GETLINK notification - if the operation return with NL_SUCCESS we can manually update our cache and move on :param master: :param slave: :return: """ try: with self._cache_lock: master_slaves = self._masters_and_slaves.get(master) if not master_slaves: self._masters_and_slaves[master] = slave_list else: for slave_ifname in slave_list: if slave_ifname not in master_slaves: master_slaves.append(slave_ifname) for slave in slave_list: self._slaves_master[slave] = master except Exception: # since this is an optimization function we can ignore all error pass def force_remove_link(self, ifname): """ When calling link_del (RTM_DELLINK) we need to manually remove the associated cache entry - the RTM_DELLINK notification isn't received instantaneously - we don't want to keep stale value in our cache :param ifname: :return: """ try: ifindex = self.get_ifindex(ifname) except (KeyError, NetlinkCacheIfnameNotFoundError): ifindex = None self.remove_link(None, link_ifname=ifname, link_ifindex=ifindex) def remove_link(self, link, link_ifname=None, link_ifindex=None): """ Process RTM_DELLINK packet and purge the cache accordingly """ if link: ifindex = link.ifindex ifname = link.get_attribute_value(Link.IFLA_IFNAME) try: # RTM_DELLINK received - we can now remove ifname from the # ignore_rtm_newlinkq list. We don't bother checkin if the # if name is present in the list (because it's likely in) with self._ignore_rtm_newlinkq_lock: self._ignore_rtm_newlinkq.remove(ifname) except ValueError: pass else: ifname = link_ifname ifindex = link_ifindex link_ifla_master = None # when an enslaved device is removed we receive the RTM_DELLINK # notification without the IFLA_MASTER attribute, we need to # get the previous cached value in order to correctly update the # _masters_and_slaves dictionary with self._cache_lock: try: try: ifla_master_attr = self._link_cache[ifname].attributes.get(Link.IFLA_MASTER) if ifla_master_attr: link_ifla_master = ifla_master_attr.get_pretty_value() except KeyError: # link is not present in the cache pass except AttributeError: # if this code is ever reached this is very concerning and # should never happen as _link_cache should always contains # nlpacket.NetlinkPacket maybe have some extra handling here # just in case? pass finally: del self._link_cache[ifname] except KeyError: # KeyError means that the link doesn't exists in the cache log.debug('del _link_cache: KeyError ifname: %s' % ifname) try: # like in __unslave_nolock() we need to make sure that all deleted link # have their bridge-vlans and _slaves_master entries cleared. for slave in list(self._masters_and_slaves[ifname]): self.__unslave_nolock(slave, master=ifname) except Exception: pass try: del self._bridge_vlan_cache[ifname] except Exception: pass try: del self._bridge_vlan_vni_cache[ifname] except Exception: pass try: del self._ifname_by_ifindex[ifindex] except KeyError: log.debug('del _ifname_by_ifindex: KeyError ifindex: %s' % ifindex) try: del self._ifindex_by_ifname[ifname] except KeyError: log.debug('del _ifindex_by_ifname: KeyError ifname: %s' % ifname) try: del self._addr_cache[ifname] except KeyError: log.debug('del _addr_cache: KeyError ifname: %s' % ifname) try: del self._masters_and_slaves[ifname] except KeyError: log.debug('del _masters_and_slaves: KeyError ifname: %s' % ifname) # if the device was enslaved to another device we need to remove # it's entry from our _masters_and_slaves dictionary if link_ifla_master and link_ifla_master > 0: try: self.__unslave_nolock(slave=ifname) except NetlinkCacheIfindexNotFoundError as e: log.debug('cache: remove_link: %s: %s' % (ifname, str(e))) except KeyError: log.debug('_masters_and_slaves[if%s].remove(%s): KeyError' % (link_ifla_master, ifname)) def _address_get_ifname_and_ifindex(self, addr): ifindex = addr.ifindex label = addr.get_attribute_value(Address.IFA_LABEL) if not label: try: label = self.get_ifname(ifindex) except NetlinkCacheIfindexNotFoundError: pass if isinstance(label, bytes): label = label.decode() return label, ifindex @staticmethod def __check_and_replace_address(address_list, new_addr): """ Check if new_addr is in address_list, if found we replace the occurrence with the new and update object "new_addr" address_list should be a valid list (check before calling to improve perf) :param address_list: :param new_addr: :return: """ ip_with_prefix = new_addr.get_attribute_value(Address.IFA_ADDRESS) for index, addr in enumerate(address_list): if addr.get_attribute_value(Address.IFA_ADDRESS) == ip_with_prefix: address_list[index] = new_addr return True return False def add_address(self, addr): ifname, ifindex = self._address_get_ifname_and_ifindex(addr) if not ifname: log.debug('nlcache: add_address: cannot cache addr for ifindex %s' % ifindex) return ip_version = addr.get_attribute_value(Address.IFA_ADDRESS).version with self._cache_lock: if ifname in self._addr_cache: address_list = self._addr_cache[ifname][ip_version] # First check if the address is already cached, if so # we need to update it's entry with the new obj if not address_list or not self.__check_and_replace_address(address_list, addr): address_list.append(addr) else: self._addr_cache[ifname] = { 4: [], 6: [], ip_version: [addr] } def force_address_flush_family(self, ifname, family): try: with self._cache_lock: self._addr_cache[ifname][family] = [] except Exception: pass def address_flush_link(self, ifname): """ Flush address cache for link 'ifname' :param ifname: :return: """ try: with self._cache_lock: self._addr_cache[ifname] = {4: [], 6: []} except Exception: pass def force_remove_addr(self, ifname, addr): """ When calling addr_del (RTM_DELADDR) we need to manually remove the associated cache entry - the RTM_DELADDR notification isn't received instantaneously - we don't want to keep stale value in our cache :param ifname: :param addr: """ try: with self._cache_lock: # iterate through the interface addresses # to find which one to remove from the cache obj_to_remove = None for cache_addr in self._addr_cache[ifname][addr.version]: try: if cache_addr.attributes[Address.IFA_ADDRESS].value == addr: obj_to_remove = cache_addr except Exception: try: if cache_addr.attributes[Address.IFA_LOCAL].value == addr: obj_to_remove = cache_addr except Exception: return if obj_to_remove: self._addr_cache[ifname][addr.version].remove(obj_to_remove) except Exception: pass def remove_address(self, addr_to_remove): ifname, _ = self._address_get_ifname_and_ifindex(addr_to_remove) with self._cache_lock: # iterate through the interface addresses # to find which one to remove from the cache try: ip_version = addr_to_remove.get_attribute_value(Address.IFA_ADDRESS).version except Exception: try: ip_version = addr_to_remove.get_attribute_value(Address.IFA_LOCAL).version except Exception: # print debug error return addrs_for_interface = self._addr_cache.get(ifname, {}).get(ip_version) if not addrs_for_interface: return list_addr_to_remove = [] for addr in addrs_for_interface: # compare each object attribute to see if they match addr_match = False for ifa_attr in self._ifa_attributes: if addr.get_attribute_value(ifa_attr) != addr_to_remove.get_attribute_value(ifa_attr): addr_match = False break addr_match = True # if the address attribute matches we need to remove this one if addr_match: list_addr_to_remove.append(addr) for addr in list_addr_to_remove: try: addrs_for_interface.remove(addr) except ValueError as e: log.debug('nlcache: remove_address: exception: %s' % e) def get_ip_addresses(self, ifname: str) -> list: addresses = [] try: with self._cache_lock: intf_addresses = self._addr_cache[ifname] for addr in intf_addresses.get(4, []): addresses.append(addr.attributes[Address.IFA_ADDRESS].value) for addr in intf_addresses.get(6, []): addresses.append(addr.attributes[Address.IFA_ADDRESS].value) return addresses except (KeyError, AttributeError): return addresses def link_has_ip(self, ifname): try: with self._cache_lock: intf_addresses = self._addr_cache[ifname] return bool(intf_addresses.get(4, None) or intf_addresses.get(6, None)) except Exception: return False ############################################################################ ############################################################################ ############################################################################ def add_netconf(self, msg): """ cache RTM_NEWNETCONF objects { family: { ifindex: RTM_NEWNETCONF } } we currently only support AF_INET, AF_INET6 and AF_MPLS family. """ try: with self._netconf_cache_lock: self._netconf_cache[msg.family][msg.get_attribute_value(msg.NETCONFA_IFINDEX)] = msg except Exception: pass def remove_netconf(self, msg): """ Process RTM_DELNETCONF, remove associated entry in our _netconf_cache """ try: with self._netconf_cache_lock: del self._netconf_cache[msg.family][msg.get_attribute_value(msg.NETCONFA_IFINDEX)] except Exception: pass def get_netconf_forwarding(self, family, ifname): """ Return netconf device forwarding value """ try: with self._netconf_cache_lock: return self._netconf_cache[family][self.get_ifindex(ifname)].get_attribute_value(Netconf.NETCONFA_FORWARDING) except Exception: # if KeyError and family == AF_INET6: ipv6 is probably disabled on this device return None def get_netconf_mpls_input(self, ifname): """ Return netconf device MPLS input value """ try: with self._netconf_cache_lock: return self._netconf_cache[AF_MPLS][self.get_ifindex(ifname)].get_attribute_value(Netconf.NETCONFA_INPUT) except Exception: return None ############################################################################ ############################################################################ ############################################################################ @staticmethod def get_user_configured_addresses(ifaceobj_list: list, with_address_virtual=False) -> list: ip4 = [] ip6 = [] for ifaceobj in ifaceobj_list: addresses = ifaceobj.get_attr_value("address") if addresses: for addr_index, addr in enumerate(addresses): if "/" in addr: ip_network_obj = ipnetwork.IPNetwork(addr) else: # if netmask is specified under the stanza we need to use to # create the IPNetwork objects, otherwise let IPNetwork figure # out the correct netmask for ip4 & ip6 ip_network_obj = ipnetwork.IPNetwork(addr, ifaceobj.get_attr_value_n("netmask", addr_index)) if ip_network_obj.version == 6: ip6.append(ip_network_obj) else: ip4.append(ip_network_obj) if not with_address_virtual: continue # # address-virtual and vrrp ips also needs to be accounted for # addresses_virtual = ifaceobj.get_attr_value("address-virtual") vrrp = ifaceobj.get_attr_value("vrrp") for attr_config in (addresses_virtual, vrrp): for addr_virtual_entry in attr_config or []: for addr in addr_virtual_entry.split(): try: ip_network_obj = ipnetwork.IPNetwork(addr) if ip_network_obj.version == 6: ip6.append(ip_network_obj) else: ip4.append(ip_network_obj) except Exception: continue # always return ip4 first, followed by ip6 return ip4 + ip6 def get_managed_ip_addresses(self, ifname: str, ifaceobj_list: list, with_address_virtual: bool = False): """ Get all ip addresses managed by ifupdown2. As a network manager we need to be able to detect manually added ip addresses (with iproute2 for example). We only addressed added by the kernel (scope: link). Args: ifname: str ifaceobj_list: list of ifaceobj with_address_virtual: boolean (to include vrrp and address-virtual) Returns: List of ipnetwork.IPNetwork objects """ config_addrs = set( self.get_user_configured_addresses( ifaceobj_list, with_address_virtual=with_address_virtual, ) ) previous_state_ifaceobjs = statemanager.statemanager_api.get_ifaceobjs(ifname) if previous_state_ifaceobjs: for previous_state_addr in self.get_user_configured_addresses( previous_state_ifaceobjs, with_address_virtual=with_address_virtual, ): config_addrs.add(previous_state_addr) managed_addresses = [] for addr in self.get_ip_addresses(ifname): if addr in config_addrs: managed_addresses.append(addr) elif not addr.scope & Route.RT_SCOPE_LINK: managed_addresses.append(addr) return managed_addresses ############################################################################ ############################################################################ ############################################################################ def addr_is_cached(self, ifname, addr): """ return True if addr is in cache We might need to check if metric/peer and other attribute are also correctly cached. We might also need to add a "force" attribute to skip the cache check :param ifname: :param ifindex: :return: """ try: with self._cache_lock: for cache_addr in self._addr_cache[ifname][addr.version]: try: ifa_address = cache_addr.attributes[Address.IFA_ADDRESS].value if ifa_address.ip == addr.ip and ifa_address.prefixlen == addr.prefixlen: return True except Exception: try: ifa_local = cache_addr.attributes[Address.IFA_LOCAL].value return ifa_local.ip == addr.ip and ifa_local.prefixlen == addr.prefixlen except Exception: pass except (KeyError, AttributeError): pass return False # old def get_link_obj(self, ifname): try: with self._cache_lock: return self._link_cache[ifname] except KeyError: return None def get_link_info_slave_data(self, ifname): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_SLAVE_DATA] except (KeyError, AttributeError): return {} def is_link_kind(self, ifname, _type): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_KIND] == _type except (KeyError, AttributeError): return False def is_link_slave_kind(self, ifname, _type): try: with self._cache_lock: return self._link_cache[ifname].attributes[Link.IFLA_LINKINFO].value[Link.IFLA_INFO_SLAVE_KIND] == _type except (KeyError, AttributeError): return False class NetlinkListenerWithCache(nllistener.NetlinkManagerWithListener, BaseObject): __instance = None VXLAN_UDP_PORT = 4789 @staticmethod def init(log_level): """ Create the singleton via this init function Following calls should use get_instance() :param log_level: :return: """ if not NetlinkListenerWithCache.__instance: try: NetlinkListenerWithCache.__instance = NetlinkListenerWithCache(log_level=log_level) except Exception as e: log.error('NetlinkListenerWithCache: init: %s' % e) traceback.print_exc() @staticmethod def get_instance(): """ Use this function to retrieve the active reference to the NetlinkListenerWithCache, make sure you called .init() first :return: """ if not NetlinkListenerWithCache.__instance: raise NetlinkListenerWithCacheErrorNotInitialized("NetlinkListenerWithCache not initialized") return NetlinkListenerWithCache.__instance @staticmethod def is_init(): return bool(NetlinkListenerWithCache.__instance) def __init__(self, log_level): """ :param log_level: """ if NetlinkListenerWithCache.__instance: raise RuntimeError("NetlinkListenerWithCache: invalid access. Please use NetlinkListenerWithCache.getInstance()") else: NetlinkListenerWithCache.__instance = self nllistener.NetlinkManagerWithListener.__init__( self, groups=( nlpacket.RTMGRP_LINK | nlpacket.RTMGRP_IPV4_IFADDR | nlpacket.RTMGRP_IPV6_IFADDR | nlpacket.RTNLGRP_IPV4_NETCONF | nlpacket.RTNLGRP_IPV6_NETCONF | nlpacket.RTNLGRP_MPLS_NETCONF ), start_listener=False, error_notification=True ) BaseObject.__init__(self) signal.signal(signal.SIGTERM, self.signal_term_handler) signal.signal(signal.SIGINT, self.signal_int_handler) self.cache = _NetlinkCache() # set specific log level to lower-level API nllistener.log.setLevel(log_level) nlpacket.log.setLevel(log_level) nlmanager.log.setLevel(log_level) self.IPNetwork_version_to_family = {4: socket.AF_INET, 6: socket.AF_INET6} nlpacket.mac_int_to_str = lambda mac_int: ':'.join(('%012x' % mac_int)[i:i + 2] for i in range(0, 12, 2)) # Override the nlmanager's mac_int_to_str function # Return an integer in MAC string format: xx:xx:xx:xx:xx:xx instead of xxxx.xxxx.xxxx self.workq_handler = { self.WORKQ_SERVICE_NETLINK_QUEUE: self.service_netlinkq, } # NetlinkListenerWithCache first dumps links and addresses then start # a worker thread before returning. The worker thread processes the # workq mainly to service (process) the netlinkq which contains our # netlink packet (notification coming from the Kernel). # When the main thread is making netlin requests (i.e. bridge add etc # ...) the main thread will sleep (thread.event.wait) until we notify # it when receiving an ack associated with the request. The request # may fail and the kernel won't return an ACK but instead return a # NLMSG_ERROR packet. We need to store those packet separatly because: # - we could have several NLMSG_ERROR for different requests from # different threads (in a multi-threaded ifupdown2 case) # - we want to decode the packet and tell the user/log or even raise # an exception with the appropriate message. # User must check the return value of it's netlink requests and # catch any exceptions, for that purpose please use API: # - tx_nlpacket_get_response_with_error self.errorq = list() self.errorq_lock = threading.Lock() self.errorq_enabled = True # when ifupdown2 starts, we need to fill the netlink cache # GET_LINK/ADDR request are asynchronous, we need to block # and wait for the cache to be filled. We are using this one # time netlinkq_notify_event to wait for the cache completion event self.netlinkq_notify_event = None # another threading event to make sure that the netlinkq worker thread is ready self.is_ready = threading.Event() self.worker = None def __str__(self): return "NetlinkListenerWithCache" def start(self): """ Start NetlinkListener - cache all links, bridges, addresses and netconfs :return: """ self.restart_listener() # set ifupdown2 specific supported and ignore messages self.listener.supported_messages = ( nlpacket.RTM_NEWLINK, nlpacket.RTM_DELLINK, nlpacket.RTM_NEWADDR, nlpacket.RTM_DELADDR, nlpacket.RTM_NEWNETCONF, nlpacket.RTM_DELNETCONF ) self.listener.ignore_messages = ( nlpacket.RTM_GETLINK, nlpacket.RTM_GETADDR, nlpacket.RTM_GETNEIGH, nlpacket.RTM_GETROUTE, nlpacket.RTM_GETQDISC, nlpacket.RTM_NEWNEIGH, nlpacket.RTM_DELNEIGH, nlpacket.RTM_NEWROUTE, nlpacket.RTM_DELROUTE, nlpacket.RTM_DELNETCONF, nlpacket.RTM_NEWQDISC, nlpacket.RTM_DELQDISC, nlpacket.RTM_GETQDISC, nlpacket.NLMSG_ERROR, # should be in supported_messages ? nlpacket.NLMSG_DONE # should be in supported_messages ? ) # get all links and wait for the cache to be filled self.get_all_links_wait_netlinkq() # get all addresses and wait for cache to be filled self.get_all_addresses_wait_netlinkq() # get a netconf dump and wait for the cached to be filled self.get_all_netconf_wait_netlinkq() # TODO: on ifquery we shoudn't start any thread (including listener in NetlinkListener) # only for standalone code. #import sys #for arg in sys.argv: # if 'ifquery' in arg: # self.worker = None # return # start the netlinkq worker thread self.worker = threading.Thread(target=self.main, name='NetlinkListenerWithCache') self.worker.start() self.is_ready.wait() def cleanup(self): if not self.__instance: return # passing 0, 0 to the handler so it doesn't log.info self.signal_term_handler(0, 0) if self.worker: self.worker.join() def main(self): self.is_ready.set() # This loop has two jobs: # - process items on our workq # - process netlink messages on our netlinkq, messages are placed there via our NetlinkListener try: while True: # Sleep until our alarm goes off...NetlinkListener will set the alarm once it # has placed a NetlinkPacket object on our netlinkq. If someone places an item on # our workq they should also set our alarm...if they don't it is not the end of # the world as we will wake up in 1s anyway to check to see if our shutdown_event # has been set. self.alarm.wait(0.1) # when ifupdown2 is not running we could change the timeout to 1 sec or more (daemon mode) # the daemon can also put a hook (pyinotify) on /etc/network/interfaces # if we detect changes to that file it probably means that ifupdown2 will be called very soon # then we can scale up our ops (or maybe unpause some of them) # lets study the use cases self.alarm.clear() if self.shutdown_event.is_set(): break while not self.workq.empty(): (event, options) = self.workq.get() if event == self.WORKQ_SERVICE_NETLINK_QUEUE: self.service_netlinkq(self.netlinkq_notify_event) elif event == self.WORKQ_SERVICE_ERROR: self.logger.error('NetlinkListenerWithCache: WORKQ_SERVICE_ERROR') else: raise Exception("Unsupported workq event %s" % event) except Exception: raise finally: # il faut surement mettre un try/except autour de la boucle au dessus # car s'il y a une exception on ne quitte pas le listener thread self.listener.shutdown_event.set() self.listener.join() def reset_errorq(self): with self.errorq_lock: self.logger.debug("nlcache: reset errorq") self.errorq = [] def rx_rtm_newaddr(self, rxed_addr_packet): super(NetlinkListenerWithCache, self).rx_rtm_newaddr(rxed_addr_packet) self.cache.add_address(rxed_addr_packet) def rx_rtm_dellink(self, link): # cache only supports AF_UNSPEC for now if link.family != socket.AF_UNSPEC: return super(NetlinkListenerWithCache, self).rx_rtm_dellink(link) self.cache.remove_link(link) def rx_rtm_deladdr(self, addr): super(NetlinkListenerWithCache, self).rx_rtm_deladdr(addr) self.cache.remove_address(addr) def rx_rtm_newlink(self, rxed_link_packet): # cache only supports AF_UNSPEC for now # we can modify the cache to support more family: # cache { # intf_name: { # AF_UNSPEC: NetlinkObj, # AF_BRIDGE: NetlinkObj # }, # } if rxed_link_packet.family != socket.AF_UNSPEC: # special handling for AF_BRIDGE packets if rxed_link_packet.family == socket.AF_BRIDGE: self.cache.add_bridge_vlan(rxed_link_packet) return super(NetlinkListenerWithCache, self).rx_rtm_newlink(rxed_link_packet) self.cache.add_link(rxed_link_packet) def rx_rtm_newnetconf(self, msg): super(NetlinkListenerWithCache, self).rx_rtm_newnetconf(msg) self.cache.add_netconf(msg) def rx_rtm_delnetconf(self, msg): super(NetlinkListenerWithCache, self).rx_rtm_delnetconf(msg) self.cache.remove_netconf(msg) def tx_nlpacket_get_response_with_error(self, nl_packet): """ After getting an ACK we need to check if this ACK was in fact an error (NLMSG_ERROR). This function go through the .errorq list to find the error packet associated with our request. If found, we process it and raise an exception with the appropriate information/message. :param nl_packet: :return: """ self.tx_nlpacket_get_response(nl_packet) error_packet = None index = 0 with self.errorq_lock: for error in self.errorq: if error.seq == nl_packet.seq and error.pid == nl_packet.pid: error_packet = error break index += 1 if error_packet: del self.errorq[index] if not error_packet: return True error_code = abs(error_packet.negative_errno) if error_packet.msgtype == NLMSG_DONE or not error_code: # code NLE_SUCCESS...this is an ACK return True if self.debug: error_packet.dump() try: # os.strerror might raise ValueError strerror = os.strerror(error_code) if strerror: error_str = "operation failed with '%s' (%s)" % (strerror, error_code) else: error_str = "operation failed with code %s" % error_code except ValueError: error_str = "operation failed with code %s" % error_code raise Exception(error_str) def tx_nlpacket_get_response_with_error_and_cache_on_ack(self, packet): """ TX packet and manually cache the object """ self.tx_nlpacket_get_response_with_error(packet) # When creating a new link via netlink, we don't always wait for the kernel # NEWLINK notification to be cached to continue. If our request is ACKed by # the OS we assume that the link was successfully created. Since we aren't # waiting for the kernel notification to continue we need to manually fill # our cache with the packet we just TX'ed. Once the NEWLINK notification # is received it will simply override the previous entry. # We need to keep track of those manually cached packets. We set a private # flag on the objects via the attribute priv_flags packet.priv_flags |= NLM_F_REQUEST try: # we need to decode the service header so all the attribute are properly # filled in the packet object that we are about to store in cache. # i.e.: packet.flags shouldn't contain NLM_F_* values but IFF_* (in case of Link object) # otherwise call to cache.link_is_up() will probably return True packet.decode_service_header() except Exception: # we can ignore all errors pass # Then we can use our normal "add_link" API call to cache the packet # and fill up our additional internal data structures. self.cache.add_link(packet) def tx_nlpacket_get_response_with_error_and_wait_for_cache(self, ifname, nl_packet): """ The netlink request are asynchronus, but sometimes the main thread/user would like to wait until the result of the request is cached. To do so a cache event for ifname and nl_packet.msgtype is registered. Then the netlink packet is TXed, errors are checked then we sleep until the cache event is set (or we reach the timeout). This allows us to reliably make sure is up to date with newly created/removed devices or addresses. :param nl_packet: :return: """ wait_event_registered = self.cache.register_wait_event(ifname, nl_packet.msgtype) try: result = self.tx_nlpacket_get_response_with_error(nl_packet) except Exception: # an error was caught, we need to unregister the event and raise again self.cache.unregister_wait_event() raise if wait_event_registered: self.cache.wait_event() return result def get_all_links_wait_netlinkq(self): self.logger.info("requesting link dump") # create netlinkq notify event so we can wait until the links are cached self.netlinkq_notify_event = threading.Event() self.get_all_links() # we also need a dump of all existing bridge vlans self.get_all_br_links(compress_vlans=True) # block until the netlinkq was serviced and cached self.service_netlinkq(self.netlinkq_notify_event) self.netlinkq_notify_event.wait() self.netlinkq_notify_event.clear() def get_all_addresses_wait_netlinkq(self): self.logger.info("requesting address dump") self.netlinkq_notify_event = threading.Event() self.get_all_addresses() # block until the netlinkq was serviced and cached self.service_netlinkq(self.netlinkq_notify_event) self.netlinkq_notify_event.wait() self.netlinkq_notify_event.clear() self.netlinkq_notify_event = False def get_all_netconf_wait_netlinkq(self): self.logger.info("requesting netconf dump") self.netlinkq_notify_event = threading.Event() self.netconf_dump() # block until the netlinkq was serviced and cached self.service_netlinkq(self.netlinkq_notify_event) self.netlinkq_notify_event.wait() self.netlinkq_notify_event.clear() self.netlinkq_notify_event = False def vlan_modify(self, msgtype, ifindex, vlanid_start, vlanid_end=None, bridge_self=False, bridge_master=False, pvid=False, untagged=False): """ iproute2 bridge/vlan.c vlan_modify() """ assert msgtype in (RTM_SETLINK, RTM_DELLINK), "Invalid msgtype %s, must be RTM_SETLINK or RTM_DELLINK" % msgtype assert vlanid_start >= 1 and vlanid_start <= 4096, "Invalid VLAN start %s" % vlanid_start if vlanid_end is None: vlanid_end = vlanid_start assert vlanid_end >= 1 and vlanid_end <= 4096, "Invalid VLAN end %s" % vlanid_end assert vlanid_start <= vlanid_end, "Invalid VLAN range %s-%s, start must be <= end" % (vlanid_start, vlanid_end) debug = msgtype in self.debug bridge_flags = 0 vlan_info_flags = 0 link = Link(msgtype, debug, use_color=self.use_color) link.flags = NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('Bxxxiii', socket.AF_BRIDGE, ifindex, 0, 0) if bridge_self: bridge_flags |= Link.BRIDGE_FLAGS_SELF if bridge_master: bridge_flags |= Link.BRIDGE_FLAGS_MASTER if pvid: vlan_info_flags |= Link.BRIDGE_VLAN_INFO_PVID if untagged: vlan_info_flags |= Link.BRIDGE_VLAN_INFO_UNTAGGED ifla_af_spec = OrderedDict() if bridge_flags: ifla_af_spec[Link.IFLA_BRIDGE_FLAGS] = bridge_flags # just one VLAN if vlanid_start == vlanid_end: ifla_af_spec[Link.IFLA_BRIDGE_VLAN_INFO] = [(vlan_info_flags, vlanid_start), ] # a range of VLANs else: ifla_af_spec[Link.IFLA_BRIDGE_VLAN_INFO] = [ (vlan_info_flags | Link.BRIDGE_VLAN_INFO_RANGE_BEGIN, vlanid_start), (vlan_info_flags | Link.BRIDGE_VLAN_INFO_RANGE_END, vlanid_end) ] link.add_attribute(Link.IFLA_AF_SPEC, ifla_af_spec) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error(link) def vlan_set_bridge_binding(self, ifname, bridge_binding=True): """ Set VLAN_FLAG_BRIDGE_BINDING on vlan interface :param ifname: the vlan interface :param bridge_binding: True to set the flag, False to unset """ self.logger.info("%s: netlink: ip link set dev %s type vlan bridge_binding %s" % (ifname, ifname, "on" if bridge_binding else "off")) debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('=BxxxiLL', socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) info_data = {Link.IFLA_VLAN_FLAGS: {Link.VLAN_FLAG_BRIDGE_BINDING: bridge_binding}} link.add_attribute(Link.IFLA_LINKINFO, { Link.IFLA_INFO_KIND: "vlan", Link.IFLA_INFO_DATA: info_data }) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error(link) ############################################################################################################# # Netlink API ############################################################################################### ############################################################################################################# def link_add(self, ifname, kind): self.link_add_with_attributes(ifname, kind, {}) def link_add_with_attributes(self, ifname, kind, ifla): """ Build and TX a RTM_NEWLINK message to add the desired interface """ if ifla: self.logger.info("%s: netlink: ip link add dev %s type %s (with attributes)" % (ifname, ifname, kind)) self.logger.debug("attributes: %s" % ifla) else: self.logger.info("%s: netlink: ip link add dev %s type %s" % (ifname, ifname, kind)) try: debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('Bxxxiii', socket.AF_UNSPEC, 0, 0, 0) for nl_attr, value in list(ifla.items()): link.add_attribute(nl_attr, value) link.add_attribute(Link.IFLA_IFNAME, ifname) link.add_attribute(Link.IFLA_LINKINFO, { Link.IFLA_INFO_KIND: kind }) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error_and_cache_on_ack(link) except Exception as e: raise Exception("%s: cannot create link %s type %s" % (ifname, ifname, kind)) def link_add_with_attributes_dry_run(self, ifname, kind, ifla): self.log_info_ifname_dry_run(ifname, "netlink: ip link add dev %s type %s" % (ifname, kind)) self.logger.debug("attributes: %s" % ifla) ### def __link_set_flag(self, ifname, flags): """ Bring interface 'ifname' up (raises on error) :param ifname: :return: """ try: link = Link(RTM_NEWLINK, RTM_NEWLINK in self.debug, use_color=self.use_color) link.flags = NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack("=BxxxiLL", socket.AF_UNSPEC, 0, flags, Link.IFF_UP) link.add_attribute(Link.IFLA_IFNAME, ifname) link.build_message(next(self.sequence), self.pid) result = self.tx_nlpacket_get_response_with_error(link) # if we reach this code it means the operation went through # without exception we can update the cache value this is # needed for the following case (and probably others): # # ifdown bond0 ; ip link set dev bond_slave down # ifup bond0 # at the beginning the slaves are admin down # ifupdownmain:run_up link up the slave # the bond addon check if the slave is up or down # and admin down the slave before enslavement # but the cache didn't process the UP notification yet # so the cache has a stale value and we try to enslave # a port, that is admin up, to a bond resulting # in an unexpected failure self.cache.override_link_flag(ifname, flags) return result except Exception as e: raise NetlinkError(e, "ip link set dev %s %s" % (ifname, "up" if flags == Link.IFF_UP else "down"), ifname=ifname) def link_up(self, ifname): if not self.cache.link_is_up(ifname): self.logger.info("%s: netlink: ip link set dev %s up" % (ifname, ifname)) self.__link_set_flag(ifname, flags=Link.IFF_UP) def link_up_force(self, ifname): self.logger.info("%s: netlink: ip link set dev %s up" % (ifname, ifname)) self.__link_set_flag(ifname, flags=Link.IFF_UP) def link_down(self, ifname): if self.cache.link_is_up(ifname): self.logger.info("%s: netlink: ip link set dev %s down" % (ifname, ifname)) self.__link_set_flag(ifname, flags=0) def link_down_force(self, ifname): self.logger.info("%s: netlink: ip link set dev %s down" % (ifname, ifname)) self.__link_set_flag(ifname, flags=0) def link_up_dry_run(self, ifname): self.log_info_ifname_dry_run(ifname, "netlink: ip link set dev %s up" % ifname) def link_down_dry_run(self, ifname): self.log_info_ifname_dry_run(ifname, "netlink: ip link set dev %s down" % ifname) def link_down_force_dry_run(self, ifname): self.link_down_dry_run(ifname) ### def __link_set_protodown(self, ifname, state): debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack("=BxxxiLL", socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) link.add_attribute(Link.IFLA_PROTO_DOWN, state) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error(link) def link_set_protodown_on(self, ifname): """ Bring ifname up by setting IFLA_PROTO_DOWN on """ if self.cache.get_link_protodown(ifname) == 1: return True self.logger.info("%s: netlink: set link %s protodown on" % (ifname, ifname)) try: self.__link_set_protodown(ifname, 1) except Exception as e: raise NetlinkError(e, "cannot set link %s protodown on" % ifname, ifname=ifname) def link_set_protodown_off(self, ifname): """ Take ifname down by setting IFLA_PROTO_DOWN off """ if self.cache.get_link_protodown(ifname) == 0: return True self.logger.info("%s: netlink: set link %s protodown off" % (ifname, ifname)) try: self.__link_set_protodown(ifname, 0) except Exception as e: raise NetlinkError(e, "cannot set link %s protodown off" % ifname, ifname=ifname) def link_set_protodown_on_dry_run(self, ifname): self.log_info_ifname_dry_run(ifname, "netlink: set link %s protodown on" % ifname) def link_set_protodown_off_dry_run(self, ifname): self.log_info_ifname_dry_run(ifname, "netlink: set link %s protodown off" % ifname) ### def link_del(self, ifname): """ Send RTM_DELLINK request :param ifname: :return: """ try: try: ifindex = self.cache.get_ifindex(ifname) except NetlinkCacheIfnameNotFoundError: # link doesn't exists on the system return True self.logger.info("%s: netlink: ip link del %s" % (ifname, ifname)) debug = RTM_DELLINK in self.debug link = Link(RTM_DELLINK, debug, use_color=self.use_color) link.flags = NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack("Bxxxiii", socket.AF_UNSPEC, ifindex, 0, 0) link.build_message(next(self.sequence), self.pid) try: # We need to register this ifname so the cache can ignore and discard # any further RTM_NEWLINK packet until we receive the associated # RTM_DELLINK notification self.cache.append_to_ignore_rtm_newlinkq(ifname) result = self.tx_nlpacket_get_response_with_error(link) # Manually purge the cache entry for ifname to make sure we don't have # any stale value in our cache self.cache.force_remove_link(ifname) return result except Exception: # Something went wrong while sending the RTM_DELLINK request # we need to clear ifname from the ignore_rtm_newlinkq list self.cache.remove_from_ignore_rtm_newlinkq(ifname) raise except Exception as e: raise NetlinkError(e, "cannot delete link %s" % ifname, ifname=ifname) def link_del_dry_run(self, ifname): self.log_info_ifname_dry_run(ifname, "netlink: ip link del %s" % ifname) ### def __link_set_master(self, ifname, master_ifindex, master_ifname=None): debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack("=BxxxiLL", socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) link.add_attribute(Link.IFLA_MASTER, master_ifindex) link.build_message(next(self.sequence), self.pid) result = self.tx_nlpacket_get_response_with_error(link) # opti: # if we reach this code it means the slave/unslave opreation went through # we can manually update our cache to reflect the change without having # to wait for the netlink notification if master_ifindex: self.cache.force_add_slave(master_ifname, ifname) else: self.cache.override_cache_unslave_link(slave=ifname, master=master_ifname) return result def link_set_master(self, ifname, master_ifname): self.logger.info("%s: netlink: ip link set dev %s master %s" % (ifname, ifname, master_ifname)) try: self.__link_set_master(ifname, self.cache.get_ifindex(master_ifname), master_ifname=master_ifname) except Exception as e: raise NetlinkError(e, "cannot enslave link %s to %s" % (ifname, master_ifname), ifname=ifname) def link_set_nomaster(self, ifname): self.logger.info("%s: netlink: ip link set dev %s nomaster" % (ifname, ifname)) try: self.cache.append_to_rtm_newlink_nomasterq(ifname) self.__link_set_master(ifname, 0) except Exception as e: self.cache.remove_from_rtm_newlink_nomasterq(ifname) raise NetlinkError(e, "cannot un-enslave link %s" % ifname, ifname=ifname) def link_set_master_dry_run(self, ifname, master_dev): self.log_info_ifname_dry_run(ifname, "netlink: ip link set dev %s master %s" % (ifname, master_dev)) def link_set_nomaster_dry_run(self, ifname): self.log_info_ifname_dry_run(ifname, "netlink: ip link set dev %s nomaster" % ifname) ### def link_set_address_dry_run(self, ifname, hw_address, hw_address_int): self.log_info_ifname_dry_run(ifname, "netlink: ip link set dev %s address %s" % (ifname, hw_address)) def link_set_address(self, ifname, hw_address, hw_address_int): is_link_up = self.cache.link_is_up(ifname) # check if the link is already up or not if the link is # up we need to down it then make sure we up it again try: if is_link_up: self.link_down_force(ifname) self.logger.info("%s: netlink: ip link set dev %s address %s" % (ifname, ifname, hw_address)) debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('Bxxxiii', socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) link.add_attribute(Link.IFLA_ADDRESS, hw_address) link.build_message(next(self.sequence), self.pid) result = self.tx_nlpacket_get_response_with_error(link) # if we reach that code it means we got an ACK from the kernel, we can pro-actively # update our local cache to reflect the change until the notificate arrives self.cache.update_link_ifla_address(ifname, hw_address, hw_address_int) return result except Exception as e: raise NetlinkError(e, "cannot set dev %s address %s" % (ifname, hw_address), ifname=ifname) finally: if is_link_up: self.link_up_force(ifname) ### __macvlan_mode = { "private": Link.MACVLAN_MODE_PRIVATE, "vepa": Link.MACVLAN_MODE_VEPA, "bridge": Link.MACVLAN_MODE_BRIDGE, "passthru": Link.MACVLAN_MODE_PASSTHRU, "source": Link.MACVLAN_MODE_SOURCE } def link_add_macvlan(self, ifname, macvlan_ifname, macvlan_mode=None): self.logger.info( "%s: netlink: ip link add link %s name %s type macvlan mode %s" % (ifname, ifname, macvlan_ifname, macvlan_mode if macvlan_mode else "private") ) try: ifindex = self.cache.get_ifindex(ifname) debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack("Bxxxiii", socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) if ifindex: link.add_attribute(Link.IFLA_LINK, ifindex) link.add_attribute(Link.IFLA_LINKINFO, { Link.IFLA_INFO_KIND: "macvlan", Link.IFLA_INFO_DATA: { Link.IFLA_MACVLAN_MODE: self.__macvlan_mode.get( macvlan_mode, Link.MACVLAN_MODE_PRIVATE ) } }) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error_and_cache_on_ack(link) except Exception as e: raise Exception( "netlink: %s: cannot create macvlan %s: %s" % (ifname, macvlan_ifname, str(e)) ) def link_add_macvlan_dry_run(self, ifname, macvlan_ifame, macvlan_mode=None): self.log_info_ifname_dry_run( ifname, "netlink: ip link add link %s name %s type macvlan mode %s" % (ifname, macvlan_ifame, macvlan_mode if macvlan_mode else "private") ) return True ### def link_add_vrf(self, ifname, vrf_table): self.logger.info("%s: netlink: ip link add dev %s type vrf table %s" % (ifname, ifname, vrf_table)) debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('Bxxxiii', socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) link.add_attribute(Link.IFLA_LINKINFO, { Link.IFLA_INFO_KIND: "vrf", Link.IFLA_INFO_DATA: { Link.IFLA_VRF_TABLE: int(vrf_table) } }) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error_and_cache_on_ack(link) def link_add_vrf_dry_run(self, ifname, vrf_table): self.log_info_ifname_dry_run(ifname, "netlink: ip link add dev %s type vrf table %s" % (ifname, vrf_table)) return True ### def link_add_bridge(self, ifname, mtu=None): self.logger.info("%s: netlink: ip link add dev %s type bridge" % (ifname, ifname)) debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('Bxxxiii', socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) if mtu: self.logger.info("%s: netlink: set bridge mtu %s" % (ifname, mtu)) link.add_attribute(Link.IFLA_MTU, mtu) link.add_attribute(Link.IFLA_LINKINFO, { Link.IFLA_INFO_KIND: "bridge", }) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error_and_cache_on_ack(link) def link_add_bridge_dry_run(self, ifname): self.log_info_ifname_dry_run(ifname, "netlink: ip link add dev %s type bridge" % ifname) return True def link_set_bridge_info_data(self, ifname, ifla_info_data): self.logger.info( "%s: netlink: ip link set dev %s type bridge (with attributes)" % (ifname, ifname) ) self.logger.debug("attributes: %s" % ifla_info_data) try: debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('Bxxxiii', socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) link.add_attribute(Link.IFLA_LINKINFO, { Link.IFLA_INFO_KIND: "bridge", Link.IFLA_INFO_DATA: ifla_info_data }) link.build_message(next(self.sequence), self.pid) result = self.tx_nlpacket_get_response_with_error(link) self.cache.update_link_info_data(ifname, ifla_info_data) return result except Exception as e: raise Exception("%s: netlink: cannot create bridge or set attributes: %s" % (ifname, str(e))) def link_set_bridge_info_data_dry_run(self, ifname, ifla_info_data): self.log_info_ifname_dry_run(ifname, "netlink: ip link add dev %s type bridge (with attributes)" % ifname) self.logger.debug("attributes: %s" % ifla_info_data) ### def link_add_bridge_vlan(self, ifname, vlan_id): """ Add VLAN(s) to a bridge interface """ self.logger.info("%s: netlink: bridge vlan add vid %s dev %s" % (ifname, vlan_id, ifname)) try: ifindex = self.cache.get_ifindex(ifname) self.vlan_modify(RTM_SETLINK, ifindex, vlan_id, bridge_self=True) # TODO: we should probably fill our internal cache when when the ACK is received. except Exception as e: raise NetlinkError(e, "cannot add bridge vlan %s" % vlan_id, ifname=ifname) def link_del_bridge_vlan(self, ifname, vlan_id): """ Delete VLAN(s) from a bridge interface """ self.logger.info("%s: netlink: bridge vlan del vid %s dev %s" % (ifname, vlan_id, ifname)) try: ifindex = self.cache.get_ifindex(ifname) self.vlan_modify(RTM_DELLINK, ifindex, vlan_id, bridge_self=True) except Exception as e: raise NetlinkError(e, "cannot remove bridge vlan %s" % vlan_id, ifname=ifname) def link_add_bridge_vlan_dry_run(self, ifname, vlan_id): self.log_info_ifname_dry_run(ifname, "netlink: bridge vlan add vid %s dev %s" % (vlan_id, ifname)) def link_del_bridge_vlan_dry_run(self, ifname, vlan_id): self.log_info_ifname_dry_run(ifname, "netlink: bridge vlan del vid %s dev %s" % (vlan_id, ifname)) ### def link_add_vlan(self, vlan_raw_device, ifname, vlan_id, vlan_protocol=None, bridge_binding=None): """ ifindex is the index of the parent interface that this sub-interface is being added to If you name an interface swp2.17 but assign it to vlan 12, the kernel will return a very misleading NLE_MSG_OVERFLOW error. It only does this check if the ifname uses dot notation. Do this check here so we can provide a more intuitive error """ vlan_iproute2_cmd = ["ip link add link %s name %s type vlan id %s" % (vlan_raw_device, ifname, vlan_id)] try: ifla_info_data = {Link.IFLA_VLAN_ID: vlan_id} if vlan_protocol: vlan_iproute2_cmd.append("protocol %s" % vlan_protocol) ifla_info_data[Link.IFLA_VLAN_PROTOCOL] = vlan_protocol bridge_binding_str = "" if bridge_binding is not None: bridge_binding_str = "bridge_binding %s" % ("on" if bridge_binding else "off") ifla_info_data[Link.IFLA_VLAN_FLAGS] = {Link.VLAN_FLAG_BRIDGE_BINDING: bridge_binding} self.logger.info("%s: netlink: %s %s" % (ifname, " ".join(vlan_iproute2_cmd), bridge_binding_str)) if "." in ifname: ifname_vlanid = int(ifname.split(".")[-1]) if ifname_vlanid != vlan_id: raise Exception( "Interface %s must belong to VLAN %d (VLAN %d was requested)" % (ifname, ifname_vlanid, vlan_id) ) ifindex = self.cache.get_ifindex(vlan_raw_device) debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('Bxxxiii', socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) link.add_attribute(Link.IFLA_LINK, ifindex) link.add_attribute(Link.IFLA_LINKINFO, { Link.IFLA_INFO_KIND: "vlan", Link.IFLA_INFO_DATA: ifla_info_data }) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error_and_cache_on_ack(link) except Exception as e: if "Invalid argument" in str(e) and bridge_binding is not None: raise RetryCMD(cmd=" ".join(vlan_iproute2_cmd)) else: raise NetlinkError(e, "cannot create vlan %s %s" % (ifname, vlan_id), ifname=ifname) def link_add_vlan_dry_run(self, vlan_raw_device, ifname, vlan_id, vlan_protocol=None): """ ifindex is the index of the parent interface that this sub-interface is being added to If you name an interface swp2.17 but assign it to vlan 12, the kernel will return a very misleading NLE_MSG_OVERFLOW error. It only does this check if the ifname uses dot notation. Do this check here so we can provide a more intuitive error """ if vlan_protocol: self.log_info_ifname_dry_run( ifname, "netlink: ip link add link %s name %s type vlan id %s protocol %s" % (vlan_raw_device, ifname, vlan_id, vlan_protocol) ) else: self.log_info_ifname_dry_run( ifname, "netlink: ip link add link %s name %s type vlan id %s" % (vlan_raw_device, ifname, vlan_id) ) ### def link_add_vxlan_with_info_data(self, ifname, info_data): """ cmd = ["ip link add %s type vxlan id %s" % (ifname, id)] if port: cmd.append("dstport %s" % port) info_data[nlpacket.Link.IFLA_VXLAN_PORT] = int(port) if local: cmd.append("local %s" % local) info_data[nlpacket.Link.IFLA_VXLAN_LOCAL] = local if ageing: cmd.append("ageing %s" % ageing) info_data[nlpacket.Link.IFLA_VXLAN_AGEING] = int(ageing) if group: if group.ip.is_multicast: cmd.append("group %s" % group) else: cmd.append("remote %s" % group) info_data[nlpacket.Link.IFLA_VXLAN_GROUP] = group else: cmd.append("noremote") if not learning: cmd.append("nolearning") info_data[nlpacket.Link.IFLA_VXLAN_LEARNING] = int(learning) if not udp_csum: cmd.append("noudpcsum") info_data[nlpacket.Link.IFLA_VXLAN_UDP_CSUM] = int(udp_csum) if physdev: cmd.append("dev %s" % physdev) info_data[nlpacket.Link.IFLA_VXLAN_LINK] = self.cache.get_ifindex(physdev) if ttl: cmd.append("ttl %s" % ttl) info_data[nlpacket.Link.IFLA_VXLAN_TTL] = ttl self.logger.info('%s: netlink: %s' % (ifname, " ".join(cmd))) :param ifname: :param info_data: :return: """ self.logger.info( "%s: netlink: ip link add dev %s type vxlan id %s (with attributes)" % (ifname, ifname, info_data.get(Link.IFLA_VXLAN_ID)) ) self.logger.debug("attributes: %s" % info_data) debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('Bxxxiii', socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) link.add_attribute(Link.IFLA_LINKINFO, { Link.IFLA_INFO_KIND: "vxlan", Link.IFLA_INFO_DATA: info_data }) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error_and_cache_on_ack(link) def link_add_vxlan_with_info_data_dry_run(self, ifname, info_data): self.log_info_ifname_dry_run( ifname, "netlink: ip link add dev %s type vxlan id %s (with attributes)" % (ifname, info_data.get(Link.IFLA_VXLAN_ID)) ) self.logger.debug("attributes: %s" % info_data) return True ### def link_add_bond_with_info_data(self, ifname, ifla_info_data): self.logger.info( "%s: netlink: ip link add dev %s type bond (with attributes)" % (ifname, ifname) ) self.logger.debug("attributes: %s" % ifla_info_data) try: debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack('Bxxxiii', socket.AF_UNSPEC, 0, 0, 0) link.add_attribute(Link.IFLA_IFNAME, ifname) link.add_attribute(Link.IFLA_LINKINFO, { Link.IFLA_INFO_KIND: "bond", Link.IFLA_INFO_DATA: ifla_info_data }) link.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error_and_cache_on_ack(link) except Exception as e: raise Exception("%s: netlink: cannot create bond with attributes: %s" % (ifname, str(e))) def link_add_bond_with_info_data_dry_run(self, ifname, ifla_info_data): self.log_info_ifname_dry_run(ifname, "netlink: ip link add dev %s type bond (with attributes)" % ifname) self.logger.debug("attributes: %s" % ifla_info_data) ### def link_set_brport_with_info_slave_data(self, ifname, kind, ifla_info_data, ifla_info_slave_data): """ Build and TX a RTM_NEWLINK message to add the desired interface """ self.logger.info("%s: netlink: ip link set dev %s: bridge port attributes" % (ifname, ifname)) self.logger.debug("attributes: %s" % ifla_info_slave_data) try: debug = RTM_NEWLINK in self.debug link = Link(RTM_NEWLINK, debug, use_color=self.use_color) link.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK link.body = struct.pack("Bxxxiii", socket.AF_UNSPEC, 0, 0, 0) if ifname: link.add_attribute(Link.IFLA_IFNAME, ifname) linkinfo = dict() if kind: linkinfo[Link.IFLA_INFO_KIND] = kind linkinfo[Link.IFLA_INFO_DATA] = ifla_info_data linkinfo[Link.IFLA_INFO_SLAVE_KIND] = "bridge" linkinfo[Link.IFLA_INFO_SLAVE_DATA] = ifla_info_slave_data link.add_attribute(Link.IFLA_LINKINFO, linkinfo) link.build_message(next(self.sequence), self.pid) # the brport already exists and is cached - after this operation we most # likely don't need to do anything about the brport so we don't need to # wait for the new notification to be cached. return self.tx_nlpacket_get_response_with_error(link) except Exception as e: raise Exception("netlink: %s: cannot set %s (bridge slave) with options: %s" % (kind, ifname, str(e))) def link_set_brport_with_info_slave_data_dry_run(self, ifname, kind, ifla_info_data, ifla_info_slave_data): self.log_info_ifname_dry_run(ifname, "netlink: ip link set dev %s: bridge port attributes" % ifname) self.logger.debug("attributes: %s" % ifla_info_slave_data) ############################################################################ # ADDRESS ############################################################################ def addr_add_dry_run(self, ifname, addr, broadcast=None, peer=None, scope=None, preferred_lifetime=None, metric=None): log_msg = ["netlink: ip addr add %s dev %s" % (addr, ifname)] if scope: log_msg.append("scope %s" % scope) if broadcast: log_msg.append("broadcast %s" % broadcast) if preferred_lifetime: log_msg.append("preferred_lft %s" % preferred_lifetime) if peer: log_msg.append("peer %s" % peer) if metric: log_msg.append("metric %s" % metric) self.log_info_ifname_dry_run(ifname, " ".join(log_msg)) def addr_add(self, ifname, addr, broadcast=None, peer=None, scope=None, preferred_lifetime=None, metric=None, nodad=False): log_msg = ["%s: netlink: ip addr add %s dev %s" % (ifname, addr, ifname)] log_msg_displayed = False try: # We might need to check if metric/peer and other attribute are also # correctly cached. # We might also need to add a "force" attribute to skip the cache check if self.cache.addr_is_cached(ifname, addr): return if scope: log_msg.append("scope %s" % scope) scope_value = RT_SCOPES.get(scope, 0) else: scope_value = 0 debug = RTM_NEWADDR in self.debug packet = Address(RTM_NEWADDR, debug, use_color=self.use_color) packet.flags = NLM_F_CREATE | NLM_F_REQUEST | NLM_F_ACK packet.family = self.IPNetwork_version_to_family.get(addr.version) packet.add_attribute(Address.IFA_ADDRESS, addr) packet.add_attribute(Address.IFA_LOCAL, addr) if nodad: log_msg.append("nodad") packet.add_attribute(Address.IFA_FLAGS, Address.IFA_F_NODAD) if broadcast: log_msg.append("broadcast %s" % broadcast) packet.add_attribute(Address.IFA_BROADCAST, ipnetwork.IPAddress(broadcast)) if preferred_lifetime: # struct ifa_cacheinfo { # __u32 ifa_prefered; # __u32 ifa_valid; # __u32 cstamp; /* created timestamp, hundredths of seconds */ # __u32 tstamp; /* updated timestamp, hundredths of seconds */ # }; log_msg.append("preferred_lft %s" % preferred_lifetime) if preferred_lifetime.lower() == "forever": preferred_lifetime = INFINITY_LIFE_TIME packet.add_attribute(Address.IFA_CACHEINFO, (int(preferred_lifetime), INFINITY_LIFE_TIME, 0, 0)) if metric: log_msg.append("metric %s" % metric) packet.add_attribute(Address.IFA_RT_PRIORITY, int(metric)) if peer: log_msg.append("peer %s" % peer) # peer is already in nlmanager.ipnetwork.IPNetwork format packet.add_attribute(Address.IFA_ADDRESS, peer) packet_prefixlen = peer.prefixlen else: packet_prefixlen = addr.prefixlen self.logger.info(" ".join(log_msg)) log_msg_displayed = True packet.body = struct.pack("=4Bi", packet.family, packet_prefixlen, 0, scope_value, self.cache.get_ifindex(ifname)) packet.build_message(next(self.sequence), self.pid) return self.tx_nlpacket_get_response_with_error(packet) except Exception as e: if not log_msg_displayed: # just in case we get an exception before we reach the log.info # we should display it before we raise the exception log.info(" ".join(log_msg)) raise NetlinkError(e, "cannot add address %s dev %s" % (addr, ifname), ifname=ifname) ### def addr_del_dry_run(self, ifname, addr): self.log_info_ifname_dry_run(ifname, "netlink: ip addr del %s dev %s" % (addr, ifname)) def addr_del(self, ifname, addr): if not self.cache.addr_is_cached(ifname, addr): return self.logger.info("%s: netlink: ip addr del %s dev %s" % (ifname, addr, ifname)) try: debug = RTM_DELADDR in self.debug packet = Address(RTM_DELADDR, debug, use_color=self.use_color) packet.flags = NLM_F_REQUEST | NLM_F_ACK packet.family = self.IPNetwork_version_to_family.get(addr.version) packet.body = struct.pack("=4Bi", packet.family, addr.prefixlen, 0, 0, self.cache.get_ifindex(ifname)) packet.add_attribute(Address.IFA_LOCAL, addr) packet.build_message(next(self.sequence), self.pid) result = self.tx_nlpacket_get_response_with_error(packet) # RTM_DELADDR successful, we need to update our cache # to make sure we don't have any stale ip addr cached self.cache.force_remove_addr(ifname, addr) return result except Exception as e: raise NetlinkError(e, "cannot delete address %s dev %s" % (addr, ifname), ifname=ifname) def addr_flush(self, ifname): """ From iproute2/ip/ipaddress.c /* * Note that the kernel may delete multiple addresses for one * delete request (e.g. if ipv4 address promotion is disabled). * Since a flush operation is really a series of delete requests * its possible that we may request an address delete that has * already been done by the kernel. Therefore, ignore EADDRNOTAVAIL * errors returned from a flush request */ """ for addr in self.cache.get_ip_addresses(ifname): try: self.addr_del(ifname, addr) except Exception: pass ######################## # TEMPORARY DEBUG CODE # ######################## def DEBUG_ON(self): self.debug_link(True) self.debug_address(True) nllistener.log.setLevel(DEBUG) nlpacket.log.setLevel(DEBUG) nlmanager.log.setLevel(DEBUG) def DEBUG_OFF(self): self.debug_address(False) self.debug_link(False) nllistener.log.setLevel(WARNING) nlpacket.log.setLevel(WARNING) nlmanager.log.setLevel(WARNING)