"""This module contains MC objects representing generic capability interfacesthat may be realized by multiple concrete/image MC objects.A good example of this is routers. Different types of routers may realize BGPand OSPF connections, so the GenericRouter object is located here."""importnetaddrfrombase_objectsimportSwitch,VMEndpointfromfirewheel.control.experiment_graphimportrequire_class
[docs]@require_class(VMEndpoint)classGenericRouter:""" This MC object represents interfaces common to many/all router platforms. This mostly takes the form of defining routing protocol connections, where the interconnection between routers is formed by a ``<protocol>_connect()`` method that takes the place of the typical :py:meth:`connect() <base_objects.VMEndpoint.connect>` method. Currently supported protocols are: * :term:`OSPF <Open Shortest Path First>` * :term:`BGP <Border Gateway Protocol>` By default, routes from one protocol will not propagate to peers using other protocols. However, this object supports explicit definition of route redistribution. "In a router, route redistribution allows a network that uses one routing protocol to route traffic dynamically based on information learned from another routing protocol." [#]_ Currently supported redistributions include: - :term:`OSPF <Open Shortest Path First>` into :term:`BGP <Border Gateway Protocol>` - :term:`BGP <Border Gateway Protocol>` into :term:`OSPF <Open Shortest Path First>` - Connected into :term:`OSPF <Open Shortest Path First>` .. [#] https://en.wikipedia.org/wiki/Route_redistribution """def__init__(self,name=None):"""Initialize common attributes for all routers. For example, routers need a name and a routing dictionary. Args: name (str, optional): The name of the router. This is typically created with the creation of a new :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>`, but this parameter also provides that ability. Defaults to :py:data:`None`. Raises: NameError: Occurs if the router does not have a name. Attributes: name (str): The hostname of this router. routing (dict): The routing configuration for this router, including protocol configurations. """# Set the Vertex typeself.type="router"# A name must be specified when using a GenericRouter.self.name=getattr(self,"name",None)ifself.nameisNone:raiseNameError("Name must be specified for router!")# Overwrite the Vertex name if one is providedifname:self.name=name# Sanity check that this Vertex attribute has not been created.self.routing=getattr(self,"routing",{})
[docs]defredistribute_bgp_into_ospf(self):""" Enable redistributing routes from BGP peers to OSPF peers. Raises: Exception: If some unknown error occurs while trying to redistribute routes. """try:try:self.routing["ospf"]["redistribution"]={"bgp":{}}exceptAttributeError:self.routing={"ospf":{"redistribution":{"bgp":{}}}}exceptKeyError:if"ospf"notinself.routing:self.routing["ospf"]={}if"redistribution"notinself.routing["ospf"]:self.routing["ospf"]["redistribution"]={}self.routing["ospf"]["redistribution"]["bgp"]={}exceptException:self.log.exception("Cannot redistribute OSPF into BGP (%s)",self.name,)raise
[docs]defredistribute_ospf_into_bgp(self):""" Enable redistributing routes from OSPF peers to BGP peers. Raises: Exception: If some unknown error occurs while trying to redistribute routes. """try:try:self.routing["bgp"]["redistribution"]={"ospf":{}}exceptAttributeError:self.routing={"bgp":{"redistribution":{"ospf":{}}}}exceptKeyError:if"bgp"notinself.routing:self.routing["bgp"]={}if"redistribution"notinself.routing["bgp"]:self.routing["bgp"]["redistribution"]={}self.routing["bgp"]["redistribution"]["ospf"]={}exceptException:self.log.exception("Cannot redistribute BGP into OSPF (%s)",self.name,)raise
[docs]defredistribute_ospf_connected(self):""" Redistribute routes for directly connected subnets to OSPF peers. """try:if"ospf"notinself.routing:self.log.warning("OSPF not yet defined for: %s. Creating it now.",self.name,)self.routing["ospf"]={}exceptAttributeError:self.log.exception("No routing block defined for: %s. Must specify routing protocols ""before redistribution",self.name,)returnif"redistribution"notinself.routing["ospf"]:self.routing["ospf"]["redistribution"]={}self.routing["ospf"]["redistribution"]["connected"]={}
[docs]defospf_connect(self,switch,ip,netmask,delay=None,rate=None,rate_unit=None,packet_loss=None,area="0",):"""Connect this router to a switch, and define OSPF on the connection. Note: The rate is set as a multiple of bits **not** bytes. That is, a rate of ``1 kbit`` would equal 1000 bits, not 1000 bytes. For bytes, multiply the rate by 8 (e.g. 64 KBytes = 8 * 64 = 512 kbit). Args: switch (base_objects.Switch): Switch instance to connect to. ip (str or netaddr.IPAddress): IP address to use on the connecting interface. netmask (str or netaddr.IPAddress): The netmask for the connecting interface. The netmask can either be in Dotted Decimal or CIDR (without the slash) notation. That is, both ``"255.255.255.0"`` and ``"24"`` would represent the same netmask. delay (str): The amount of egress delay to add for the link. This should be formatted like ``<delay><unit of delay>``. For example, ``100ms``. You must add this in the opposing direction if you want it to be bidirectional. rate (int): The maximum egress transmission rate (e.g. bandwidth of this link) as a multiple of bits. The ``rate_unit`` should also be set if the unit is not ``mbit``. rate_unit (str): The bandwidth unit (one of ``{'kbit', 'mbit', 'gbit'}``). Defaults to ``"mbit"``. packet_loss (int): Percent of packet loss on the link. For example, ``packet_loss = 25`` is 25% packet loss. area (str): The OSPF area. Defaults to ``"0"``. """self.routing=getattr(self,"routing",{})if"parameters"notinself.routingornotself.routing["parameters"]:self.routing["parameters"]={}if"ospf"notinself.routingornotself.routing["ospf"]:self.routing["ospf"]={}if("interfaces"notinself.routing["ospf"]ornotself.routing["ospf"]["interfaces"]):self.routing["ospf"]["interfaces"]={}if"areas"notinself.routing["ospf"]ornotself.routing["ospf"]["areas"]:self.routing["ospf"]["areas"]={}net=netaddr.IPNetwork(ip)iface_name,_=self.connect(switch,ip,netmask,delay=delay,rate=rate,rate_unit=rate_unit,packet_loss=packet_loss,)# Add in OSPF informationself.routing["ospf"]["interfaces"][iface_name]={"dead-interval":"40","hello-interval":"10","area":area,"transmit-delay":"1.0","retransmit-interval":"5.0",}self.routing["ospf"]["areas"][area]={}if("parameters"notinself.routingor"router-id"notinself.routing["parameters"]):self.routing["parameters"]["router-id"]=str(net.ip)
[docs]defvalid_as(self,as_num):""" Validate that a given AS number is valid. The valid AS numbers were pulled from [#]_. Note: This may not be completely up-to-date. Args: as_num (str): AS number, convertible to an integer. Returns: bool: :py:data:`True` if the AS number is valid, :py:data:`False` otherwise. .. [#] https://en.wikipedia.org/wiki/Autonomous_system_(Internet) """try:num=int(as_num)if0<num<4294967295:returnTrueexceptTypeError:self.log.error("The AS number %s is not valid",as_num)exceptValueError:self.log.error("The AS number %s is not valid",as_num)returnFalse
[docs]defset_bgp_as(self,as_num):""" Set the Autonomous System Number (ASN) used by BGP for this router. Args: as_num (str): AS number, convertible to an integer. Must be valid according to :py:meth:`valid_as() <generic_vm_objects.GenericRouter.valid_as>`. Raises: RuntimeError: If the ASN is not valid. """ifnotself.valid_as(as_num):raiseRuntimeError(f"The ASN={as_num} is NOT valid. Please see ""https://en.wikipedia.org/wiki/Autonomous_system_(Internet) "" for more details.")self.routing=getattr(self,"routing",{})if"bgp"notinself.routingornotself.routing["bgp"]:self.routing["bgp"]={}if("parameters"notinself.routing["bgp"]ornotself.routing["bgp"]["parameters"]):self.routing["bgp"]["parameters"]={}self.routing["bgp"]["parameters"]["router-as"]=as_num
[docs]defget_bgp_as(self):"""Retrieve the ASN used by BGP on this router. Returns: str: The AS number or a negative value if an error occurred. """try:returnself.routing["bgp"]["parameters"]["router-as"]exceptKeyError:self.log.error("Unable to get BGP AS number: unspecified value.")return-1exceptAttributeError:self.log.error("Unable to get BGP AS number: no routing structure.")return-2
[docs]defadd_bgp_network(self,network):"""Add a subnet to be advertised by BGP from this router. Args: network (str): The subnet to be advertised, this string can also be a :obj:`netaddr.IPNetwork`. The format of the string should be in the format of ``<Network IP>/<netmask>`` where ``netmask`` can either be CIDR or decimal dot notation. (e.g. ``"192.168.0.0/24"`` or ``"192.168.0.0/255.255.255.0"``) """network=netaddr.IPNetwork(network)self.routing=getattr(self,"routing",{})if"bgp"notinself.routingornotself.routing["bgp"]:self.routing["bgp"]={}if"networks"notinself.routing["bgp"]ornotself.routing["bgp"]["networks"]:self.routing["bgp"]["networks"]=[]self.routing["bgp"]["networks"].append(network)
[docs]defget_all_bgp_networks(self):"""Retrieve the list of subnets advertised by BGP on this router. Returns: list(netaddr.IPNetwork): List of the subnets advertised by BGP. """try:returnself.routing["bgp"]["networks"]exceptKeyError:return[]exceptAttributeError:return[]
[docs]deflink_bgp(self,neighbor,switch,neighbor_switch=None):"""Connect this router to another as BGP peers. ``switch`` and ``neighbor_switch`` are used to determine the IP address of the peers by identifying a single interface. This means they should be the first-hop for this router and neighbor, respectively, so the IP that is on the correct link is addressed in BGP. Args: neighbor (generic_vm_objects.GenericRouter): The :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` to configure as a BGP peer. switch (base_objects.Switch): First-hop switch for this router's connection to neighbor. neighbor_switch (base_objects.Switch, optional): First-hop switch for neighbor's connection to this router. Defaults to :py:data:`None`. Raises: ValueError: Parameters incorrectly typed. RuntimeError: Trying to establish a BGP connection without an ASN. RuntimeError: Unable to determine IP address for self or peer. Exception: Unable to get interfaces for BGP connection. """# If a neighbor switch was not passed in then use this vertex's switch# that was passed in for bothifnotneighbor_switch:neighbor_switch=switchifnotswitch.is_decorated_by(Switch):raiseValueError("switch must be a decorated by Switch.")ifnotneighbor_switch.is_decorated_by(Switch):raiseValueError("neighbor_switch must be a decorated by Switch.")ifnotneighbor.is_decorated_by(GenericRouter):raiseValueError("neighbor must be decorated by GenericRouter.")# Establish neighbor connections on each sideself.routing=getattr(self,"routing",{})if"bgp"notinself.routingornotself.routing["bgp"]:self.routing["bgp"]={}if("neighbors"notinself.routing["bgp"]ornotself.routing["bgp"]["neighbors"]):self.routing["bgp"]["neighbors"]=[]if("parameters"notinself.routing["bgp"]ornotself.routing["bgp"]["parameters"]):self.routing["bgp"]["parameters"]={}if("router-as"notinself.routing["bgp"]["parameters"]ornotself.routing["bgp"]["parameters"]["router-as"]):raiseRuntimeError(f"Trying to establish bgp connection without a set AS on {self.name}")neighbor.routing=getattr(neighbor,"routing",{})if"bgp"notinneighbor.routingornotneighbor.routing["bgp"]:neighbor.routing["bgp"]={}if("neighbors"notinneighbor.routing["bgp"]ornotneighbor.routing["bgp"]["neighbors"]):neighbor.routing["bgp"]["neighbors"]=[]if("parameters"notinneighbor.routing["bgp"]ornotneighbor.routing["bgp"]["parameters"]):neighbor.routing["bgp"]["parameters"]={}if("router-as"notinneighbor.routing["bgp"]["parameters"]ornotneighbor.routing["bgp"]["parameters"]["router-as"]):raiseRuntimeError("Trying to establish bgp connection without a set"f" AS on neighbor {neighbor.name}")v_as=self.routing["bgp"]["parameters"]["router-as"]n_as=neighbor.routing["bgp"]["parameters"]["router-as"]try:v_ip=Nonen_ip=Noneforiinself.interfaces.interfaces:ifi["switch"]==switch:v_ip=i["address"]breakforiinneighbor.interfaces.interfaces:ifi["switch"]==neighbor_switch:n_ip=i["address"]breakifnotv_ipornotn_ip:raiseRuntimeError(f"Unable to find IP address. Have {v_ip!s} for self "f"and {n_ip!s} for neighbor.")exceptException:self.log.exception("Unable to get interfaces for BGP connection")raiseself.routing["bgp"]["neighbors"].append({"remote-as":n_as,"address":n_ip})neighbor.routing["bgp"]["neighbors"].append({"remote-as":v_as,"address":v_ip})
[docs]defenable_dhcp_server(self,switch):"""Enable a DHCP server listening on the interface to the specified Switch. Args: switch (base_objects.Switch): The switch representing the network which will have DHCP. """forifaceinself.interfaces.interfaces:ififace["switch"]==switch:iface["dhcp"]=True