diff --git a/examples/basic/A00_simple_as/simple_as.py b/examples/basic/A00_simple_as/simple_as.py index 8f937ee76..5a9d5b39c 100755 --- a/examples/basic/A00_simple_as/simple_as.py +++ b/examples/basic/A00_simple_as/simple_as.py @@ -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): @@ -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() @@ -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') diff --git a/examples/scion/S02_scion_bgp_mixed/scion_bgp_mixed.py b/examples/scion/S02_scion_bgp_mixed/scion_bgp_mixed.py index bfe8489c7..7f2180a0d 100755 --- a/examples/scion/S02_scion_bgp_mixed/scion_bgp_mixed.py +++ b/examples/scion/S02_scion_bgp_mixed/scion_bgp_mixed.py @@ -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) @@ -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)) diff --git a/seedemu/compiler/Docker.py b/seedemu/compiler/Docker.py index 2b06eb8b6..710f39c96 100644 --- a/seedemu/compiler/Docker.py +++ b/seedemu/compiler/Docker.py @@ -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 * @@ -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 @@ -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: @@ -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). @@ -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 diff --git a/seedemu/core/AutonomousSystem.py b/seedemu/core/AutonomousSystem.py index f84101904..8fee192d1 100644 --- a/seedemu/core/AutonomousSystem.py +++ b/seedemu/core/AutonomousSystem.py @@ -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 @@ -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): @@ -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] diff --git a/seedemu/core/Binding.py b/seedemu/core/Binding.py index b349fecd4..e7f02ba3a 100644 --- a/seedemu/core/Binding.py +++ b/seedemu/core/Binding.py @@ -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): """! @@ -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()) diff --git a/seedemu/core/Configurable.py b/seedemu/core/Configurable.py index 928fa5450..dd0688ce6 100644 --- a/seedemu/core/Configurable.py +++ b/seedemu/core/Configurable.py @@ -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() @@ -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): diff --git a/seedemu/core/Customizable.py b/seedemu/core/Customizable.py index cd1ccc32e..43c310a20 100644 --- a/seedemu/core/Customizable.py +++ b/seedemu/core/Customizable.py @@ -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): @@ -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] @@ -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]: @@ -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. @@ -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 diff --git a/seedemu/core/Emulator.py b/seedemu/core/Emulator.py index 8c991e2c8..1f1ed45a0 100644 --- a/seedemu/core/Emulator.py +++ b/seedemu/core/Emulator.py @@ -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 diff --git a/seedemu/core/InternetExchange.py b/seedemu/core/InternetExchange.py index a50ba1be7..307659eee 100644 --- a/seedemu/core/InternetExchange.py +++ b/seedemu/core/InternetExchange.py @@ -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) diff --git a/seedemu/core/Network.py b/seedemu/core/Network.py index 906377082..5193f20a3 100644 --- a/seedemu/core/Network.py +++ b/seedemu/core/Network.py @@ -7,9 +7,12 @@ from .Registry import Registrable from .AddressAssignmentConstraint import AddressAssignmentConstraint, Assigner from .Visualization import Vertex +from .Customizable import Customizable +from .Scope import NodeScope,NetScope, NetScopeTier, NetScopeType from typing import Dict, Tuple, List +from .OptionUtil import OptionDomain -class Network(Printable, Registrable, Vertex): +class Network(Printable, Registrable, Vertex, Customizable): """! @brief The network class. @@ -24,19 +27,13 @@ class Network(Printable, Registrable, Vertex): __connected_nodes: List['Node'] - __d_latency: int # in ms - __d_bandwidth: int # in bps - __d_drop: float # percentage - - __mtu: int - __direct: bool # these two should be aggregated into a single instance of a common base class ?! __rap: RemoteAccessProvider __ecp: ExternalConnectivityProvider - def __init__(self, name: str, type: NetworkType, prefix: IPv4Network, aac: AddressAssignmentConstraint = None, direct: bool = False): + def __init__(self, name: str, type: NetworkType, prefix: IPv4Network, aac: AddressAssignmentConstraint = None, direct: bool = False, scope: str = None): """! @brief Network constructor. @@ -56,6 +53,7 @@ def __init__(self, name: str, type: NetworkType, prefix: IPv4Network, aac: Addre self.__prefix = prefix self.__aac = aac if aac != None else AddressAssignmentConstraint() self.__assigners = {} + self.__scope = scope self.__connected_nodes = [] @@ -66,17 +64,27 @@ def __init__(self, name: str, type: NetworkType, prefix: IPv4Network, aac: Addre self.__assigners[ NodeRole.Host ] = ahost self.__assigners[ NodeRole.ControlService ] = ahost - self.__d_latency = 0 - self.__d_bandwidth = 0 - self.__d_drop = 0 - - self.__mtu = 1500 - self.__direct = direct self.__rap = None self.__ecp = None + def scope(self, domain: OptionDomain = None)-> NodeScope: + """return a Scope that is specific to this Network""" + + assert domain in [OptionDomain.NET, None], 'input error' + match (nt:=NetScopeType.from_net(self)): + case NetScopeType.XC: + return NetScope(tier=NetScopeTier.Individual, + net_type=nt, + scope_id=0, # scope of XC nets is None otherwise + net_id=self.getName()) + case _: + return NetScope(tier=NetScopeTier.Individual, + net_type=nt, + scope_id=int(self.__scope), + net_id=self.getName()) + def isDirect(self) -> bool: """! @brief test if this network is direct network. A direct network will be @@ -109,7 +117,8 @@ def setMtu(self, mtu: int) -> Network: @returns self, for chaining API calls. """ - self.__mtu = mtu + from .OptionRegistry import OptionRegistry + self.setOption( OptionRegistry().net_mtu(mtu) ) return self @@ -119,7 +128,7 @@ def getMtu(self) -> int: @returns mtu. """ - return self.__mtu + return self.getOption('mtu', prefix='net').value def setDefaultLinkProperties(self, latency: int = 0, bandwidth: int = 0, packetDrop: float = 0) -> Network: """! @@ -135,11 +144,13 @@ def setDefaultLinkProperties(self, latency: int = 0, bandwidth: int = 0, packetD assert latency >= 0, 'invalid latency' assert bandwidth >= 0, 'invalid bandwidth' assert packetDrop >= 0 and packetDrop <= 100, 'invalid packet drop' - - self.__d_latency = latency - self.__d_bandwidth = bandwidth - self.__d_drop = packetDrop - + from .OptionRegistry import OptionRegistry + if latency > 0: + self.setOption(OptionRegistry().net_latency(latency)) + if bandwidth > 0: + self.setOption(OptionRegistry().net_bandwidth(bandwidth)) + if packetDrop > 0: + self.setOption(OptionRegistry().net_packetloss(packetDrop)) return self def setType(self, newType: NetworkType) -> Network: @@ -161,7 +172,9 @@ def getDefaultLinkProperties(self) -> Tuple[int, int, float]: @returns tuple (latency, bandwidth, packet drop) """ - return (self.__d_latency, self.__d_bandwidth, self.__d_drop) + return (self.getOption('latency', prefix='net').value, + self.getOption('bandwidth', prefix='net').value, + self.getOption('packetloss', prefix='net').value) def getName(self) -> str: """! diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index 59b32b8b9..b7900873f 100644 --- a/seedemu/core/Node.py +++ b/seedemu/core/Node.py @@ -8,6 +8,7 @@ from .Registry import Registrable from .Emulator import Emulator from .Customizable import Customizable +from .OptionUtil import OptionDomain from .Volume import BaseVolume from .Configurable import Configurable from .enums import NetworkType @@ -120,15 +121,18 @@ def __init__(self, net: Network): """ self.__address = None self.__network = net - (l, b, d) = net.getDefaultLinkProperties() - self.__latency = l - self.__bandwidth = b - self.__drop = d + + #(l, b, d) = net.getDefaultLinkProperties() + self.__latency = 0 + self.__bandwidth = 0 + self.__drop = 0 + def setLinkProperties(self, latency: int = 0, bandwidth: int = 0, packetDrop: float = 0) -> Interface: """! @brief Set link properties. + @note if not overriden the default link properties of the network are used @param latency (optional) latency to add to the link in ms, default 0. @param bandwidth (optional) egress bandwidth of the link in bps, 0 for unlimited, default 0. @param packetDrop (optional) link packet drop as percentage, 0 for unlimited, default 0. @@ -152,7 +156,11 @@ def getLinkProperties(self) -> Tuple[int, int, int]: @returns tuple (latency, bandwidth, packet drop) """ - return (self.__latency, self.__bandwidth, self.__drop) + #return (self.__latency, self.__bandwidth, self.__drop) + (l, b, d) = self.__network.getDefaultLinkProperties() + return (self.__latency if self.__latency>0 else l, + self.__bandwidth if self.__bandwidth>0 else b, + self.__drop if self.__drop>0 else d,) def getNet(self) -> Network: """! @@ -284,9 +292,10 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): self.__note = None - def scope(self)-> Scope: - return Scope(ScopeTier.Node, - node_type=ScopeType.from_node(self), + def scope(self, domain: OptionDomain = None)-> NodeScope: + assert domain in [OptionDomain.NODE, None], 'input error' + return NodeScope(NodeScopeTier.Node, + node_type=NodeScopeType.from_node(self), node_id=self.getName(), as_id=self.getAsn()) @@ -353,9 +362,19 @@ def configure(self, emulator: Emulator): else: # netname = 'as{}.{}_as{}.{}'.format(self.getAsn(), self.getName(), peerasn, peername) netname = ''.join(choice(ascii_letters) for i in range(10)) - net = Network(netname, NetworkType.CrossConnect, localaddr.network, direct = False) # TODO: XC nets w/ direct flag? + # TOODO scope of XC nets ?! pair of both ASes .. ?! + net = Network(netname, NetworkType.CrossConnect, localaddr.network, direct = False, scope='0') # TODO: XC nets w/ direct flag? net.setDefaultLinkProperties(latency, bandwidth, packetDrop).setMtu(mtu) # Set link properties - self.__joinNetwork(reg.register('xc', 'net', netname, net), str(localaddr.ip)) + obj = reg.register('xc', 'net', netname, net) + # pass any NetworkOptions down to the new XcNet + base = emulator.getLayer('Base') + parent_as = base.getAutonomousSystem(self.getAsn()) + parent_as.handDown(obj) + for o in base.getNetOptions(): + obj.setOption(o, NetScope(NetScopeTier.Global)) + + + self.__joinNetwork(obj, str(localaddr.ip)) self.__xcs[(peername, peerasn)] = (localaddr, netname, (latency, bandwidth, packetDrop, mtu)) if issubclass(self.__class__, Router): diff --git a/seedemu/core/Option.py b/seedemu/core/Option.py index ebf92a95a..b551cf8a0 100644 --- a/seedemu/core/Option.py +++ b/seedemu/core/Option.py @@ -1,6 +1,5 @@ -from enum import Flag, auto from typing import List, Optional, Type, Any - +from .OptionUtil import * class AutoRegister(): @@ -74,17 +73,6 @@ def components_recursive(cls, prefix: str = None) -> Optional[List['BaseComponen opts.extend(c.components_recursive(prefix = f'{cls.getName()}_{c.getName()}')) return opts -class OptionMode(Flag): - """!@brief characteristics of an option, - during which time it might be changed or set - """ - # static/hardcoded (require re-compile + image-rebuild to change) - BUILD_TIME = auto() - # i.e. envsubst (require only docker compose stop/start ) - RUN_TIME = auto() - - - class OptionGroupMeta(type): # or BaseComponentMeta .. """Metaclass to auto-register nested options within a group.""" @@ -94,6 +82,8 @@ def __new__(cls, name, bases, class_dict): name = 'scion' if name.lower() == 'sysctlopts': name = 'sysctl' + if name.lower() == 'netopts': + name = 'net' from .OptionRegistry import OptionRegistry @@ -110,6 +100,7 @@ def __new__(cls, name, bases, class_dict): # prefixed_name = f"{name}_{attr_value.name()}" # better call new_cls.add() # here new_cls._children[attr_value.name] = attr_value + attr_value.domain = new_cls.optiondomain() # don't register nested options twice (but only once as child of the parent 'composite') if '.' not in qname or qname.startswith('SEEDEmuOptionSystemTestCase'): OptionRegistry().register(new_cls) @@ -119,6 +110,17 @@ def __new__(cls, name, bases, class_dict): class BaseOption(BaseComponent, metaclass=OptionGroupMeta): """! a base class for KEY-VALUE pairs representing Settings, Parameters or Feature Flags""" + domain: OptionDomain = None # set by the parent option container + + @property + def domain(self) -> OptionDomain: + # if domain not specified by parent container, + # user must implement optiondomain() + if (d:= self.__class__.domain) != None: + return d + else: + return self.optiondomain() + def __eq__(self, other): if not other: return False @@ -137,10 +139,15 @@ def value(self, new_value: str): """Should allow setting a new value.""" pass + @classmethod + def optiondomain(cls) -> OptionDomain: + return cls.domain + @property def mode(self)->OptionMode: """Should return the mode of the option.""" pass + @mode.setter def mode(self, new_mode: OptionMode): pass @@ -159,6 +166,7 @@ class Option(BaseOption): """ # Immutable class variable to be defined in subclasses value_type: Type[Any] + def __init__(self, value: Optional[Any] = None, mode: OptionMode = None): cls = self.__class__ @@ -227,6 +235,8 @@ def value(self, new_value: Any): assert new_value != None, 'Logic Error - option value cannot be None!' self._mutable_value = new_value + + @property def mode(self): if (mode := self._mutable_mode) != None: @@ -244,6 +254,12 @@ def description(cls) -> str: and its allowed values """ return cls.__doc__ or "No documentation available." + + @classmethod + def domain(cls) -> OptionDomain: + """ the types of entities that a given Option + can apply or refer to""" + return OptionDomain.NODE #class ScopedOption: @@ -255,6 +271,7 @@ def description(cls) -> str: class BaseOptionGroup(BaseComponent , metaclass=OptionGroupMeta): _children = {} + domain: OptionDomain = None def describe(self) -> str: @@ -273,4 +290,8 @@ def get(self, option_name: str) -> Optional[BaseComponent]: @classmethod def components(cls): - return [v for _, v in cls._children.items()] \ No newline at end of file + return [v for _, v in cls._children.items()] + + @classmethod + def optiondomain(cls) -> OptionDomain: + return cls.domain \ No newline at end of file diff --git a/seedemu/core/OptionRegistry.py b/seedemu/core/OptionRegistry.py index 64af499f2..20a818ef8 100644 --- a/seedemu/core/OptionRegistry.py +++ b/seedemu/core/OptionRegistry.py @@ -1,4 +1,4 @@ -from typing import Dict, Type +from typing import Dict, Type, Optional class SingletonMeta(type): @@ -51,6 +51,14 @@ def create_option(cls, name: str, *args, **kwargs) -> 'Option': # Instantiate with given arguments return option_cls(*args[1:], **kwargs) + @classmethod + def getDomain(cls, key: str, prefix: str = None) -> Optional['OptionDomain']: + o = cls.getType(key, prefix) + if o: + return o.optiondomain() + else: + return None + @classmethod def getType(cls, name: str, prefix: str = None) -> Type['BaseComponent']: diff --git a/seedemu/core/OptionUtil.py b/seedemu/core/OptionUtil.py new file mode 100644 index 000000000..11e0818a0 --- /dev/null +++ b/seedemu/core/OptionUtil.py @@ -0,0 +1,26 @@ +from enum import IntEnum +from enum import Flag, auto + +class OptionDomain(IntEnum): + """!@ defines the types of entities that a given Option + can apply or refer to + Attempting to set an option on an entity to which it does not apply is meaningless. + i.e. Latency or MTU can only pertain to Networks, not Nodes + """ + NODE = 0 + NET = 4 + ''' these lastly exist only during build time really, + and are only aggregates of either NODES or NETS depending on the context + AS = 1 + ISD = 2 + IX = 3 + ''' + +class OptionMode(Flag): + """!@brief characteristics of an option, + during which time it might be changed or set + """ + # static/hardcoded (require re-compile + image-rebuild to change) + BUILD_TIME = auto() + # i.e. envsubst (require only docker compose stop/start ) + RUN_TIME = auto() \ No newline at end of file diff --git a/seedemu/core/ScionAutonomousSystem.py b/seedemu/core/ScionAutonomousSystem.py index bb5d81009..76161ad59 100644 --- a/seedemu/core/ScionAutonomousSystem.py +++ b/seedemu/core/ScionAutonomousSystem.py @@ -8,7 +8,8 @@ from .Emulator import Emulator from .enums import NodeRole from .Node import Node -from .Scope import Scope, ScopeTier +from .Scope import NodeScope, NodeScopeTier +from .OptionUtil import OptionDomain class ScionASN: """! @@ -158,8 +159,8 @@ def __init__(self, asn: int, subnetTemplate: str = "10.{}.0.0/16"): self.__note = None self.__generateStaticInfoConfig = False - def scope(self)-> Scope: - return Scope(ScopeTier.AS, as_id=self.getAsn()) + def scope(self, domain: OptionDomain = None)-> NodeScope: + return super().scope(domain) def createRealWorldRouter(self, name: str, hideHops: bool = True, prefixes: List[str] = None) -> Node: """! diff --git a/seedemu/core/Scope.py b/seedemu/core/Scope.py index a69565b97..d54c8d4d1 100644 --- a/seedemu/core/Scope.py +++ b/seedemu/core/Scope.py @@ -1,6 +1,7 @@ from enum import Enum, IntEnum -from typing import Tuple - +from typing import Tuple, Optional +from abc import ABC, abstractmethod +from .OptionUtil import OptionDomain # could be replaced by @total_order class ComparableEnum(Enum): @@ -25,6 +26,9 @@ def __ge__(self, other): return NotImplemented class ScopeTier(ComparableEnum): + pass + +class NodeScopeTier(ScopeTier): """! @brief the domain or extent(kind) of a scope """ @@ -36,8 +40,15 @@ class ScopeTier(ComparableEnum): #"individual node setting(per container)" Node=1 -#TODO: we could just use NodeRole itself, but SEED folks are afraid of touching it, so we don't .. + + class ScopeType(IntEnum): + pass + + + +#TODO: we could just use NodeRole itself, but SEED folks are afraid of touching it, so we don't .. +class NodeScopeType(ScopeType): """Defines the type of entity affected by the scope.""" NONE = 0 # only for checks that intersection is empty ANY = 15 # No specific type -> matches everything @@ -52,21 +63,72 @@ def from_node(node: 'Node'): match node.getRole(): case NodeRole.Host: - return ScopeType.HNODE + return NodeScopeType.HNODE case NodeRole.Router: - return ScopeType.RNODE + return NodeScopeType.RNODE case NodeRole.BorderRouter: - return ScopeType.BRDNODE + return NodeScopeType.BRDNODE case NodeRole.ControlService: - return ScopeType.CSNODE + return NodeScopeType.CSNODE case NodeRole.RouteServer: - return ScopeType.RSNODE + return NodeScopeType.RSNODE +class Scope(ABC): + @abstractmethod + def domain(self) -> OptionDomain: + pass + + @property + @abstractmethod + def tier(self) -> ScopeTier: + pass + @property + @abstractmethod + def type(self)-> ScopeType: + pass -class Scope: + def __eq__(self, other: 'Scope') -> bool: + """Compares two Scope objects for equality.""" + assert isinstance(other, Scope) + _, eq2= self._comparable(other) + return eq2 + + @abstractmethod + def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: + pass + + @abstractmethod + def __lt__(self, other: 'Scope'): + pass + @abstractmethod + def __gt__(self, other: 'Scope'): + pass + + @abstractmethod + def _intersection(self, other: 'Scope') -> 'Scope': + pass + # for use with functools.cmp_to_key( Scope.collate ) + @staticmethod + def collate(a: 'Scope', b: 'Scope') -> int: + #c,_= a._comparable(b) + try: + if a < b: + return -1 + elif b < a: + return 1 + except TypeError: # This happens if Python encounters NotImplemented in both cases + pass + return 0 # Fallback: Treat as equal or use another sorting logic + + +# TODO create EmuScope (EmulationScope) that is domain agnostic and can be compared with Node&NetScope +# for DockerCompiler::generateEnvFile() +# It would do the comparison solely on 'scope' (that is ASN) and ignore type + +class NodeScope(Scope): """! @brief strong type for the hierarchical scope of configuration settings. i.e. ''(global/simulation wide), '150' (AS level) or '150_brdnode_br0' (individual node override) @@ -74,6 +136,8 @@ class Scope: Scopes implement <> comparison to readily determine when options are overriden by more specific scopes. However they do not form a total order (i.e. ScopeTypes like rnode, hnode within the same AS or Globally cannot be compared ) Also Scope does not cater for multihomed ASes where nodes can be in more than one AS at the same time. + @details A NodeScope object can define or specify the scope (or extension) of a NodeOption. + This scope can encompass the following registry types/items: 'hnode', 'rnode', 'csnode', 'brdnode', 'rsnode' """ # NOTE ISD scope could be added here @@ -82,8 +146,8 @@ class Scope: # But this complicates the code ... It would in fact be easier to disaggregate Type into an ordenary Enum, # at the cost of having to set an Option multiple times, once for each NodeType that is to be included def __init__(self, - tier: ScopeTier, - node_type: ScopeType = ScopeType.ANY, + tier: NodeScopeTier, + node_type: NodeScopeType = NodeScopeType.ANY, node_id: str = None, as_id: int = None): ''' @@ -93,13 +157,13 @@ def __init__(self, Since an AS can be part of several ISDs, picking a globally unique AS number also facilitates joining new ISDs.[TheCompleteGuideToSCION] ''' - if tier==ScopeTier.AS: + if tier==NodeScopeTier.AS: assert as_id!=None, 'Invalid Input' assert node_id==None, 'Invalid Input' - if tier==ScopeTier.Global: + if tier==NodeScopeTier.Global: assert node_id==None assert as_id==None - if tier==ScopeTier.Node: + if tier==NodeScopeTier.Node: assert node_id!=None assert as_id!=None @@ -112,32 +176,38 @@ def __hash__(self): """Allows Scope instances to be used as dictionary keys.""" return hash((self.tier, self.node_type, self.node_id, self.as_id)) ''' + + def domain(self) -> OptionDomain: + return OptionDomain.NODE + @property - def tier(self) -> ScopeTier: + def tier(self) -> NodeScopeTier: return self._tier @property - def type(self)-> ScopeType: + def type(self)-> NodeScopeType: return self._node_type @property - def node(self) -> str: + def node(self) -> Optional[str]: return self._node_id @property - def asn(self) -> int: + def asn(self) -> Optional[int]: return self._as_id - def _intersection(self, other: 'Scope') -> 'Scope': + def _intersection(self, other: 'NodeScope') -> 'NodeScope': """! @brief return a new scope which represents the intersection of both scopes """ pass - def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: + def _comparable(self, other: 'NodeScope') -> Tuple[bool,bool]: """ returns a two bools indicating wheter the scopes are: lt/gt comparable or identical""" + if not isinstance(other, NodeScope): return (False, False) + same_type = True if self.type == other.type else False common_type = self.type & other.type otherTypeInSelf = ( (self.type & other.type) == other.type ) @@ -149,13 +219,13 @@ def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: if self.tier==other.tier: match self.tier: - case ScopeTier.Global: # asn, nodeID irrelevant + case NodeScopeTier.Global: # asn, nodeID irrelevant if same_type: return False, True else: return False, False - case ScopeTier.AS: # nodeID irrelevant + case NodeScopeTier.AS: # nodeID irrelevant if same_asn: if same_type: return False, True @@ -164,7 +234,7 @@ def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: else: return False, False - case ScopeTier.Node: # type should be irrelevant (i.e. redundant ) here + case NodeScopeTier.Node: # type should be irrelevant (i.e. redundant ) here if same_asn: if same_node: return False,True @@ -174,10 +244,10 @@ def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: return False, False else: match self.tier: - case ScopeTier.Global: + case NodeScopeTier.Global: # other.tier must be AS or Node match other.tier: - case ScopeTier.AS: + case NodeScopeTier.AS: if same_type: # other AS scope is subset of self return True, False @@ -187,7 +257,7 @@ def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: else: # types conflict and prevent inclusion return False, False - case ScopeTier.Node: + case NodeScopeTier.Node: if same_type: return True, False elif otherTypeInSelf: @@ -195,10 +265,10 @@ def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: else: return False, False - case ScopeTier.AS: + case NodeScopeTier.AS: match other.tier: - case ScopeTier.Global: + case NodeScopeTier.Global: if same_type: # self is subset of other return True, False elif selfTypeInOther: @@ -206,7 +276,7 @@ def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: else: # both scopes make statements about different types of scopes return False, False - case ScopeTier.Node: + case NodeScopeTier.Node: if same_asn: if same_type: return True, False @@ -217,10 +287,10 @@ def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: else: return False, False - case ScopeTier.Node: + case NodeScopeTier.Node: match other.tier: - case ScopeTier.AS: + case NodeScopeTier.AS: if same_asn: if same_type: return True, False @@ -231,7 +301,7 @@ def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: else: return False, False - case ScopeTier.Global: + case NodeScopeTier.Global: if same_type: return True, False elif selfTypeInOther: @@ -240,19 +310,17 @@ def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: return False, False - def __eq__(self, other): - """Compares two Scope objects for equality.""" - assert isinstance(other, Scope) - _, eq2= self._comparable(other) - return eq2 - def __gt__(self, other): + + def __gt__(self, other: 'NodeScope'): """!@brief defines scope hierarchy comparison i.e. broader more general(less specific) > more specific). i.e. LHS supserset RHS """ - if not isinstance(other, Scope): + if not isinstance(other, NodeScope): return NotImplemented + # TODO what is correct ?! NotImpl or False + #if not isinstance(other, NodeScope): return False same_type = True if self.type == other.type else False common_type = self.type & other.type @@ -265,7 +333,7 @@ def __gt__(self, other): if self.tier==other.tier: match self.tier: - case ScopeTier.Global: # asn, nodeID irrelevant + case NodeScopeTier.Global: # asn, nodeID irrelevant if same_type: return False # they are equal not gt elif otherTypeInSelf: @@ -273,7 +341,7 @@ def __gt__(self, other): else: return NotImplemented - case ScopeTier.AS: # nodeID irrelevant + case NodeScopeTier.AS: # nodeID irrelevant if same_asn: if same_type: return False # they are equal not gt @@ -285,7 +353,7 @@ def __gt__(self, other): # scopes of different ASes are disjoint return NotImplemented - case ScopeTier.Node: # type should be irrelevant (i.e. redundant ) here + case NodeScopeTier.Node: # type should be irrelevant (i.e. redundant ) here if same_asn: if same_node: return False # equal and not gt @@ -295,10 +363,10 @@ def __gt__(self, other): return NotImplemented else: match self.tier: - case ScopeTier.Global: + case NodeScopeTier.Global: # other.tier must be AS or Node match other.tier: - case ScopeTier.AS: + case NodeScopeTier.AS: if same_type: # other AS scope is subset of self (gt) return True @@ -307,7 +375,7 @@ def __gt__(self, other): return True else: return False - case ScopeTier.Node: + case NodeScopeTier.Node: if same_type: # other is subset of self (gt) return True @@ -317,13 +385,13 @@ def __gt__(self, other): else: return False - case ScopeTier.AS: + case NodeScopeTier.AS: match other.tier: - case ScopeTier.Global: + case NodeScopeTier.Global: return False - case ScopeTier.Node: + case NodeScopeTier.Node: if same_asn: if same_type: return True @@ -334,14 +402,14 @@ def __gt__(self, other): else: return False - case ScopeTier.Node: + case NodeScopeTier.Node: match other.tier: - case ScopeTier.AS: + case NodeScopeTier.AS: return False - case ScopeTier.Global: + case NodeScopeTier.Global: return False @@ -357,11 +425,11 @@ def lt_old(self, other): return bool(self._node_id) and not bool(other._node_id) # Node scope is most specific return False - def __lt__(self, other): + def __lt__(self, other: 'NodeScope'): """!@brie fDefines scope hierarchy comparison (more specific < broader). i.e. LHS subset RHS """ - if not isinstance(other, Scope): + if not isinstance(other, NodeScope): return NotImplemented same_type = True if self.type == other.type else False @@ -375,7 +443,7 @@ def __lt__(self, other): if self.tier==other.tier: match self.tier: - case ScopeTier.Global: # asn, nodeID irrelevant + case NodeScopeTier.Global: # asn, nodeID irrelevant if same_type: return False # they are equal not lt elif selfTypeInOther: @@ -383,7 +451,7 @@ def __lt__(self, other): else: return NotImplemented - case ScopeTier.AS: # nodeID irrelevant + case NodeScopeTier.AS: # nodeID irrelevant if same_asn: if same_type: return False # they are equal not lt @@ -395,7 +463,7 @@ def __lt__(self, other): # scopes of different ASes are disjoint return NotImplemented - case ScopeTier.Node: # type should be irrelevant (i.e. redundant ) here + case NodeScopeTier.Node: # type should be irrelevant (i.e. redundant ) here if same_asn: if same_node: return False # equal and not lt @@ -405,10 +473,10 @@ def __lt__(self, other): return NotImplemented else: match self.tier: - case ScopeTier.Global: + case NodeScopeTier.Global: # other.tier must be AS or Node match other.tier: - case ScopeTier.AS: + case NodeScopeTier.AS: if same_type: # other AS scope is subset of self (gt) return False @@ -417,7 +485,7 @@ def __lt__(self, other): return False else: return False - case ScopeTier.Node: + case NodeScopeTier.Node: if same_type: # other is subset of self (gt) return False @@ -427,10 +495,10 @@ def __lt__(self, other): else: return False - case ScopeTier.AS: + case NodeScopeTier.AS: match other.tier: - case ScopeTier.Global: + case NodeScopeTier.Global: if same_type: # self is subset of other return True elif selfTypeInOther: @@ -438,7 +506,7 @@ def __lt__(self, other): else: # both scopes make statements about different types of scopes return False - case ScopeTier.Node: + case NodeScopeTier.Node: if same_asn: if same_type: return False @@ -449,10 +517,10 @@ def __lt__(self, other): else: return False - case ScopeTier.Node: + case NodeScopeTier.Node: match other.tier: - case ScopeTier.AS: + case NodeScopeTier.AS: if same_asn: if same_type: return True @@ -463,7 +531,7 @@ def __lt__(self, other): else: return False - case ScopeTier.Global: + case NodeScopeTier.Global: if same_type: return True elif selfTypeInOther: @@ -476,21 +544,436 @@ def __repr__(self): details = [] if self._as_id is not None: details.append(f"AS={self._as_id}") - if self._node_type != ScopeType.ANY: + if self._node_type != NodeScopeType.ANY: details.append(f"Type={self._node_type.name}") if self._node_id: details.append(f"Node={self._node_id}") - return f"Scope({', '.join(details) or 'Global'})" + return f"NodeScope({', '.join(details) or 'Global'})" - # for use with functools.cmp_to_key( Scope.collate ) + + + + +class NetScopeTier(ScopeTier): + Global = 3 + Scoped = 2 # by IX, ASN scope Nr + Individual = 1 # specific Network instance +class NetScopeType(ScopeType): + NONE = 0 + ANY = 9 + LOCAL = 1 # LocalNetwork + XC = 2 # CrossConnect + IX = 4 # InternetExchange + BRIDGE = 8 # Bridge @staticmethod - def collate(a: 'Scope', b: 'Scope') -> int: - #c,_= a._comparable(b) - try: - if a < b: - return -1 - elif b < a: - return 1 - except TypeError: # This happens if Python encounters NotImplemented in both cases - pass - return 0 # Fallback: Treat as equal or use another sorting logic + def from_net(net: 'Network') -> 'NetScopeType': + from .enums import NetworkType + match net.getType(): + case NetworkType.InternetExchange: + return NetScopeType.IX + case NetworkType.Local: + return NetScopeType.LOCAL + case NetworkType.CrossConnect: + return NetScopeType.XC + case NetworkType.Bridge: + return NetScopeType.BRIDGE + + +class NetScope(Scope): + """! + @brief strong type for the hierarchical scope of configuration settings. + + @details A NetScope object can define or specify the scope (or extension) of a NetOption. + This scope can encompass the following registry types/items: 'net' + """ + + def __init__(self, + tier: NetScopeTier, + net_type: NetScopeType = NetScopeType.ANY, + net_id: str = None, + scope_id: int = None): + ''' + @param scope_id an IX or ASN number + @param net_id name of a specific Network + ''' + if tier==NetScopeTier.Scoped: + assert scope_id!=None, 'Invalid Input' + assert net_id==None, 'Invalid Input' + if tier==NetScopeTier.Global: + assert net_id==None, 'invalid input' + assert scope_id==None, 'invalid input' + if tier==NetScopeTier.Individual: + assert net_id!=None, 'invalid input' + assert scope_id!=None, 'invalid input' + + self._tier = tier + self._net_type = net_type + self._net_id = net_id # Only set for per-node scopes + self._scope_id = scope_id # Only set for per-AS scopes + ''' + def __hash__(self): + """Allows Scope instances to be used as dictionary keys.""" + return hash((self.tier, self.node_type, self.node_id, self.as_id)) + ''' + + def domain(self) -> OptionDomain: + return OptionDomain.NET + + @property + def tier(self) -> NetScopeTier: + return self._tier + + @property + def type(self)-> NetScopeType: + return self._net_type + + @property + def net(self) -> Optional[str]: + return self._net_id + + @property + def scope(self) -> Optional[int]: + return self._scope_id + + def _intersection(self, other: 'NetScope') -> 'NodeScope': + """! + @brief return a new scope which represents the intersection of both scopes + """ + pass + + def _comparable(self, other: 'NetScope') -> Tuple[bool,bool]: + """ returns a two bools indicating wheter the scopes are: + lt/gt comparable or identical""" + + if not isinstance(other, NetScope): + return (False, False) + + same_type = True if self.type == other.type else False + common_type = self.type & other.type + otherTypeInSelf = ( (self.type & other.type) == other.type ) + selfTypeInOther = ( ( (other.type & self.type)==self.type ) ) + contained_type = selfTypeInOther or otherTypeInSelf + same_type = self.type == other.type + same_scope = self._scope_id == other._scope_id + same_node = same_scope and self._net_id == other._net_id # and same_type ?! + + if self.tier==other.tier: + match self.tier: + case NetScopeTier.Global: # asn, nodeID irrelevant + if same_type: + return False, True + else: + return False, False + + case NetScopeTier.Scoped: # nodeID irrelevant + if same_scope: + if same_type: + return False, True + else: + return False, False + else: + return False, False + + case NetScopeTier.Individual: # type should be irrelevant (i.e. redundant ) here + if same_scope: + if same_node: + return False,True + else: + return False, False + else: + return False, False + else: + match self.tier: + case NetScopeTier.Global: + # other.tier must be AS or Node + match other.tier: + case NetScopeTier.Scoped: + if same_type: + # other AS scope is subset of self + return True, False + elif otherTypeInSelf: + # other AS scope is a subset of self + return True, False + else: + # types conflict and prevent inclusion + return False, False + case NetScopeTier.Individual: + if same_type: + return True, False + elif otherTypeInSelf: + return True, False + else: + return False, False + + case NetScopeTier.Scoped: + + match other.tier: + case NetScopeTier.Global: + if same_type: # self is subset of other + return True, False + elif selfTypeInOther: + return True, False + else: + # both scopes make statements about different types of scopes + return False, False + case NetScopeTier.Individual: + if same_scope: + if same_type: + return True, False + elif otherTypeInSelf: + return True, False + else: + return False, False + else: + return False, False + + case NetScopeTier.Individual: + + match other.tier: + case NetScopeTier.Scoped: + if same_scope: + if same_type: + return True, False + elif selfTypeInOther: + return True, False + else: + return False, False + else: + return False, False + + case NetScopeTier.Global: + if same_type: + return True, False + elif selfTypeInOther: + return True, False + else: + return False, False + + + + + + def __gt__(self, other: 'NetScope'): + """!@brief defines scope hierarchy comparison i.e. broader more general(less specific) > more specific). + i.e. LHS supserset RHS + """ + if not isinstance(other, NetScope): + return NotImplemented + # TODO what is correct ?! NotImpl or False + #if not isinstance(other, NodeScope): return False + + same_type = True if self.type == other.type else False + common_type = self.type & other.type + otherTypeInSelf = ( (self.type & other.type) == other.type ) + selfTypeInOther = ( ( (other.type & self.type)==self.type ) ) + contained_type = selfTypeInOther or otherTypeInSelf + same_type = self.type == other.type + same_asn = self.scope == other.scope + same_node = same_asn and self.net == other.net # and same_type ?! + + if self.tier==other.tier: + match self.tier: + case NetScopeTier.Global: # asn, nodeID irrelevant + if same_type: + return False # they are equal not gt + elif otherTypeInSelf: + return True + else: + return NotImplemented + + case NetScopeTier.Scoped: # nodeID irrelevant + if same_asn: + if same_type: + return False # they are equal not gt + elif otherTypeInSelf: + return True + else: + return NotImplemented + else: + # scopes of different ASes are disjoint + return NotImplemented + + case NetScopeTier.Node: # type should be irrelevant (i.e. redundant ) here + if same_asn: + if same_node: + return False # equal and not gt + else: + return NotImplemented + else: + return NotImplemented + else: + match self.tier: + case NetScopeTier.Global: + # other.tier must be AS or Node + match other.tier: + case NetScopeTier.Scoped: + if same_type: + # other AS scope is subset of self (gt) + return True + elif otherTypeInSelf: + # other AS scope is a subset of self (gt) + return True + else: + return False + case NetScopeTier.Individual: + if same_type: + # other is subset of self (gt) + return True + elif otherTypeInSelf: + # other is subset of self (gt) + return True + else: + return False + + case NetScopeTier.Scoped: + + match other.tier: + case NetScopeTier.Global: + + return False + case NetScopeTier.Individual: + if same_asn: + if same_type: + return True + elif otherTypeInSelf: + return True + else: + return False + else: + return False + + case NetScopeTier.Individual: + + match other.tier: + case NetScopeTier.Scoped: + return False + + case NetScopeTier.Global: + return False + + + def __lt__(self, other: 'NetScope'): + """!@brie fDefines scope hierarchy comparison (more specific < broader). + i.e. LHS subset RHS + """ + if not isinstance(other, NetScope): + return NotImplemented + + same_type = True if self.type == other.type else False + common_type = self.type & other.type + otherTypeInSelf = ( (self.type & other.type) == other.type ) + selfTypeInOther = ( ( (other.type & self.type)==self.type ) ) + contained_type = selfTypeInOther or otherTypeInSelf + same_type = self.type == other.type + same_scope = self.scope == other.scope + same_net = same_scope and self.net == other.net # and same_type ?! + + if self.tier==other.tier: + match self.tier: + case NetScopeTier.Global: # asn, nodeID irrelevant + if same_type: + return False # they are equal not lt + elif selfTypeInOther: + return True + else: + return NotImplemented + + case NetScopeTier.AS: # nodeID irrelevant + if same_scope: + if same_type: + return False # they are equal not lt + elif selfTypeInOther: + return True + else: + return NotImplemented + else: + # scopes of different ASes are disjoint + return NotImplemented + + case NetScopeTier.Individual: # type should be irrelevant (i.e. redundant ) here + if same_scope: + if same_net: + return False # equal and not lt + else: + return NotImplemented + else: + return NotImplemented + else: + match self.tier: + case NetScopeTier.Global: + # other.tier must be AS or Node + match other.tier: + case NetScopeTier.Scoped: + if same_type: + # other AS scope is subset of self (gt) + return False + elif otherTypeInSelf: + # other AS scope is a subset of self (gt) + return False + else: + return False + case NetScopeTier.Individual: + if same_type: + # other is subset of self (gt) + return False + elif otherTypeInSelf: + # other is subset of self (gt) + return False + else: + return False + + case NetScopeTier.Scoped: + + match other.tier: + case NetScopeTier.Global: + if same_type: # self is subset of other + return True + elif selfTypeInOther: + return True + else: + # both scopes make statements about different types of scopes + return False + case NetScopeTier.Individual: + if same_scope: + if same_type: + return False + elif otherTypeInSelf: + return False + else: + return False + else: + return False + + case NetScopeTier.Individual: + + match other.tier: + case NetScopeTier.Scoped: + if same_scope: + if same_type: + return True + elif selfTypeInOther: + return True + else: + return False + else: + return False + + case NetScopeTier.Global: + if same_type: + return True + elif selfTypeInOther: + return True + else: + return False + + def __repr__(self): + """String representation for debugging.""" + details = [] + if self._scope_id is not None: + details.append(f"scope={self._scope_id}") + if self._net_type != NetScopeType.ANY: + details.append(f"Type={self._net_type.name}") + if self._net_id: + details.append(f"Net={self._net_id}") + return f"NetScope({', '.join(details) or 'Global'})" + diff --git a/seedemu/core/__init__.py b/seedemu/core/__init__.py index c3b3a66a0..3591e30a1 100644 --- a/seedemu/core/__init__.py +++ b/seedemu/core/__init__.py @@ -24,5 +24,6 @@ from .BaseSystem import BaseSystem from .Scope import * from .Option import BaseOption, OptionMode, Option, BaseComponent, BaseOptionGroup, AutoRegister, OptionGroupMeta +from .OptionUtil import OptionDomain from .OptionRegistry import OptionRegistry from .Volume import BaseVolume, ServiceLvlVolume, TopLvlVolume diff --git a/seedemu/layers/Base.py b/seedemu/layers/Base.py index 9c42f43e9..311d28fa7 100644 --- a/seedemu/layers/Base.py +++ b/seedemu/layers/Base.py @@ -1,7 +1,8 @@ from __future__ import annotations -from seedemu.core import AutonomousSystem, InternetExchange, AddressAssignmentConstraint, Node, Graphable, Emulator, Layer +from seedemu.core import AutonomousSystem, InternetExchange, AddressAssignmentConstraint, Node, Graphable, Emulator, Layer, NetScope, NetScopeTier from typing import Dict, List from seedemu.options.Sysctl import SysctlOpts +from seedemu.options.Net import NetOpts BaseFileTemplates: Dict[str, str] = {} BaseFileTemplates["interface_setup_script"] = """\ @@ -45,11 +46,21 @@ class Base(Layer, Graphable): __name_servers: List[str] def getAvailableOptions(self): + """options that should be installed on all Nodes in the emulation""" from seedemu.core.OptionRegistry import OptionRegistry - opt_keys = [ o.fullname() for o in SysctlOpts().components_recursive()] + opt_groups = [SysctlOpts()] # , NetOpts() + opt_keys = [ o.fullname() for og in opt_groups for o in og.components_recursive()] return [OptionRegistry().getOption(o) for o in opt_keys] + + def getNetOptions(self): + """options that should be installed on all Networks in the emulation""" + from seedemu.core.OptionRegistry import OptionRegistry + opt_groups = [ NetOpts()] + opt_keys = [ o.fullname() for og in opt_groups for o in og.components_recursive()] + return [OptionRegistry().getOption(o) for o in opt_keys] + - def __init__(self): + def __init__(self, **kwargs): """! @brief Base layer constructor. """ @@ -57,6 +68,16 @@ def __init__(self): self.__ases = {} self.__ixes = {} self.__name_servers = [] + from seedemu.core import OptionRegistry + for k,v in kwargs.items(): + # Replace the 'defaults' class methods dynamically + + opt_cls = type(v) + # Capture 'new_value' as default argument (forces a snapshot of the current value) + opt_cls.default = classmethod(lambda cls, new_value=v.value: new_value) + opt_cls.defaultMode = classmethod(lambda cls, newmode=v.mode: newmode) + prefix = getattr(opt_cls, '__prefix') if hasattr(opt_cls, '__prefix') else None + OptionRegistry().register(opt_cls, prefix) def getName(self) -> str: return "Base" @@ -81,6 +102,7 @@ def applyFeaturesToNodes(self, _as: AutonomousSystem, emulator: Emulator): ''' def configure(self, emulator: Emulator): + self._log('registering nodes...') for asobj in self.__ases.values(): @@ -89,14 +111,22 @@ def configure(self, emulator: Emulator): asobj.registerNodes(emulator) asobj.inheritOptions(emulator) - - self._log('setting up internet exchanges...') for ix in self.__ixes.values(): ix.configure(emulator) + # now all Networks are registered ..(exchept XCs which are only created in the process of Node::configure() ) + nets = [ n for (scope,typ,name), n in emulator.getRegistry().getAll().items() if typ=='net'] + # set defaults for NetOptions like Bandwidth, Mtu etc. ... + for n in nets: + for o in self.getNetOptions(): + n.setOption(o, NetScope(NetScopeTier.Global)) + + super().configure(emulator) self._log('setting up autonomous systems...') for asobj in self.__ases.values(): asobj.configure(emulator) - super().configure(emulator) + # this calls Node::configure() in turn and instantiates and registers XC networks + + #super().configure(emulator) def render(self, emulator: Emulator) -> None: for ((scope, type, name), obj) in emulator.getRegistry().getAll().items(): diff --git a/seedemu/layers/Scion.py b/seedemu/layers/Scion.py index 95326e8f5..98feb6f9d 100644 --- a/seedemu/layers/Scion.py +++ b/seedemu/layers/Scion.py @@ -11,7 +11,7 @@ from seedemu.core import (Emulator, Interface, Layer, Network, Registry, Router, ScionAutonomousSystem, ScopedRegistry, Graphable, Node, - Option, AutoRegister, OptionMode) + Option, OptionDomain, AutoRegister, OptionMode) from seedemu.core.ScionAutonomousSystem import IA from seedemu.layers import ScionBase, ScionIsd import shutil @@ -86,6 +86,7 @@ class SCION_ETC_CONFIG_VOL(Option, AutoRegister): where to put all the SCION related files on the host. """ value_type = ScionConfigMode + domain: OptionDomain = OptionDomain.NODE @classmethod def supportedModes(cls) -> OptionMode: return OptionMode.BUILD_TIME diff --git a/seedemu/layers/ScionRouting.py b/seedemu/layers/ScionRouting.py index 79585d6aa..8eeb3f8a8 100644 --- a/seedemu/layers/ScionRouting.py +++ b/seedemu/layers/ScionRouting.py @@ -12,7 +12,7 @@ from seedemu.core import ( Emulator, Node, ScionAutonomousSystem, promote_to_scion_router, Network, Router, BaseOption, OptionMode, Layer, - BaseOptionGroup, Option) + BaseOptionGroup, Option, OptionDomain) from seedemu.core.enums import NetworkType from seedemu.core.ScionAutonomousSystem import IA from seedemu.layers import Routing, ScionBase, ScionIsd @@ -27,7 +27,7 @@ class ScionStackOpts(BaseOptionGroup): # TODO: add CS tracing # TODO: add dispatchable port range # make installation of test-tools optional (bwtester etc.) - + domain: OptionDomain = OptionDomain.NODE class ROTATE_LOGS(Option): """prevent excessive growth of log files for longer running simulations by rotating log files """ diff --git a/seedemu/options/Net.py b/seedemu/options/Net.py new file mode 100644 index 000000000..f75e20725 --- /dev/null +++ b/seedemu/options/Net.py @@ -0,0 +1,57 @@ +from seedemu.core import BaseOptionGroup, Option, OptionMode, OptionDomain + + +class NetOpts(BaseOptionGroup): + domain = OptionDomain.NET + + class Mtu(Option): + """!@ Maximum Transmission Unit of a Network in [byte] + """ + value_type = int + @classmethod + def supportedModes(cls): + return OptionMode.BUILD_TIME + + @classmethod + def default(cls): + """the default MTU of Ethernet is 1500bytes""" + return 1500 + + class Latency(Option): + """!@ propagation latency of a link in [ms] + """ + value_type = int + @classmethod + def supportedModes(cls): + return OptionMode.BUILD_TIME + + @classmethod + def default(cls): + """default is unlimited datarate (infinite propagation speed, causing no delay)""" + return 0 + + class Bandwidth(Option): + """!@ capacity of a link in [bits/second] + """ + value_type = int + @classmethod + def supportedModes(cls): + return OptionMode.BUILD_TIME + + @classmethod + def default(cls): + """default is unlimited bandwidth""" + return 0 + + class PacketLoss(Option): + """!@ probability of packet loss on a link [0,1) + """ + value_type = float + @classmethod + def supportedModes(cls): + return OptionMode.BUILD_TIME + + @classmethod + def default(cls): + """default is perfect ideal link without loss""" + return 0 \ No newline at end of file diff --git a/seedemu/options/Sysctl.py b/seedemu/options/Sysctl.py index 320583b61..3a190fdd5 100644 --- a/seedemu/options/Sysctl.py +++ b/seedemu/options/Sysctl.py @@ -1,11 +1,14 @@ -from seedemu.core import BaseOptionGroup, Option, OptionMode +from seedemu.core import BaseOptionGroup, Option, OptionMode, OptionDomain class SysctlOpts(BaseOptionGroup): # NOTE: the classname is dynamically changed to just 'sysctl' so the # nested option names don't become too lengthy... + domain = OptionDomain.NODE + class NetIpv4(BaseOptionGroup): + domain = OptionDomain.NODE class IP_FORWARD(Option): """net.ipv4.ip_forward flag""" @@ -46,7 +49,7 @@ class Conf(BaseOptionGroup): # all..sets a value for all interfaces # 'interface' .. changes special settings per interface # (where "interface" is the name of your network interface) - + domain = OptionDomain.NODE class RP_FILTER(Option): value_type = dict diff --git a/seedemu/options/__init__.py b/seedemu/options/__init__.py index f2d493b72..d47b5aab7 100644 --- a/seedemu/options/__init__.py +++ b/seedemu/options/__init__.py @@ -1 +1,2 @@ -from .Sysctl import * \ No newline at end of file +from .Sysctl import * +from .Net import * \ No newline at end of file diff --git a/tests/options/OptionsTestCase.py b/tests/options/OptionsTestCase.py index 91633b37c..3d9928efd 100644 --- a/tests/options/OptionsTestCase.py +++ b/tests/options/OptionsTestCase.py @@ -4,9 +4,12 @@ import unittest as ut import os from seedemu.core import ( - Scope, - ScopeTier, - ScopeType, + NodeScope, + NetScope, + NetScopeTier, + NetScopeType, + NodeScopeTier, + NodeScopeType, BaseOption, OptionMode, Customizable, @@ -37,54 +40,76 @@ def down_emulator(cls): """currently there are just no integration tests for options""" pass + def test_netscope(self): + cmpr = NetScope.collate + self.assertTrue( NetScope(NetScopeTier.Global) > NetScope(NetScopeTier.Scoped, scope_id=150), 'global scope is superset of AS scopes') + self.assertTrue( NetScope(NetScopeTier.Scoped, scope_id=150) < NetScope(NetScopeTier.Global) , 'AS scopes are subset of global scope') + + self.assertTrue( cmpr(NetScope(NetScopeTier.Scoped, scope_id=150), + NetScope(NetScopeTier.Scoped, scope_id=160))==0, 'disjoint AS scopes') + self.assertTrue( cmpr(NetScope(NetScopeTier.Individual, scope_id=150, net_id='br0'), + NetScope(NetScopeTier.Individual, scope_id=160, net_id='br0'))==0, + 'disjoint Net scopes different Scopes') + self.assertTrue( cmpr(NetScope(NetScopeTier.Individual, scope_id=150, net_id='br0'), + NetScope(NetScopeTier.Individual, scope_id=150, net_id='br1'))==0, + 'disjoint Net scopes same AS') + self.assertTrue( cmpr(NetScope(NetScopeTier.Scoped, scope_id=150,net_type=NetScopeType.IX), + NetScope(NetScopeTier.Scoped, scope_id=150,net_type=NetScopeType.BRIDGE))==0, + 'disjoint Types scopes at AS level') + self.assertTrue( cmpr(NetScope(NetScopeTier.Scoped, scope_id=150,net_type=NetScopeType.LOCAL), + NetScope(NetScopeTier.Scoped, scope_id=160,net_type=NetScopeType.XC))==0, + 'disjoint Types as well as ASes') + self.assertTrue( cmpr(NetScope(NetScopeTier.Global,net_type=NetScopeType.LOCAL), + NetScope(NetScopeTier.Global,net_type=NetScopeType.IX))==0, + 'disjoint Types scopes at global level') def test_scope(self): - """!@brief tests proper inclusion/exclusion, intersection and union of Scopes""" - cmpr = Scope.collate + """!@brief tests proper inclusion/exclusion, intersection and union of NodeScopes""" + cmpr = NodeScope.collate - self.assertTrue( Scope(ScopeTier.Global) > Scope(ScopeTier.AS, as_id=150), 'global scope is superset of AS scopes') - self.assertTrue( Scope(ScopeTier.AS, as_id=150) < Scope(ScopeTier.Global) , 'AS scopes are subset of global scope') + self.assertTrue( NodeScope(NodeScopeTier.Global) > NodeScope(NodeScopeTier.AS, as_id=150), 'global scope is superset of AS scopes') + self.assertTrue( NodeScope(NodeScopeTier.AS, as_id=150) < NodeScope(NodeScopeTier.Global) , 'AS scopes are subset of global scope') - self.assertTrue( cmpr(Scope(ScopeTier.AS, as_id=150), - Scope(ScopeTier.AS, as_id=160))==0, 'disjoint AS scopes') - self.assertTrue( cmpr(Scope(ScopeTier.Node, as_id=150, node_id='br0'), - Scope(ScopeTier.Node, as_id=160, node_id='br0'))==0, + self.assertTrue( cmpr(NodeScope(NodeScopeTier.AS, as_id=150), + NodeScope(NodeScopeTier.AS, as_id=160))==0, 'disjoint AS scopes') + self.assertTrue( cmpr(NodeScope(NodeScopeTier.Node, as_id=150, node_id='br0'), + NodeScope(NodeScopeTier.Node, as_id=160, node_id='br0'))==0, 'disjoint Nodes scopes different ASes') - self.assertTrue( cmpr(Scope(ScopeTier.Node, as_id=150, node_id='br0'), - Scope(ScopeTier.Node, as_id=150, node_id='br1'))==0, + self.assertTrue( cmpr(NodeScope(NodeScopeTier.Node, as_id=150, node_id='br0'), + NodeScope(NodeScopeTier.Node, as_id=150, node_id='br1'))==0, 'disjoint Nodes scopes same AS') - self.assertTrue( cmpr(Scope(ScopeTier.AS, as_id=150,node_type=ScopeType.HNODE), - Scope(ScopeTier.AS, as_id=150,node_type=ScopeType.BRDNODE))==0, + self.assertTrue( cmpr(NodeScope(NodeScopeTier.AS, as_id=150,node_type=NodeScopeType.HNODE), + NodeScope(NodeScopeTier.AS, as_id=150,node_type=NodeScopeType.BRDNODE))==0, 'disjoint Types scopes at AS level') - self.assertTrue( cmpr(Scope(ScopeTier.AS, as_id=150,node_type=ScopeType.HNODE), - Scope(ScopeTier.AS, as_id=160,node_type=ScopeType.BRDNODE))==0, + self.assertTrue( cmpr(NodeScope(NodeScopeTier.AS, as_id=150,node_type=NodeScopeType.HNODE), + NodeScope(NodeScopeTier.AS, as_id=160,node_type=NodeScopeType.BRDNODE))==0, 'disjoint Types as well as ASes') - self.assertTrue( cmpr(Scope(ScopeTier.Global,node_type=ScopeType.HNODE), - Scope(ScopeTier.Global,node_type=ScopeType.BRDNODE))==0, + self.assertTrue( cmpr(NodeScope(NodeScopeTier.Global,node_type=NodeScopeType.HNODE), + NodeScope(NodeScopeTier.Global,node_type=NodeScopeType.BRDNODE))==0, 'disjoint Types scopes at global level') - self.assertTrue ( Scope(ScopeTier.AS, as_id=150) > - Scope(ScopeTier.Node, as_id=150, node_id='brd0', node_type=ScopeType.BRDNODE), + self.assertTrue ( NodeScope(NodeScopeTier.AS, as_id=150) > + NodeScope(NodeScopeTier.Node, as_id=150, node_id='brd0', node_type=NodeScopeType.BRDNODE), 'node scope is subset of AS scope') - self.assertTrue( not ( Scope(ScopeTier.AS, as_id=150) < - Scope(ScopeTier.Node, as_id=150, node_id='brd0', node_type=ScopeType.BRDNODE))) + self.assertTrue( not ( NodeScope(NodeScopeTier.AS, as_id=150) < + NodeScope(NodeScopeTier.Node, as_id=150, node_id='brd0', node_type=NodeScopeType.BRDNODE))) - self.assertTrue( Scope(ScopeTier.AS, as_id=160) == - Scope(ScopeTier.AS, as_id=160) , 'identical AS scope') - self.assertTrue( Scope(ScopeTier.AS, as_id=160, node_type=ScopeType.ANY) == - Scope(ScopeTier.AS, as_id=160) , 'identical AS scope') - self.assertTrue( Scope(ScopeTier.AS, as_id=160, node_type=ScopeType.ANY) > - Scope(ScopeTier.AS, as_id=160, node_type=ScopeType.BRDNODE), 'ANY includes all types') - self.assertTrue( Scope(ScopeTier.AS, as_id=150) != - Scope(ScopeTier.AS, as_id=160) , 'not identical scope') - self.assertTrue( Scope(ScopeTier.Node, as_id=150,node_id='brd0') == - Scope(ScopeTier.Node, as_id=150,node_id='brd0',node_type=ScopeType.BRDNODE), + self.assertTrue( NodeScope(NodeScopeTier.AS, as_id=160) == + NodeScope(NodeScopeTier.AS, as_id=160) , 'identical AS scope') + self.assertTrue( NodeScope(NodeScopeTier.AS, as_id=160, node_type=NodeScopeType.ANY) == + NodeScope(NodeScopeTier.AS, as_id=160) , 'identical AS scope') + self.assertTrue( NodeScope(NodeScopeTier.AS, as_id=160, node_type=NodeScopeType.ANY) > + NodeScope(NodeScopeTier.AS, as_id=160, node_type=NodeScopeType.BRDNODE), 'ANY includes all types') + self.assertTrue( NodeScope(NodeScopeTier.AS, as_id=150) != + NodeScope(NodeScopeTier.AS, as_id=160) , 'not identical scope') + self.assertTrue( NodeScope(NodeScopeTier.Node, as_id=150,node_id='brd0') == + NodeScope(NodeScopeTier.Node, as_id=150,node_id='brd0',node_type=NodeScopeType.BRDNODE), 'same node but with extra type info') - self.assertTrue( Scope(ScopeTier.Global) > Scope(ScopeTier.Node, as_id=150, node_id='brd0')) - self.assertTrue( not (Scope(ScopeTier.Global, ScopeType.HNODE) > - Scope(ScopeTier.Node, as_id=150, node_id='brd0', node_type=ScopeType.BRDNODE) ), + self.assertTrue( NodeScope(NodeScopeTier.Global) > NodeScope(NodeScopeTier.Node, as_id=150, node_id='brd0')) + self.assertTrue( not (NodeScope(NodeScopeTier.Global, NodeScopeType.HNODE) > + NodeScope(NodeScopeTier.Node, as_id=150, node_id='brd0', node_type=NodeScopeType.BRDNODE) ), 'node unaffected by global type') def test_customizable(self): @@ -165,10 +190,10 @@ def __repr__(self): config = Customizable() # Define scopes - global_scope = Scope(ScopeTier. Global) - global_router_scope = Scope(ScopeTier. Global, ScopeType.RNODE) - as_router_scope = Scope(ScopeTier.AS, ScopeType.RNODE, as_id=42) - node_scope = Scope(ScopeTier.Node, ScopeType.RNODE, node_id="A", as_id=42) + global_scope = NodeScope(NodeScopeTier. Global) + global_router_scope = NodeScope(NodeScopeTier. Global, NodeScopeType.RNODE) + as_router_scope = NodeScope(NodeScopeTier.AS, NodeScopeType.RNODE, as_id=42) + node_scope = NodeScope(NodeScopeTier.Node, NodeScopeType.RNODE, node_id="A", as_id=42) config.setOption( _Option.custom("max_bandwidth", 100), global_scope) config.setOption( _Option.custom("max_bandwidth", 200), global_router_scope) @@ -176,11 +201,11 @@ def __repr__(self): config.setOption( _Option.custom("max_bandwidth", 500), node_scope) # Retrieve values using a Scope object - self.assertTrue( (opt:=config.getOption("max_bandwidth", Scope(ScopeTier.Node, ScopeType.RNODE, node_id="A",as_id=42))) != None and opt.value==500)# 500 (Node-specific) - self.assertTrue( (opt:=config.getOption("max_bandwidth", Scope(ScopeTier.Node, ScopeType.HNODE, node_id="C", as_id=42))) != None and opt.value==100)# 100 (Global fallback) - self.assertTrue( (opt:=config.getOption("max_bandwidth", Scope(ScopeTier.Node, ScopeType.RNODE, node_id="D", as_id=99))) != None and opt.value==200)# 200 (Global & Type) - self.assertTrue( (opt:=config.getOption("max_bandwidth", Scope(ScopeTier.Node, ScopeType.HNODE, node_id="E", as_id=99))) != None and opt.value==100)# 100 (Global-wide) - self.assertTrue( (opt:=config.getOption("max_bandwidth", Scope(ScopeTier.Node, ScopeType.RNODE, node_id="B", as_id=42))) != None and opt.value==400)# 400 (AS & Type) + self.assertTrue( (opt:=config.getOption("max_bandwidth", NodeScope(NodeScopeTier.Node, NodeScopeType.RNODE, node_id="A",as_id=42))) != None and opt.value==500)# 500 (Node-specific) + self.assertTrue( (opt:=config.getOption("max_bandwidth", NodeScope(NodeScopeTier.Node, NodeScopeType.HNODE, node_id="C", as_id=42))) != None and opt.value==100)# 100 (Global fallback) + self.assertTrue( (opt:=config.getOption("max_bandwidth", NodeScope(NodeScopeTier.Node, NodeScopeType.RNODE, node_id="D", as_id=99))) != None and opt.value==200)# 200 (Global & Type) + self.assertTrue( (opt:=config.getOption("max_bandwidth", NodeScope(NodeScopeTier.Node, NodeScopeType.HNODE, node_id="E", as_id=99))) != None and opt.value==100)# 100 (Global-wide) + self.assertTrue( (opt:=config.getOption("max_bandwidth", NodeScope(NodeScopeTier.Node, NodeScopeType.RNODE, node_id="B", as_id=42))) != None and opt.value==400)# 400 (AS & Type) child_config = Customizable() child_config._scope = node_scope @@ -195,6 +220,7 @@ def __repr__(self): def get_test_suite(cls): test_suite = ut.TestSuite() test_suite.addTest(SEEDEmuOptionSystemTestCase('test_scope')) + test_suite.addTest(SEEDEmuOptionSystemTestCase('test_netscope')) test_suite.addTest(SEEDEmuOptionSystemTestCase('test_customizable')) return test_suite