Source code for generic_vm_objects

"""
This module contains MC objects representing generic capability interfaces
that may be realized by multiple concrete/image MC objects.

A good example of this is routers. Different types of routers may realize BGP
and OSPF connections, so the GenericRouter object is located here.
"""

import netaddr
from base_objects import Switch, VMEndpoint

from firewheel.control.experiment_graph import require_class


[docs] @require_class(VMEndpoint) class GenericRouter: """ 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 type self.type = "router" # A name must be specified when using a GenericRouter. self.name = getattr(self, "name", None) if self.name is None: raise NameError("Name must be specified for router!") # Overwrite the Vertex name if one is provided if name: self.name = name # Sanity check that this Vertex attribute has not been created. self.routing = getattr(self, "routing", {})
[docs] def redistribute_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": {}} except AttributeError: self.routing = {"ospf": {"redistribution": {"bgp": {}}}} except KeyError: if "ospf" not in self.routing: self.routing["ospf"] = {} if "redistribution" not in self.routing["ospf"]: self.routing["ospf"]["redistribution"] = {} self.routing["ospf"]["redistribution"]["bgp"] = {} except Exception: self.log.exception( "Cannot redistribute OSPF into BGP (%s)", self.name, ) raise
[docs] def redistribute_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": {}} except AttributeError: self.routing = {"bgp": {"redistribution": {"ospf": {}}}} except KeyError: if "bgp" not in self.routing: self.routing["bgp"] = {} if "redistribution" not in self.routing["bgp"]: self.routing["bgp"]["redistribution"] = {} self.routing["bgp"]["redistribution"]["ospf"] = {} except Exception: self.log.exception( "Cannot redistribute BGP into OSPF (%s)", self.name, ) raise
[docs] def redistribute_ospf_connected(self): """ Redistribute routes for directly connected subnets to OSPF peers. """ try: if "ospf" not in self.routing: self.log.warning( "OSPF not yet defined for: %s. Creating it now.", self.name, ) self.routing["ospf"] = {} except AttributeError: self.log.exception( "No routing block defined for: %s. Must specify routing protocols " "before redistribution", self.name, ) return if "redistribution" not in self.routing["ospf"]: self.routing["ospf"]["redistribution"] = {} self.routing["ospf"]["redistribution"]["connected"] = {}
[docs] def ospf_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" not in self.routing or not self.routing["parameters"]: self.routing["parameters"] = {} if "ospf" not in self.routing or not self.routing["ospf"]: self.routing["ospf"] = {} if ( "interfaces" not in self.routing["ospf"] or not self.routing["ospf"]["interfaces"] ): self.routing["ospf"]["interfaces"] = {} if "areas" not in self.routing["ospf"] or not self.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 information self.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" not in self.routing or "router-id" not in self.routing["parameters"] ): self.routing["parameters"]["router-id"] = str(net.ip)
[docs] def valid_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) if 0 < num < 4294967295: return True except TypeError: self.log.error("The AS number %s is not valid", as_num) except ValueError: self.log.error("The AS number %s is not valid", as_num) return False
[docs] def set_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. """ if not self.valid_as(as_num): raise RuntimeError( 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" not in self.routing or not self.routing["bgp"]: self.routing["bgp"] = {} if ( "parameters" not in self.routing["bgp"] or not self.routing["bgp"]["parameters"] ): self.routing["bgp"]["parameters"] = {} self.routing["bgp"]["parameters"]["router-as"] = as_num
[docs] def get_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: return self.routing["bgp"]["parameters"]["router-as"] except KeyError: self.log.error("Unable to get BGP AS number: unspecified value.") return -1 except AttributeError: self.log.error("Unable to get BGP AS number: no routing structure.") return -2
[docs] def add_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" not in self.routing or not self.routing["bgp"]: self.routing["bgp"] = {} if "networks" not in self.routing["bgp"] or not self.routing["bgp"]["networks"]: self.routing["bgp"]["networks"] = [] self.routing["bgp"]["networks"].append(network)
[docs] def get_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: return self.routing["bgp"]["networks"] except KeyError: return [] except AttributeError: return []
[docs] def enable_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. """ for iface in self.interfaces.interfaces: if iface["switch"] == switch: iface["dhcp"] = True