Skip to content

Make Networks Customizables #295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions examples/basic/A00_simple_as/simple_as.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from seedemu.layers import Base, Routing, Ebgp
from seedemu.services import WebService
from seedemu.compiler import Docker, Platform
from seedemu.core import Emulator, Binding, Filter
from seedemu.core import Emulator, Binding, Filter, OptionRegistry
import sys, os

def run(dumpfile = None):
Expand All @@ -29,7 +29,9 @@ def run(dumpfile = None):

# Initialize the emulator and layers
emu = Emulator()
base = Base()
# set global defaults..
base = Base(bandwidth=OptionRegistry().net_bandwidth(10000000),# 10 Mbit/s on all links
mtu=OptionRegistry().net_mtu(9000)) # use JumboFrames
routing = Routing()
ebgp = Ebgp()
web = WebService()
Expand All @@ -43,6 +45,7 @@ def run(dumpfile = None):

# Create an autonomous system
as150 = base.createAutonomousSystem(150)
as150.setOption(OptionRegistry().net_mtu(1500))

# Create a network
as150.createNetwork('net0')
Expand Down
6 changes: 3 additions & 3 deletions examples/scion/S02_scion_bgp_mixed/scion_bgp_mixed.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3

from seedemu.compiler import Docker, Graphviz
from seedemu.core import Emulator, OptionMode, Scope, ScopeTier, ScopeType, OptionRegistry
from seedemu.core import Emulator, OptionMode, NodeScope, NodeScopeTier, NodeScopeType, OptionRegistry
from seedemu.layers import (
ScionBase, ScionRouting, ScionIsd, Scion, Ospf, Ibgp, Ebgp, PeerRelationship,
SetupSpecification, CheckoutSpecification)
Expand Down Expand Up @@ -63,9 +63,9 @@
# override global default for AS150
as150.setOption(OptionRegistry().scion_loglevel('info', OptionMode.RUN_TIME))
as150.setOption(OptionRegistry().scion_disable_bfd(mode = OptionMode.RUN_TIME),
Scope(ScopeTier.AS,
NodeScope(NodeScopeTier.AS,
as_id=as150.getAsn(),
node_type=ScopeType.BRDNODE))
node_type=NodeScopeType.BRDNODE))

# override AS settings for individual nodes
as150_br0.setOption(OptionRegistry().scion_loglevel('debug', OptionMode.RUN_TIME))
Expand Down
40 changes: 20 additions & 20 deletions seedemu/compiler/Docker.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations
from seedemu.core.Emulator import Emulator
from seedemu.core import Node, Network, Compiler, BaseSystem, BaseOption, Scope, ScopeType, ScopeTier, OptionHandling, BaseVolume, OptionMode
from seedemu.core import Node, Network, Compiler, BaseSystem, BaseOption, NodeScope, NodeScope, NodeScopeType, NodeScopeTier, OptionHandling, BaseVolume, OptionMode
from seedemu.core.enums import NodeRole, NetworkType
from .DockerImage import DockerImage
from .DockerImageConstant import *
Expand Down Expand Up @@ -315,7 +315,7 @@ class Docker(Compiler):
__disable_images: bool
__image_per_node_list: Dict[Tuple[str, str], DockerImage]
_used_images: Set[str]
__config: List[ Tuple[BaseOption , Scope] ] # all encountered Options for .env file
__config: List[ Tuple[BaseOption , NodeScope] ] # all encountered Options for .env file
__option_handling: OptionHandling # strategy how to deal with Options
__basesystem_dockerimage_mapping: dict

Expand Down Expand Up @@ -1156,45 +1156,45 @@ def cmp_snd(a, b):
keyval_list = map(lambda x: f'- {x[0].name.upper()}=${{{ self._sndary_key(x[0],x[1])}}}', scopts )
return '\n '.join(keyval_list)

def _sndary_key(self, o: BaseOption, s: Scope ) -> str:
def _sndary_key(self, o: BaseOption, s: NodeScope ) -> str:
base = o.name.upper()
match s.tier:
case ScopeTier.Global:
case NodeScopeTier.Global:
match s.type:
case ScopeType.ANY:
case NodeScopeType.ANY:
return base
case ScopeType.BRDNODE:
case NodeScopeType.BRDNODE:
return f'{base}_BRDNODE'
case ScopeType.HNODE:
case NodeScopeType.HNODE:
return f'{base}_HNODE'
case ScopeType.CSNODE:
case NodeScopeType.CSNODE:
return f'{base}_CSNODE'
case ScopeType.RSNODE:
case NodeScopeType.RSNODE:
return f'{base}_RSNODE'
case ScopeType.RNODE:
case NodeScopeType.RNODE:
return f'{base}_RNODE'
case _:
#TODO: combination (ORed) Flags not yet implemented
raise NotImplementedError
case ScopeTier.AS:
case NodeScopeTier.AS:
match s.type:
case ScopeType.ANY:
case NodeScopeType.ANY:
return f'{base}_{s.asn}'
case ScopeType.BRDNODE:
case NodeScopeType.BRDNODE:
return f'{base}_{s.asn}_BRDNODE'
case ScopeType.HNODE:
case NodeScopeType.HNODE:
return f'{base}_{s.asn}_HNODE'
case ScopeType.CSNODE:
case NodeScopeType.CSNODE:
return f'{base}_{s.asn}_CSNODE'
case ScopeType.RSNODE:
case NodeScopeType.RSNODE:
return f'{base}_{s.asn}_RSNODE'
case ScopeType.RNODE:
case NodeScopeType.RNODE:
return f'{base}_{s.asn}_RNODE'
case _:
# combination (ORed) Flags not yet implemented
#TODO: How should we call CSNODE|HNODE or BRDNODE|RSNODE|RNODE ?!
raise NotImplementedError
case ScopeTier.Node:
case NodeScopeTier.Node:
return f'{base}_{s.asn}_{s.node.upper()}' # maybe add type here

def _compileNet(self, net: Network) -> str:
Expand All @@ -1219,7 +1219,7 @@ def _compileNet(self, net: Network) -> str:
labelList = self._getNetMeta(net)
)

def generateEnvFile(self, scope: Scope, dir_prefix: str = '/'):
def generateEnvFile(self, scope: NodeScope, dir_prefix: str = '/'):
"""!
@brief generates the '.env' file that accompanies any 'docker-compose.yml' file
@param scope filter ENV variables by scope (i.e. ASN).
Expand Down Expand Up @@ -1346,7 +1346,7 @@ def _doCompile(self, emulator: Emulator):
dummies = local_images + self._makeDummies()
), file=open('docker-compose.yml', 'w'))

self.generateEnvFile(Scope(ScopeTier.Global),'')
self.generateEnvFile(NodeScope(NodeScopeTier.Global),'')

def _computeComposeTopLvlVolumes(self) -> str:
"""!@brief render the 'volumes:' section of the docker-compose.yml file
Expand Down
17 changes: 13 additions & 4 deletions seedemu/core/AutonomousSystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from .AddressAssignmentConstraint import AddressAssignmentConstraint
from .enums import NetworkType, NodeRole
from .Node import Node, Router
from .Scope import ScopeTier, Scope
from .Scope import NodeScopeTier, NodeScope, NodeScope, NetScope, NetScopeTier
from .Option import OptionDomain
from .Emulator import Emulator
from .Configurable import Configurable
from .Customizable import Customizable
Expand Down Expand Up @@ -133,9 +134,17 @@ def inheritOptions(self, emulator: Emulator):
for n in all_nodes:
self.handDown(n)

def scope(self)-> Scope:
all_nets=[obj for(scope,typ,name),obj in reg.getAll().items() if scope==str(self.getAsn()) and typ=='net']
for n in all_nets:
self.handDown(n) # TODO: this also installs NodeOptions on the Net... which is no harm, but unnecessary

def scope(self, domain: OptionDomain = None)-> NodeScope:
"""return a scope specific to this AS"""
return Scope(ScopeTier.AS, as_id=self.getAsn())
match domain:
case OptionDomain.NODE:
return NodeScope(NodeScopeTier.AS, as_id=self.getAsn())
case OptionDomain.NET:
return NetScope(NetScopeTier.Scoped, scope_id=self.getAsn())


def configure(self, emulator: Emulator):
Expand Down Expand Up @@ -188,7 +197,7 @@ def createNetwork(self, name: str, prefix: str = "auto", direct: bool = True, aa

network = IPv4Network(prefix) if prefix != "auto" else self.__subnets.pop(0)
assert name not in self.__nets, 'Network with name {} already exist.'.format(name)
self.__nets[name] = Network(name, NetworkType.Local, network, aac, direct)
self.__nets[name] = Network(name, NetworkType.Local, network, aac, direct, scope=str(self.__asn))

return self.__nets[name]

Expand Down
4 changes: 2 additions & 2 deletions seedemu/core/Binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ipaddress import IPv4Network, IPv4Address
from sys import stderr
import re, random, string
from .Scope import Scope, ScopeTier
from .Scope import NodeScope, NodeScopeTier

class Action(Enum):
"""!
Expand Down Expand Up @@ -181,7 +181,7 @@ def __create(self, emulator: Emulator) -> Node:
# 'host' didnt exist back when Base::configure() installed
# the global default sysctl options on all nodes
for o in base.getAvailableOptions():
host.setOption(o, Scope(ScopeTier.Global))
host.setOption(o, NodeScope(NodeScopeTier.Global))

# set name servers
host.setNameServers(asObject.getNameServers())
Expand Down
4 changes: 2 additions & 2 deletions seedemu/core/Configurable.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def _prepare(self, emulator: Emulator):
Override this method in your Layer if you want more targeted
setting of Options i.e. only on border-routers or hosts etc..
"""
from .Scope import Scope, ScopeTier
from .Scope import NodeScope, NodeScopeTier

# set options on nodes directly
reg = emulator.getRegistry()
Expand All @@ -65,7 +65,7 @@ def _prepare(self, emulator: Emulator):
for o in self.getAvailableOptions():
assert o, 'implementation error'
# TODO: if o has __prefix attribute add prefix argument to setOption()
n.setOption(o, Scope(ScopeTier.Global))
n.setOption(o, NodeScope(NodeScopeTier.Global))


def configure(self, emulator: Emulator):
Expand Down
78 changes: 58 additions & 20 deletions seedemu/core/Customizable.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from functools import cmp_to_key
from typing import Optional, Dict, Tuple
from seedemu.core.Scope import *
from seedemu.core.Option import BaseOption, OptionMode
from seedemu.core.Option import BaseOption, OptionMode, OptionDomain

class Customizable(object):

Expand All @@ -16,17 +16,22 @@ def __init__(self): # scope param. actually only for debug/tests , scope: Scop
self._config = {}
self._scope = None

def scope(self)-> Scope:
"""!@brief returns a scope that includes only this very customizable instance ,nothing else"""
def scope(self, domain: OptionDomain = None)-> Scope:
"""!@brief returns a scope that includes only this very customizable instance ,nothing else
@param domain depending on what you need the scope object for
(i.e. for which kind of Option you want to specify the scope for)
you might need a different kind of scope
"""
# it's only natural for a customizable to know its place in the hierarchy
if not self._scope: return Scope(ScopeTier.Global) # maybe introduce a ScopeTier.NONE for this...
if not self._scope: return NodeScope(NodeScopeTier.Global) # maybe introduce a ScopeTier.NONE for this...
else: return self._scope


def getScopedOption(self, key: str, scope: Scope = None, prefix: str = None) -> Optional[Tuple[BaseOption, Scope]]:
"""! @brief retrieves an option along with the most specific Scope in which it was set.
"""
if not scope: scope = self.scope()
from seedemu.core.OptionRegistry import OptionRegistry
if not scope: scope = self.scope(domain=OptionRegistry().getDomain(key, prefix))

keys = [key]

Expand Down Expand Up @@ -76,15 +81,32 @@ def getOption(self, key: str, scope: Scope = None, prefix: str = None ) -> Optio
return None

def _possible_scopes(scope: Scope) -> List[Scope]:
possible_scopes = [
Scope(ScopeTier.Node, scope._node_type,
as_id=scope.asn, node_id=scope._node_id) if scope._node_id and scope._as_id and scope._node_type else None, # Node-specific + type
Scope(ScopeTier.Node,node_id=scope._node_id, as_id=scope._as_id) if scope._node_id and scope._as_id else None, # Node-specific
Scope(ScopeTier.AS, scope._node_type, as_id=scope._as_id) if scope._as_id and scope._node_type else None, # AS & Type
Scope(ScopeTier.AS, ScopeType.ANY, as_id=scope._as_id) if scope._as_id else None, # AS-wide
Scope(ScopeTier.Global, scope._node_type), # Global & Type
Scope(ScopeTier.Global) # Global (fallback)
]
if isinstance(scope, NodeScope):
possible_scopes = [
NodeScope(NodeScopeTier.Node, scope._node_type,
as_id=scope.asn, node_id=scope._node_id) if scope._node_id and scope._as_id and scope._node_type else None, # Node-specific + type
NodeScope(NodeScopeTier.Node,node_id=scope._node_id, as_id=scope._as_id) if scope._node_id and scope._as_id else None, # Node-specific
NodeScope(NodeScopeTier.AS, scope._node_type, as_id=scope._as_id) if scope._as_id and scope._node_type else None, # AS & Type
NodeScope(NodeScopeTier.AS, NodeScopeType.ANY, as_id=scope._as_id) if scope._as_id else None, # AS-wide
NodeScope(NodeScopeTier.Global, scope._node_type), # Global & Type
NodeScope(NodeScopeTier.Global) # Global (fallback)
]
if isinstance(scope, NetScope):
possible_scopes = [
NetScope(NetScopeTier.Individual,net_type=scope.type, scope_id=scope.scope, net_id=scope.net )
if scope.scope and scope.net and scope.type else None, # Net specific + type
NetScope(NetScopeTier.Individual, scope_id=scope.scope, net_id=scope.net )
if scope.scope and scope.net else None, # Net specific

NetScope(NetScopeTier.Scoped, scope.type, scope_id=scope.scope)
if scope.scope and scope.type else None, # scope & Type
NetScope(NetScopeTier.Scoped, NetScopeType.ANY, scope_id=scope.scope)
if scope.scope else None, # scope-wide

NetScope(NetScopeTier.Global, net_type=scope.type),
NetScope(NetScopeTier.Global)
]

return possible_scopes

def _getKeys(self) -> List[str]:
Expand All @@ -108,15 +130,29 @@ def handDown(self, child: 'Customizable'):
i.e. ASes are a collection of Nodes.
This methods performs the inheritance of options from parent to child.
"""

try: # scopes could be incomparable
assert self.scope()>child.scope(), 'logic error - cannot inherit options from more general scopes'
except :
pass
from .Network import Network
from .Node import Node
dom = OptionDomain.NET if type(child)== Network else OptionDomain.NODE
s1=self.scope(dom)
s2=child.scope(dom)


assert not s1 < s2, 'logic error - cannot inherit options from more general scopes'

for k, val in self._config.items():
for (op, s) in val:
child.setOption(op, s)
if self.valid_down(op,child):
child.setOption(op, s)

@staticmethod
def valid_down(op, child):
from .Network import Network
from .Node import Node
# enforce option-domains only at the lowest granularity (customizable hierarchy): Nodes and Networks
# Any aggregates of higher levels i.e. ASes, ISDs (that don't inherit neither Node nor Network)
# can have Options of mixed domains
return not ((op.optiondomain() == OptionDomain.NET and issubclass(type(child), Node)) or
(issubclass( type(child), Network) and op.optiondomain()==OptionDomain.NODE ) )

def setOption(self, op: BaseOption, scope: Scope = None ):
"""! @brief set option within the given scope.
Expand All @@ -126,6 +162,8 @@ def setOption(self, op: BaseOption, scope: Scope = None ):
# Everything else would be counterintuitive i.e. setting individual node overrides through the
# API of the AS , rather than the respective node's itself

assert self.valid_down(op, self), 'input error'

if not scope: scope = self.scope()

opname = op.name
Expand Down
2 changes: 1 addition & 1 deletion seedemu/core/Emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ def getServiceNetwork(self) -> Network:
@returns service network.
"""
if self.__service_net == None:
self.__service_net = self.__registry.register('seedemu', 'net', '000_svc', Network('000_svc', NetworkType.Bridge, IPv4Network(self.__service_net_prefix), direct = False))
self.__service_net = self.__registry.register('seedemu', 'net', '000_svc', Network('000_svc', NetworkType.Bridge, IPv4Network(self.__service_net_prefix), direct = False, scope='-1'))

return self.__service_net

Expand Down
2 changes: 1 addition & 1 deletion seedemu/core/InternetExchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __init__(self, id: int, prefix: str = "auto", aac: AddressAssignmentConstrai
network = IPv4Network(prefix) if prefix != "auto" else IPv4Network("10.{}.0.0/24".format(self.__id))

self.__name = 'ix{}'.format(str(self.__id))
self.__net = Network(self.__name, NetworkType.InternetExchange, network, aac, False)
self.__net = Network(self.__name, NetworkType.InternetExchange, network, aac, False, scope=str(id))

if create_rs:
self.__rs = Router(self.__name, NodeRole.RouteServer, self.__id)
Expand Down
Loading