From ddb92bb4c4fb4099497899c1ce4ed07fdd40cdff Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Wed, 16 Apr 2025 17:38:51 +0200 Subject: [PATCH 1/8] Network Options --- seedemu/core/Network.py | 36 ++++++++++-------------- seedemu/core/Option.py | 2 ++ seedemu/options/Net.py | 56 +++++++++++++++++++++++++++++++++++++ seedemu/options/__init__.py | 3 +- 4 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 seedemu/options/Net.py diff --git a/seedemu/core/Network.py b/seedemu/core/Network.py index 90637708..66027c56 100644 --- a/seedemu/core/Network.py +++ b/seedemu/core/Network.py @@ -7,9 +7,10 @@ from .Registry import Registrable from .AddressAssignmentConstraint import AddressAssignmentConstraint, Assigner from .Visualization import Vertex +from .Customizable import Customizable from typing import Dict, Tuple, List -class Network(Printable, Registrable, Vertex): +class Network(Printable, Registrable, Vertex, Customizable): """! @brief The network class. @@ -24,12 +25,6 @@ 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 ?! @@ -66,12 +61,6 @@ 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 @@ -109,7 +98,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 +109,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 +125,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 +153,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/Option.py b/seedemu/core/Option.py index ebf92a95..42495414 100644 --- a/seedemu/core/Option.py +++ b/seedemu/core/Option.py @@ -94,6 +94,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 diff --git a/seedemu/options/Net.py b/seedemu/options/Net.py new file mode 100644 index 00000000..d03942f0 --- /dev/null +++ b/seedemu/options/Net.py @@ -0,0 +1,56 @@ +from seedemu.core import BaseOptionGroup, Option, OptionMode + + +class NetOpts(BaseOptionGroup): + + 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/__init__.py b/seedemu/options/__init__.py index f2d493b7..d47b5aab 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 From 81abf1909f421dc8511faf591ae96beafe18c565 Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Wed, 16 Apr 2025 20:18:25 +0200 Subject: [PATCH 2/8] wip separate Net and Node-Scopes --- examples/basic/A00_simple_as/simple_as.py | 3 +- .../S02_scion_bgp_mixed/scion_bgp_mixed.py | 6 +- seedemu/compiler/Docker.py | 34 +- seedemu/core/AutonomousSystem.py | 10 +- seedemu/core/Binding.py | 4 +- seedemu/core/Configurable.py | 4 +- seedemu/core/Customizable.py | 14 +- seedemu/core/Network.py | 11 +- seedemu/core/Node.py | 21 +- seedemu/core/ScionAutonomousSystem.py | 4 +- seedemu/core/Scope.py | 622 +++++++++++++++--- seedemu/layers/Base.py | 25 +- tests/options/OptionsTestCase.py | 84 +-- 13 files changed, 674 insertions(+), 168 deletions(-) diff --git a/examples/basic/A00_simple_as/simple_as.py b/examples/basic/A00_simple_as/simple_as.py index 8f937ee7..4a9518c3 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): @@ -43,6 +43,7 @@ def run(dumpfile = None): # Create an autonomous system as150 = base.createAutonomousSystem(150) + as150.setOption(OptionRegistry().net_mtu(9000)) # 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 bfe8489c..15f377f4 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, Scope, 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, + Scope(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 2b06eb8b..b72ccfaa 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, Scope, NodeScopeType, NodeScopeTier, OptionHandling, BaseVolume, OptionMode from seedemu.core.enums import NodeRole, NetworkType from .DockerImage import DockerImage from .DockerImageConstant import * @@ -1159,42 +1159,42 @@ def cmp_snd(a, b): def _sndary_key(self, o: BaseOption, s: Scope ) -> 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: @@ -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(Scope(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 f8410190..4481e08d 100644 --- a/seedemu/core/AutonomousSystem.py +++ b/seedemu/core/AutonomousSystem.py @@ -5,7 +5,7 @@ from .AddressAssignmentConstraint import AddressAssignmentConstraint from .enums import NetworkType, NodeRole from .Node import Node, Router -from .Scope import ScopeTier, Scope +from .Scope import NodeScopeTier, Scope from .Emulator import Emulator from .Configurable import Configurable from .Customizable import Customizable @@ -133,9 +133,13 @@ def inheritOptions(self, emulator: Emulator): for n in all_nodes: self.handDown(n) + 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)-> Scope: """return a scope specific to this AS""" - return Scope(ScopeTier.AS, as_id=self.getAsn()) + return Scope(NodeScopeTier.AS, as_id=self.getAsn()) def configure(self, emulator: Emulator): @@ -188,7 +192,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 b349fecd..f337f4f7 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 Scope, 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, Scope(NodeScopeTier.Global)) # set name servers host.setNameServers(asObject.getNameServers()) diff --git a/seedemu/core/Configurable.py b/seedemu/core/Configurable.py index 928fa545..0096128b 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 Scope, 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, Scope(NodeScopeTier.Global)) def configure(self, emulator: Emulator): diff --git a/seedemu/core/Customizable.py b/seedemu/core/Customizable.py index cd1ccc32..69d596d9 100644 --- a/seedemu/core/Customizable.py +++ b/seedemu/core/Customizable.py @@ -19,7 +19,7 @@ def __init__(self): # scope param. actually only for debug/tests , scope: Scop def scope(self)-> Scope: """!@brief returns a scope that includes only this very customizable instance ,nothing else""" # 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 Scope(NodeScopeTier.Global) # maybe introduce a ScopeTier.NONE for this... else: return self._scope @@ -77,13 +77,13 @@ def getOption(self, key: str, scope: Scope = None, prefix: str = None ) -> Optio def _possible_scopes(scope: Scope) -> List[Scope]: possible_scopes = [ - Scope(ScopeTier.Node, scope._node_type, + Scope(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 - 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) + Scope(NodeScopeTier.Node,node_id=scope._node_id, as_id=scope._as_id) if scope._node_id and scope._as_id else None, # Node-specific + Scope(NodeScopeTier.AS, scope._node_type, as_id=scope._as_id) if scope._as_id and scope._node_type else None, # AS & Type + Scope(NodeScopeTier.AS, NodeScopeType.ANY, as_id=scope._as_id) if scope._as_id else None, # AS-wide + Scope(NodeScopeTier.Global, scope._node_type), # Global & Type + Scope(NodeScopeTier.Global) # Global (fallback) ] return possible_scopes diff --git a/seedemu/core/Network.py b/seedemu/core/Network.py index 66027c56..c7ae4837 100644 --- a/seedemu/core/Network.py +++ b/seedemu/core/Network.py @@ -8,6 +8,7 @@ from .AddressAssignmentConstraint import AddressAssignmentConstraint, Assigner from .Visualization import Vertex from .Customizable import Customizable +from .Scope import Scope,NetScope, NetScopeTier, NetScopeType from typing import Dict, Tuple, List class Network(Printable, Registrable, Vertex, Customizable): @@ -31,7 +32,7 @@ class Network(Printable, Registrable, Vertex, Customizable): __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. @@ -51,6 +52,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,6 +68,13 @@ def __init__(self, name: str, type: NetworkType, prefix: IPv4Network, aac: Addre self.__rap = None self.__ecp = None + def scope(self)-> Scope: + """return a Scope that is specific to this Network""" + return NetScope(tier=NetScopeTier.Individual, + net_type=NetScopeType.from_net(self), + 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 diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index 59b32b8b..46aafd4d 100644 --- a/seedemu/core/Node.py +++ b/seedemu/core/Node.py @@ -120,15 +120,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 +155,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: """! @@ -285,8 +292,8 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): def scope(self)-> Scope: - return Scope(ScopeTier.Node, - node_type=ScopeType.from_node(self), + return Scope(NodeScopeTier.Node, + node_type=NodeScopeType.from_node(self), node_id=self.getName(), as_id=self.getAsn()) diff --git a/seedemu/core/ScionAutonomousSystem.py b/seedemu/core/ScionAutonomousSystem.py index bb5d8100..d5d476b7 100644 --- a/seedemu/core/ScionAutonomousSystem.py +++ b/seedemu/core/ScionAutonomousSystem.py @@ -8,7 +8,7 @@ from .Emulator import Emulator from .enums import NodeRole from .Node import Node -from .Scope import Scope, ScopeTier +from .Scope import Scope, NodeScopeTier class ScionASN: """! @@ -159,7 +159,7 @@ def __init__(self, asn: int, subnetTemplate: str = "10.{}.0.0/16"): self.__generateStaticInfoConfig = False def scope(self)-> Scope: - return Scope(ScopeTier.AS, as_id=self.getAsn()) + return Scope(NodeScopeTier.AS, as_id=self.getAsn()) 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 a69565b9..c253e70c 100644 --- a/seedemu/core/Scope.py +++ b/seedemu/core/Scope.py @@ -1,6 +1,6 @@ from enum import Enum, IntEnum -from typing import Tuple - +from typing import Tuple, Optional +from abc import ABC, abstractmethod # could be replaced by @total_order class ComparableEnum(Enum): @@ -25,6 +25,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 +39,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 +62,64 @@ 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): + @property + @abstractmethod + def tier(self) -> ScopeTier: + pass + + @property + @abstractmethod + def type(self)-> ScopeType: + pass + 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 -class Scope: + @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 + + +class NodeScope: """! @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) @@ -82,8 +135,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 +146,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 @@ -113,19 +166,19 @@ def __hash__(self): return hash((self.tier, self.node_type, self.node_id, self.as_id)) ''' @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': @@ -134,10 +187,12 @@ def _intersection(self, other: 'Scope') -> 'Scope': """ pass - def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: + def _comparable(self, other: Scope) -> 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 +204,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 +219,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 +229,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 +242,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 +250,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 +261,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 +272,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 +286,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 +295,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: Scope): """!@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 +318,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 +326,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 +338,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 +348,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 +360,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 +370,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 +387,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 +410,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: Scope): """!@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 +428,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 +436,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 +448,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 +458,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 +470,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 +480,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 +491,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 +502,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 +516,7 @@ def __lt__(self, other): else: return False - case ScopeTier.Global: + case NodeScopeTier.Global: if same_type: return True elif selfTypeInOther: @@ -476,21 +529,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: + """! + @brief strong type for the hierarchical scope of configuration settings. + + @note Scopes are immutable after construction and serve as a multi-level key or index into a set of options. + 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. + """ + + # NOTE ISD scope could be added here + + + 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.Node: + 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)) + ''' + @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: 'Scope') -> 'Scope': + """! + @brief return a new scope which represents the intersection of both scopes + """ + pass + + def _comparable(self, other: Scope) -> 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.AS: + 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.Node: + if same_type: + return True, False + elif otherTypeInSelf: + return True, False + else: + return False, False + + case NetScopeTier.AS: + + 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.Node: + if same_scope: + if same_type: + return True, False + elif otherTypeInSelf: + return True, False + else: + return False, False + else: + return False, False + + case NetScopeTier.Node: + + match other.tier: + case NetScopeTier.AS: + 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: Scope): + """!@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.AS: + + match other.tier: + case NetScopeTier.Global: + + return False + case NetScopeTier.Node: + if same_asn: + if same_type: + return True + elif otherTypeInSelf: + return True + else: + return False + else: + return False + + case NetScopeTier.Node: + + match other.tier: + case NetScopeTier.Scoped: + return False + + case NetScopeTier.Global: + return False + + + def __lt__(self, other: Scope): + """!@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/layers/Base.py b/seedemu/layers/Base.py index 9c42f43e..faf7c0dc 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,9 +46,19 @@ 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): """! @@ -81,6 +92,7 @@ def applyFeaturesToNodes(self, _as: AutonomousSystem, emulator: Emulator): ''' def configure(self, emulator: Emulator): + self._log('registering nodes...') for asobj in self.__ases.values(): @@ -89,13 +101,18 @@ 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 .. + 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)) self._log('setting up autonomous systems...') for asobj in self.__ases.values(): asobj.configure(emulator) + super().configure(emulator) def render(self, emulator: Emulator) -> None: diff --git a/tests/options/OptionsTestCase.py b/tests/options/OptionsTestCase.py index 91633b37..71634b94 100644 --- a/tests/options/OptionsTestCase.py +++ b/tests/options/OptionsTestCase.py @@ -5,8 +5,8 @@ import os from seedemu.core import ( Scope, - ScopeTier, - ScopeType, + NodeScopeTier, + NodeScopeType, BaseOption, OptionMode, Customizable, @@ -42,49 +42,49 @@ def test_scope(self): """!@brief tests proper inclusion/exclusion, intersection and union of Scopes""" cmpr = Scope.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( Scope(NodeScopeTier.Global) > Scope(NodeScopeTier.AS, as_id=150), 'global scope is superset of AS scopes') + self.assertTrue( Scope(NodeScopeTier.AS, as_id=150) < Scope(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(Scope(NodeScopeTier.AS, as_id=150), + Scope(NodeScopeTier.AS, as_id=160))==0, 'disjoint AS scopes') + self.assertTrue( cmpr(Scope(NodeScopeTier.Node, as_id=150, node_id='br0'), + Scope(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(Scope(NodeScopeTier.Node, as_id=150, node_id='br0'), + Scope(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(Scope(NodeScopeTier.AS, as_id=150,node_type=NodeScopeType.HNODE), + Scope(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(Scope(NodeScopeTier.AS, as_id=150,node_type=NodeScopeType.HNODE), + Scope(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(Scope(NodeScopeTier.Global,node_type=NodeScopeType.HNODE), + Scope(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 ( Scope(NodeScopeTier.AS, as_id=150) > + Scope(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 ( Scope(NodeScopeTier.AS, as_id=150) < + Scope(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( Scope(NodeScopeTier.AS, as_id=160) == + Scope(NodeScopeTier.AS, as_id=160) , 'identical AS scope') + self.assertTrue( Scope(NodeScopeTier.AS, as_id=160, node_type=NodeScopeType.ANY) == + Scope(NodeScopeTier.AS, as_id=160) , 'identical AS scope') + self.assertTrue( Scope(NodeScopeTier.AS, as_id=160, node_type=NodeScopeType.ANY) > + Scope(NodeScopeTier.AS, as_id=160, node_type=NodeScopeType.BRDNODE), 'ANY includes all types') + self.assertTrue( Scope(NodeScopeTier.AS, as_id=150) != + Scope(NodeScopeTier.AS, as_id=160) , 'not identical scope') + self.assertTrue( Scope(NodeScopeTier.Node, as_id=150,node_id='brd0') == + Scope(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( Scope(NodeScopeTier.Global) > Scope(NodeScopeTier.Node, as_id=150, node_id='brd0')) + self.assertTrue( not (Scope(NodeScopeTier.Global, NodeScopeType.HNODE) > + Scope(NodeScopeTier.Node, as_id=150, node_id='brd0', node_type=NodeScopeType.BRDNODE) ), 'node unaffected by global type') def test_customizable(self): @@ -165,10 +165,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 = Scope(NodeScopeTier. Global) + global_router_scope = Scope(NodeScopeTier. Global, NodeScopeType.RNODE) + as_router_scope = Scope(NodeScopeTier.AS, NodeScopeType.RNODE, as_id=42) + node_scope = Scope(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 +176,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", Scope(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", Scope(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", Scope(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", Scope(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", Scope(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 From 1a9ea48c7a6ecd7e9e5f140fdb84bda5bacf0e59 Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Wed, 16 Apr 2025 22:16:57 +0200 Subject: [PATCH 3/8] option domains --- seedemu/compiler/Docker.py | 4 +- seedemu/core/AutonomousSystem.py | 11 ++-- seedemu/core/Configurable.py | 4 +- seedemu/core/Customizable.py | 78 ++++++++++++++++++++------- seedemu/core/InternetExchange.py | 2 +- seedemu/core/Network.py | 16 ++++-- seedemu/core/Node.py | 7 ++- seedemu/core/Option.py | 47 +++++++++++----- seedemu/core/OptionRegistry.py | 10 +++- seedemu/core/OptionUtil.py | 26 +++++++++ seedemu/core/ScionAutonomousSystem.py | 5 +- seedemu/core/Scope.py | 55 ++++++++++++------- seedemu/core/__init__.py | 1 + seedemu/options/Net.py | 3 +- seedemu/options/Sysctl.py | 7 ++- 15 files changed, 203 insertions(+), 73 deletions(-) create mode 100644 seedemu/core/OptionUtil.py diff --git a/seedemu/compiler/Docker.py b/seedemu/compiler/Docker.py index b72ccfaa..b850f5d1 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, NodeScopeType, NodeScopeTier, OptionHandling, BaseVolume, OptionMode +from seedemu.core import Node, Network, Compiler, BaseSystem, BaseOption, Scope, NodeScope, NodeScopeType, NodeScopeTier, OptionHandling, BaseVolume, OptionMode from seedemu.core.enums import NodeRole, NetworkType from .DockerImage import DockerImage from .DockerImageConstant import * @@ -1346,7 +1346,7 @@ def _doCompile(self, emulator: Emulator): dummies = local_images + self._makeDummies() ), file=open('docker-compose.yml', 'w')) - self.generateEnvFile(Scope(NodeScopeTier.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 4481e08d..9428f435 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 NodeScopeTier, Scope +from .Scope import NodeScopeTier, Scope, NodeScope, NetScope, NetScopeTier +from .Option import OptionDomain from .Emulator import Emulator from .Configurable import Configurable from .Customizable import Customizable @@ -137,9 +138,13 @@ def inheritOptions(self, emulator: Emulator): 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)-> Scope: + def scope(self, domain: OptionDomain = None)-> Scope: """return a scope specific to this AS""" - return Scope(NodeScopeTier.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): diff --git a/seedemu/core/Configurable.py b/seedemu/core/Configurable.py index 0096128b..dd0688ce 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, NodeScopeTier + 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(NodeScopeTier.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 69d596d9..43c310a2 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(NodeScopeTier.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(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 - Scope(NodeScopeTier.Node,node_id=scope._node_id, as_id=scope._as_id) if scope._node_id and scope._as_id else None, # Node-specific - Scope(NodeScopeTier.AS, scope._node_type, as_id=scope._as_id) if scope._as_id and scope._node_type else None, # AS & Type - Scope(NodeScopeTier.AS, NodeScopeType.ANY, as_id=scope._as_id) if scope._as_id else None, # AS-wide - Scope(NodeScopeTier.Global, scope._node_type), # Global & Type - Scope(NodeScopeTier.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/InternetExchange.py b/seedemu/core/InternetExchange.py index a50ba1be..307659ee 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 c7ae4837..144513ab 100644 --- a/seedemu/core/Network.py +++ b/seedemu/core/Network.py @@ -10,6 +10,7 @@ from .Customizable import Customizable from .Scope import Scope,NetScope, NetScopeTier, NetScopeType from typing import Dict, Tuple, List +from .OptionUtil import OptionDomain class Network(Printable, Registrable, Vertex, Customizable): """! @@ -68,10 +69,19 @@ def __init__(self, name: str, type: NetworkType, prefix: IPv4Network, aac: Addre self.__rap = None self.__ecp = None - def scope(self)-> Scope: + def scope(self, domain: OptionDomain = None)-> Scope: """return a Scope that is specific to this Network""" - return NetScope(tier=NetScopeTier.Individual, - net_type=NetScopeType.from_net(self), + + 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()) diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index 46aafd4d..57324288 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 @@ -291,8 +292,9 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): self.__note = None - def scope(self)-> Scope: - return Scope(NodeScopeTier.Node, + def scope(self, domain: OptionDomain = None)-> Scope: + 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()) @@ -360,6 +362,7 @@ 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)) + # TOODO scope of XC nets ?! pair of both ASes .. ?! net = Network(netname, NetworkType.CrossConnect, localaddr.network, direct = False) # 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)) diff --git a/seedemu/core/Option.py b/seedemu/core/Option.py index 42495414..b551cf8a 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.""" @@ -112,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) @@ -121,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 @@ -139,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 @@ -161,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__ @@ -229,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: @@ -246,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: @@ -257,6 +271,7 @@ def description(cls) -> str: class BaseOptionGroup(BaseComponent , metaclass=OptionGroupMeta): _children = {} + domain: OptionDomain = None def describe(self) -> str: @@ -275,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 64af499f..20a818ef 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 00000000..11e0818a --- /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 d5d476b7..d3d781af 100644 --- a/seedemu/core/ScionAutonomousSystem.py +++ b/seedemu/core/ScionAutonomousSystem.py @@ -9,6 +9,7 @@ from .enums import NodeRole from .Node import Node from .Scope import Scope, 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(NodeScopeTier.AS, as_id=self.getAsn()) + def scope(self, domain: OptionDomain = None)-> Scope: + 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 c253e70c..007cc2d3 100644 --- a/seedemu/core/Scope.py +++ b/seedemu/core/Scope.py @@ -1,6 +1,7 @@ from enum import Enum, IntEnum from typing import Tuple, Optional from abc import ABC, abstractmethod +from .OptionUtil import OptionDomain # could be replaced by @total_order class ComparableEnum(Enum): @@ -75,6 +76,10 @@ def from_node(node: 'Node'): class Scope(ABC): + @abstractmethod + def domain(self) -> OptionDomain: + pass + @property @abstractmethod def tier(self) -> ScopeTier: @@ -119,7 +124,11 @@ def collate(a: 'Scope', b: 'Scope') -> int: return 0 # Fallback: Treat as equal or use another sorting logic -class NodeScope: +# 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) @@ -127,6 +136,8 @@ class NodeScope: 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 @@ -165,6 +176,10 @@ 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) -> NodeScopeTier: return self._tier @@ -564,19 +579,14 @@ def from_net(net: 'Network') -> 'NetScopeType': return NetScopeType.BRIDGE -class NetScope: +class NetScope(Scope): """! @brief strong type for the hierarchical scope of configuration settings. - @note Scopes are immutable after construction and serve as a multi-level key or index into a set of options. - 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 NetScope object can define or specify the scope (or extension) of a NetOption. + This scope can encompass the following registry types/items: 'net' """ - - # NOTE ISD scope could be added here - def __init__(self, tier: NetScopeTier, net_type: NetScopeType = NetScopeType.ANY, @@ -592,7 +602,7 @@ def __init__(self, if tier==NetScopeTier.Global: assert net_id==None, 'invalid input' assert scope_id==None, 'invalid input' - if tier==NetScopeTier.Node: + if tier==NetScopeTier.Individual: assert net_id!=None, 'invalid input' assert scope_id!=None, 'invalid input' @@ -605,6 +615,10 @@ 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 @@ -631,7 +645,8 @@ def _comparable(self, other: Scope) -> 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) + if not isinstance(other, NetScope): + return (False, False) same_type = True if self.type == other.type else False common_type = self.type & other.type @@ -672,7 +687,7 @@ def _comparable(self, other: Scope) -> Tuple[bool,bool]: case NetScopeTier.Global: # other.tier must be AS or Node match other.tier: - case NetScopeTier.AS: + case NetScopeTier.Scoped: if same_type: # other AS scope is subset of self return True, False @@ -682,7 +697,7 @@ def _comparable(self, other: Scope) -> Tuple[bool,bool]: else: # types conflict and prevent inclusion return False, False - case NetScopeTier.Node: + case NetScopeTier.Individual: if same_type: return True, False elif otherTypeInSelf: @@ -690,7 +705,7 @@ def _comparable(self, other: Scope) -> Tuple[bool,bool]: else: return False, False - case NetScopeTier.AS: + case NetScopeTier.Scoped: match other.tier: case NetScopeTier.Global: @@ -701,7 +716,7 @@ def _comparable(self, other: Scope) -> Tuple[bool,bool]: else: # both scopes make statements about different types of scopes return False, False - case NetScopeTier.Node: + case NetScopeTier.Individual: if same_scope: if same_type: return True, False @@ -712,10 +727,10 @@ def _comparable(self, other: Scope) -> Tuple[bool,bool]: else: return False, False - case NetScopeTier.Node: + case NetScopeTier.Individual: match other.tier: - case NetScopeTier.AS: + case NetScopeTier.Scoped: if same_scope: if same_type: return True, False @@ -810,13 +825,13 @@ def __gt__(self, other: Scope): else: return False - case NetScopeTier.AS: + case NetScopeTier.Scoped: match other.tier: case NetScopeTier.Global: return False - case NetScopeTier.Node: + case NetScopeTier.Individual: if same_asn: if same_type: return True @@ -827,7 +842,7 @@ def __gt__(self, other: Scope): else: return False - case NetScopeTier.Node: + case NetScopeTier.Individual: match other.tier: case NetScopeTier.Scoped: diff --git a/seedemu/core/__init__.py b/seedemu/core/__init__.py index c3b3a66a..3591e30a 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/options/Net.py b/seedemu/options/Net.py index d03942f0..f75e2072 100644 --- a/seedemu/options/Net.py +++ b/seedemu/options/Net.py @@ -1,7 +1,8 @@ -from seedemu.core import BaseOptionGroup, Option, OptionMode +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] diff --git a/seedemu/options/Sysctl.py b/seedemu/options/Sysctl.py index 320583b6..3a190fdd 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 From 30da009bac873d3878074033939343ded2f3f8e3 Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Wed, 16 Apr 2025 22:31:53 +0200 Subject: [PATCH 4/8] accept global defaults as ctor args in Base Layer --- examples/basic/A00_simple_as/simple_as.py | 6 ++++-- seedemu/core/Binding.py | 4 ++-- seedemu/core/Emulator.py | 2 +- seedemu/core/Network.py | 2 +- seedemu/core/Node.py | 2 +- seedemu/layers/Base.py | 12 +++++++++++- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/examples/basic/A00_simple_as/simple_as.py b/examples/basic/A00_simple_as/simple_as.py index 4a9518c3..5a9d5b39 100755 --- a/examples/basic/A00_simple_as/simple_as.py +++ b/examples/basic/A00_simple_as/simple_as.py @@ -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,7 +45,7 @@ def run(dumpfile = None): # Create an autonomous system as150 = base.createAutonomousSystem(150) - as150.setOption(OptionRegistry().net_mtu(9000)) + as150.setOption(OptionRegistry().net_mtu(1500)) # Create a network as150.createNetwork('net0') diff --git a/seedemu/core/Binding.py b/seedemu/core/Binding.py index f337f4f7..e7f02ba3 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, NodeScopeTier +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(NodeScopeTier.Global)) + host.setOption(o, NodeScope(NodeScopeTier.Global)) # set name servers host.setNameServers(asObject.getNameServers()) diff --git a/seedemu/core/Emulator.py b/seedemu/core/Emulator.py index 8c991e2c..1f1ed45a 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/Network.py b/seedemu/core/Network.py index 144513ab..aa9aad31 100644 --- a/seedemu/core/Network.py +++ b/seedemu/core/Network.py @@ -117,7 +117,7 @@ def setMtu(self, mtu: int) -> Network: @returns self, for chaining API calls. """ - from OptionRegistry import OptionRegistry + from .OptionRegistry import OptionRegistry self.setOption( OptionRegistry().net_mtu(mtu) ) return self diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index 57324288..246f3445 100644 --- a/seedemu/core/Node.py +++ b/seedemu/core/Node.py @@ -363,7 +363,7 @@ def configure(self, emulator: Emulator): # netname = 'as{}.{}_as{}.{}'.format(self.getAsn(), self.getName(), peerasn, peername) netname = ''.join(choice(ascii_letters) for i in range(10)) # TOODO scope of XC nets ?! pair of both ASes .. ?! - net = Network(netname, NetworkType.CrossConnect, localaddr.network, direct = False) # TODO: XC nets w/ direct flag? + 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)) diff --git a/seedemu/layers/Base.py b/seedemu/layers/Base.py index faf7c0dc..94e17a63 100644 --- a/seedemu/layers/Base.py +++ b/seedemu/layers/Base.py @@ -60,7 +60,7 @@ def getNetOptions(self): return [OptionRegistry().getOption(o) for o in opt_keys] - def __init__(self): + def __init__(self, **kwargs): """! @brief Base layer constructor. """ @@ -68,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" From 61b7a4667e44af9e98880c0c3152f423fb7db5e2 Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Thu, 17 Apr 2025 12:06:22 +0200 Subject: [PATCH 5/8] fixes --- .../S02_scion_bgp_mixed/scion_bgp_mixed.py | 4 +- seedemu/compiler/Docker.py | 8 +- seedemu/core/AutonomousSystem.py | 4 +- seedemu/core/Customizable.py | 20 ++-- seedemu/core/Network.py | 6 +- seedemu/core/Node.py | 2 +- seedemu/core/ScionAutonomousSystem.py | 4 +- seedemu/core/Scope.py | 36 +++--- tests/options/OptionsTestCase.py | 112 +++++++++++------- 9 files changed, 111 insertions(+), 85 deletions(-) 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 15f377f4..7f2180a0 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, NodeScopeTier, NodeScopeType, 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,7 +63,7 @@ # 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(NodeScopeTier.AS, + NodeScope(NodeScopeTier.AS, as_id=as150.getAsn(), node_type=NodeScopeType.BRDNODE)) diff --git a/seedemu/compiler/Docker.py b/seedemu/compiler/Docker.py index b850f5d1..710f39c9 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, NodeScope, NodeScopeType, NodeScopeTier, 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,7 +1156,7 @@ 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 NodeScopeTier.Global: @@ -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). diff --git a/seedemu/core/AutonomousSystem.py b/seedemu/core/AutonomousSystem.py index 9428f435..8fee192d 100644 --- a/seedemu/core/AutonomousSystem.py +++ b/seedemu/core/AutonomousSystem.py @@ -5,7 +5,7 @@ from .AddressAssignmentConstraint import AddressAssignmentConstraint from .enums import NetworkType, NodeRole from .Node import Node, Router -from .Scope import NodeScopeTier, Scope, NodeScope, NetScope, NetScopeTier +from .Scope import NodeScopeTier, NodeScope, NodeScope, NetScope, NetScopeTier from .Option import OptionDomain from .Emulator import Emulator from .Configurable import Configurable @@ -138,7 +138,7 @@ def inheritOptions(self, emulator: Emulator): 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)-> Scope: + def scope(self, domain: OptionDomain = None)-> NodeScope: """return a scope specific to this AS""" match domain: case OptionDomain.NODE: diff --git a/seedemu/core/Customizable.py b/seedemu/core/Customizable.py index 43c310a2..d73d39a9 100644 --- a/seedemu/core/Customizable.py +++ b/seedemu/core/Customizable.py @@ -9,14 +9,14 @@ class Customizable(object): """! @brief something that can be configured by Options """ - _config: Dict[str,Tuple[BaseOption,Scope]] + _config: Dict[str,Tuple[BaseOption,NodeScope]] def __init__(self): # scope param. actually only for debug/tests , scope: Scope = None super().__init__() self._config = {} self._scope = None - def scope(self, domain: OptionDomain = None)-> Scope: + def scope(self, domain: OptionDomain = None)-> NodeScope: """!@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) @@ -27,7 +27,7 @@ def scope(self, domain: OptionDomain = None)-> Scope: else: return self._scope - def getScopedOption(self, key: str, scope: Scope = None, prefix: str = None) -> Optional[Tuple[BaseOption, Scope]]: + def getScopedOption(self, key: str, scope: NodeScope = None, prefix: str = None) -> Optional[Tuple[BaseOption, NodeScope]]: """! @brief retrieves an option along with the most specific Scope in which it was set. """ from seedemu.core.OptionRegistry import OptionRegistry @@ -65,7 +65,7 @@ def getScopedOption(self, key: str, scope: Scope = None, prefix: str = None) -> return None - def getOption(self, key: str, scope: Scope = None, prefix: str = None ) -> Optional[BaseOption]: + def getOption(self, key: str, scope: NodeScope = None, prefix: str = None ) -> Optional[BaseOption]: """!@brief Retrieves an option(if set) based on the precedence rules (scoping). If not specified the option value for the scope most specific to 'this' customizable will be returned. @@ -80,7 +80,7 @@ def getOption(self, key: str, scope: Scope = None, prefix: str = None ) -> Optio else: return None - def _possible_scopes(scope: Scope) -> List[Scope]: + def _possible_scopes(scope: NodeScope) -> List[NodeScope]: if isinstance(scope, NodeScope): possible_scopes = [ NodeScope(NodeScopeTier.Node, scope._node_type, @@ -114,12 +114,12 @@ def _getKeys(self) -> List[str]: return list( self._config.keys()) # Tuple[ BaseOption, Scope ] or List[ScopedOption] where ScopedOption is just a wrapper around Tuple[BaseOption, Scope] - def getOptions(self, scope: Scope = None ) -> List[BaseOption]: + def getOptions(self, scope: NodeScope = None ) -> List[BaseOption]: """! @brief return all options included by the given scope. """ return [ self.getOption(k, scope) for k in self._getKeys() ] - def getScopedOptions(self, scope: Scope = None, prefix: str = None ) -> List[Tuple[BaseOption,Scope]]: + def getScopedOptions(self, scope: NodeScope = None, prefix: str = None ) -> List[Tuple[BaseOption,NodeScope]]: """! @brief return all options included by the given scope. """ return [ self.getScopedOption(k, scope) for k in self._getKeys() if (prefix != None and k.startswith(prefix)) or prefix==None ] @@ -154,7 +154,7 @@ def valid_down(op, child): 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 ): + def setOption(self, op: BaseOption, scope: NodeScope = None ): """! @brief set option within the given scope. If unspecified the option will be overridden only for "this" Customizable i.e. AS """ @@ -214,9 +214,9 @@ def cmp_snd(a, b): self._config[opname] = res - def getRuntimeOptions(self, scope: Scope = None) -> List[BaseOption]: + def getRuntimeOptions(self, scope: NodeScope = None) -> List[BaseOption]: return [ o for o in self.getOptions(scope) if o.mode==OptionMode.RUN_TIME] - def getScopedRuntimeOptions(self, scope: Scope = None) -> List[Tuple[BaseOption,Scope]]: + def getScopedRuntimeOptions(self, scope: NodeScope = None) -> List[Tuple[BaseOption,NodeScope]]: scopts = self.getScopedOptions(scope) return [ (o,s) for o,s in scopts if o.mode==OptionMode.RUN_TIME] diff --git a/seedemu/core/Network.py b/seedemu/core/Network.py index aa9aad31..5193f20a 100644 --- a/seedemu/core/Network.py +++ b/seedemu/core/Network.py @@ -8,7 +8,7 @@ from .AddressAssignmentConstraint import AddressAssignmentConstraint, Assigner from .Visualization import Vertex from .Customizable import Customizable -from .Scope import Scope,NetScope, NetScopeTier, NetScopeType +from .Scope import NodeScope,NetScope, NetScopeTier, NetScopeType from typing import Dict, Tuple, List from .OptionUtil import OptionDomain @@ -69,7 +69,7 @@ def __init__(self, name: str, type: NetworkType, prefix: IPv4Network, aac: Addre self.__rap = None self.__ecp = None - def scope(self, domain: OptionDomain = None)-> Scope: + def scope(self, domain: OptionDomain = None)-> NodeScope: """return a Scope that is specific to this Network""" assert domain in [OptionDomain.NET, None], 'input error' @@ -144,7 +144,7 @@ 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' - from OptionRegistry import OptionRegistry + from .OptionRegistry import OptionRegistry if latency > 0: self.setOption(OptionRegistry().net_latency(latency)) if bandwidth > 0: diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index 246f3445..dad90aa5 100644 --- a/seedemu/core/Node.py +++ b/seedemu/core/Node.py @@ -292,7 +292,7 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): self.__note = None - def scope(self, domain: OptionDomain = None)-> Scope: + 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), diff --git a/seedemu/core/ScionAutonomousSystem.py b/seedemu/core/ScionAutonomousSystem.py index d3d781af..76161ad5 100644 --- a/seedemu/core/ScionAutonomousSystem.py +++ b/seedemu/core/ScionAutonomousSystem.py @@ -8,7 +8,7 @@ from .Emulator import Emulator from .enums import NodeRole from .Node import Node -from .Scope import Scope, NodeScopeTier +from .Scope import NodeScope, NodeScopeTier from .OptionUtil import OptionDomain class ScionASN: @@ -159,7 +159,7 @@ def __init__(self, asn: int, subnetTemplate: str = "10.{}.0.0/16"): self.__note = None self.__generateStaticInfoConfig = False - def scope(self, domain: OptionDomain = None)-> Scope: + 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 007cc2d3..8f96e674 100644 --- a/seedemu/core/Scope.py +++ b/seedemu/core/Scope.py @@ -75,7 +75,7 @@ def from_node(node: 'Node'): -class Scope(ABC): +class NodeScope(ABC): @abstractmethod def domain(self) -> OptionDomain: pass @@ -90,29 +90,29 @@ def tier(self) -> ScopeTier: def type(self)-> ScopeType: pass - def __eq__(self, other: 'Scope') -> bool: + def __eq__(self, other: 'NodeScope') -> bool: """Compares two Scope objects for equality.""" - assert isinstance(other, Scope) + assert isinstance(other, NodeScope) _, eq2= self._comparable(other) return eq2 @abstractmethod - def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: + def _comparable(self, other: 'NodeScope') -> Tuple[bool,bool]: pass @abstractmethod - def __lt__(self, other: 'Scope'): + def __lt__(self, other: 'NodeScope'): pass @abstractmethod - def __gt__(self, other: 'Scope'): + def __gt__(self, other: 'NodeScope'): pass @abstractmethod - def _intersection(self, other: 'Scope') -> 'Scope': + def _intersection(self, other: 'NodeScope') -> 'NodeScope': pass # for use with functools.cmp_to_key( Scope.collate ) @staticmethod - def collate(a: 'Scope', b: 'Scope') -> int: + def collate(a: 'NodeScope', b: 'NodeScope') -> int: #c,_= a._comparable(b) try: if a < b: @@ -128,7 +128,7 @@ def collate(a: 'Scope', b: 'Scope') -> int: # for DockerCompiler::generateEnvFile() # It would do the comparison solely on 'scope' (that is ASN) and ignore type -class NodeScope(Scope): +class NodeScope(NodeScope): """! @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) @@ -196,13 +196,13 @@ def node(self) -> Optional[str]: 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""" @@ -313,7 +313,7 @@ def _comparable(self, other: Scope) -> Tuple[bool,bool]: - def __gt__(self, other: Scope): + def __gt__(self, other: NodeScope): """!@brief defines scope hierarchy comparison i.e. broader more general(less specific) > more specific). i.e. LHS supserset RHS """ @@ -425,7 +425,7 @@ 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: Scope): + def __lt__(self, other: NodeScope): """!@brie fDefines scope hierarchy comparison (more specific < broader). i.e. LHS subset RHS """ @@ -579,7 +579,7 @@ def from_net(net: 'Network') -> 'NetScopeType': return NetScopeType.BRIDGE -class NetScope(Scope): +class NetScope(NodeScope): """! @brief strong type for the hierarchical scope of configuration settings. @@ -635,13 +635,13 @@ def net(self) -> Optional[str]: def scope(self) -> Optional[int]: return self._scope_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""" @@ -753,7 +753,7 @@ def _comparable(self, other: Scope) -> Tuple[bool,bool]: - def __gt__(self, other: Scope): + def __gt__(self, other: NodeScope): """!@brief defines scope hierarchy comparison i.e. broader more general(less specific) > more specific). i.e. LHS supserset RHS """ @@ -852,7 +852,7 @@ def __gt__(self, other: Scope): return False - def __lt__(self, other: Scope): + def __lt__(self, other: NodeScope): """!@brie fDefines scope hierarchy comparison (more specific < broader). i.e. LHS subset RHS """ diff --git a/tests/options/OptionsTestCase.py b/tests/options/OptionsTestCase.py index 71634b94..3d9928ef 100644 --- a/tests/options/OptionsTestCase.py +++ b/tests/options/OptionsTestCase.py @@ -4,7 +4,10 @@ import unittest as ut import os from seedemu.core import ( - Scope, + NodeScope, + NetScope, + NetScopeTier, + NetScopeType, NodeScopeTier, NodeScopeType, BaseOption, @@ -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(NodeScopeTier.Global) > Scope(NodeScopeTier.AS, as_id=150), 'global scope is superset of AS scopes') - self.assertTrue( Scope(NodeScopeTier.AS, as_id=150) < Scope(NodeScopeTier.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(NodeScopeTier.AS, as_id=150), - Scope(NodeScopeTier.AS, as_id=160))==0, 'disjoint AS scopes') - self.assertTrue( cmpr(Scope(NodeScopeTier.Node, as_id=150, node_id='br0'), - Scope(NodeScopeTier.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(NodeScopeTier.Node, as_id=150, node_id='br0'), - Scope(NodeScopeTier.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(NodeScopeTier.AS, as_id=150,node_type=NodeScopeType.HNODE), - Scope(NodeScopeTier.AS, as_id=150,node_type=NodeScopeType.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(NodeScopeTier.AS, as_id=150,node_type=NodeScopeType.HNODE), - Scope(NodeScopeTier.AS, as_id=160,node_type=NodeScopeType.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(NodeScopeTier.Global,node_type=NodeScopeType.HNODE), - Scope(NodeScopeTier.Global,node_type=NodeScopeType.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(NodeScopeTier.AS, as_id=150) > - Scope(NodeScopeTier.Node, as_id=150, node_id='brd0', node_type=NodeScopeType.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(NodeScopeTier.AS, as_id=150) < - Scope(NodeScopeTier.Node, as_id=150, node_id='brd0', node_type=NodeScopeType.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(NodeScopeTier.AS, as_id=160) == - Scope(NodeScopeTier.AS, as_id=160) , 'identical AS scope') - self.assertTrue( Scope(NodeScopeTier.AS, as_id=160, node_type=NodeScopeType.ANY) == - Scope(NodeScopeTier.AS, as_id=160) , 'identical AS scope') - self.assertTrue( Scope(NodeScopeTier.AS, as_id=160, node_type=NodeScopeType.ANY) > - Scope(NodeScopeTier.AS, as_id=160, node_type=NodeScopeType.BRDNODE), 'ANY includes all types') - self.assertTrue( Scope(NodeScopeTier.AS, as_id=150) != - Scope(NodeScopeTier.AS, as_id=160) , 'not identical scope') - self.assertTrue( Scope(NodeScopeTier.Node, as_id=150,node_id='brd0') == - Scope(NodeScopeTier.Node, as_id=150,node_id='brd0',node_type=NodeScopeType.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(NodeScopeTier.Global) > Scope(NodeScopeTier.Node, as_id=150, node_id='brd0')) - self.assertTrue( not (Scope(NodeScopeTier.Global, NodeScopeType.HNODE) > - Scope(NodeScopeTier.Node, as_id=150, node_id='brd0', node_type=NodeScopeType.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(NodeScopeTier. Global) - global_router_scope = Scope(NodeScopeTier. Global, NodeScopeType.RNODE) - as_router_scope = Scope(NodeScopeTier.AS, NodeScopeType.RNODE, as_id=42) - node_scope = Scope(NodeScopeTier.Node, NodeScopeType.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(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", Scope(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", Scope(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", Scope(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", Scope(NodeScopeTier.Node, NodeScopeType.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 From a172041dbdaa4f124fda911770795af33ac3904c Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Thu, 17 Apr 2025 12:21:31 +0200 Subject: [PATCH 6/8] fix --- seedemu/core/Customizable.py | 20 ++++++++++---------- seedemu/core/Scope.py | 34 +++++++++++++++++----------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/seedemu/core/Customizable.py b/seedemu/core/Customizable.py index d73d39a9..43c310a2 100644 --- a/seedemu/core/Customizable.py +++ b/seedemu/core/Customizable.py @@ -9,14 +9,14 @@ class Customizable(object): """! @brief something that can be configured by Options """ - _config: Dict[str,Tuple[BaseOption,NodeScope]] + _config: Dict[str,Tuple[BaseOption,Scope]] def __init__(self): # scope param. actually only for debug/tests , scope: Scope = None super().__init__() self._config = {} self._scope = None - def scope(self, domain: OptionDomain = None)-> NodeScope: + 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) @@ -27,7 +27,7 @@ def scope(self, domain: OptionDomain = None)-> NodeScope: else: return self._scope - def getScopedOption(self, key: str, scope: NodeScope = None, prefix: str = None) -> Optional[Tuple[BaseOption, NodeScope]]: + 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. """ from seedemu.core.OptionRegistry import OptionRegistry @@ -65,7 +65,7 @@ def getScopedOption(self, key: str, scope: NodeScope = None, prefix: str = None) return None - def getOption(self, key: str, scope: NodeScope = None, prefix: str = None ) -> Optional[BaseOption]: + def getOption(self, key: str, scope: Scope = None, prefix: str = None ) -> Optional[BaseOption]: """!@brief Retrieves an option(if set) based on the precedence rules (scoping). If not specified the option value for the scope most specific to 'this' customizable will be returned. @@ -80,7 +80,7 @@ def getOption(self, key: str, scope: NodeScope = None, prefix: str = None ) -> O else: return None - def _possible_scopes(scope: NodeScope) -> List[NodeScope]: + def _possible_scopes(scope: Scope) -> List[Scope]: if isinstance(scope, NodeScope): possible_scopes = [ NodeScope(NodeScopeTier.Node, scope._node_type, @@ -114,12 +114,12 @@ def _getKeys(self) -> List[str]: return list( self._config.keys()) # Tuple[ BaseOption, Scope ] or List[ScopedOption] where ScopedOption is just a wrapper around Tuple[BaseOption, Scope] - def getOptions(self, scope: NodeScope = None ) -> List[BaseOption]: + def getOptions(self, scope: Scope = None ) -> List[BaseOption]: """! @brief return all options included by the given scope. """ return [ self.getOption(k, scope) for k in self._getKeys() ] - def getScopedOptions(self, scope: NodeScope = None, prefix: str = None ) -> List[Tuple[BaseOption,NodeScope]]: + def getScopedOptions(self, scope: Scope = None, prefix: str = None ) -> List[Tuple[BaseOption,Scope]]: """! @brief return all options included by the given scope. """ return [ self.getScopedOption(k, scope) for k in self._getKeys() if (prefix != None and k.startswith(prefix)) or prefix==None ] @@ -154,7 +154,7 @@ def valid_down(op, child): 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: NodeScope = None ): + def setOption(self, op: BaseOption, scope: Scope = None ): """! @brief set option within the given scope. If unspecified the option will be overridden only for "this" Customizable i.e. AS """ @@ -214,9 +214,9 @@ def cmp_snd(a, b): self._config[opname] = res - def getRuntimeOptions(self, scope: NodeScope = None) -> List[BaseOption]: + def getRuntimeOptions(self, scope: Scope = None) -> List[BaseOption]: return [ o for o in self.getOptions(scope) if o.mode==OptionMode.RUN_TIME] - def getScopedRuntimeOptions(self, scope: NodeScope = None) -> List[Tuple[BaseOption,NodeScope]]: + def getScopedRuntimeOptions(self, scope: Scope = None) -> List[Tuple[BaseOption,Scope]]: scopts = self.getScopedOptions(scope) return [ (o,s) for o,s in scopts if o.mode==OptionMode.RUN_TIME] diff --git a/seedemu/core/Scope.py b/seedemu/core/Scope.py index 8f96e674..d54c8d4d 100644 --- a/seedemu/core/Scope.py +++ b/seedemu/core/Scope.py @@ -75,7 +75,7 @@ def from_node(node: 'Node'): -class NodeScope(ABC): +class Scope(ABC): @abstractmethod def domain(self) -> OptionDomain: pass @@ -90,29 +90,29 @@ def tier(self) -> ScopeTier: def type(self)-> ScopeType: pass - def __eq__(self, other: 'NodeScope') -> bool: + def __eq__(self, other: 'Scope') -> bool: """Compares two Scope objects for equality.""" - assert isinstance(other, NodeScope) + assert isinstance(other, Scope) _, eq2= self._comparable(other) return eq2 @abstractmethod - def _comparable(self, other: 'NodeScope') -> Tuple[bool,bool]: + def _comparable(self, other: 'Scope') -> Tuple[bool,bool]: pass @abstractmethod - def __lt__(self, other: 'NodeScope'): + def __lt__(self, other: 'Scope'): pass @abstractmethod - def __gt__(self, other: 'NodeScope'): + def __gt__(self, other: 'Scope'): pass @abstractmethod - def _intersection(self, other: 'NodeScope') -> 'NodeScope': + def _intersection(self, other: 'Scope') -> 'Scope': pass # for use with functools.cmp_to_key( Scope.collate ) @staticmethod - def collate(a: 'NodeScope', b: 'NodeScope') -> int: + def collate(a: 'Scope', b: 'Scope') -> int: #c,_= a._comparable(b) try: if a < b: @@ -128,7 +128,7 @@ def collate(a: 'NodeScope', b: 'NodeScope') -> int: # for DockerCompiler::generateEnvFile() # It would do the comparison solely on 'scope' (that is ASN) and ignore type -class NodeScope(NodeScope): +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) @@ -202,7 +202,7 @@ def _intersection(self, other: 'NodeScope') -> 'NodeScope': """ pass - def _comparable(self, other: NodeScope) -> 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""" @@ -313,7 +313,7 @@ def _comparable(self, other: NodeScope) -> Tuple[bool,bool]: - def __gt__(self, other: NodeScope): + def __gt__(self, other: 'NodeScope'): """!@brief defines scope hierarchy comparison i.e. broader more general(less specific) > more specific). i.e. LHS supserset RHS """ @@ -425,7 +425,7 @@ 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: NodeScope): + def __lt__(self, other: 'NodeScope'): """!@brie fDefines scope hierarchy comparison (more specific < broader). i.e. LHS subset RHS """ @@ -579,7 +579,7 @@ def from_net(net: 'Network') -> 'NetScopeType': return NetScopeType.BRIDGE -class NetScope(NodeScope): +class NetScope(Scope): """! @brief strong type for the hierarchical scope of configuration settings. @@ -635,13 +635,13 @@ def net(self) -> Optional[str]: def scope(self) -> Optional[int]: return self._scope_id - def _intersection(self, other: 'NodeScope') -> 'NodeScope': + def _intersection(self, other: 'NetScope') -> 'NodeScope': """! @brief return a new scope which represents the intersection of both scopes """ pass - def _comparable(self, other: NodeScope) -> Tuple[bool,bool]: + def _comparable(self, other: 'NetScope') -> Tuple[bool,bool]: """ returns a two bools indicating wheter the scopes are: lt/gt comparable or identical""" @@ -753,7 +753,7 @@ def _comparable(self, other: NodeScope) -> Tuple[bool,bool]: - def __gt__(self, other: NodeScope): + def __gt__(self, other: 'NetScope'): """!@brief defines scope hierarchy comparison i.e. broader more general(less specific) > more specific). i.e. LHS supserset RHS """ @@ -852,7 +852,7 @@ def __gt__(self, other: NodeScope): return False - def __lt__(self, other: NodeScope): + def __lt__(self, other: 'NetScope'): """!@brie fDefines scope hierarchy comparison (more specific < broader). i.e. LHS subset RHS """ From bf81b2e0f68c3ebefc844625cecfb52752063df4 Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Thu, 17 Apr 2025 12:36:23 +0200 Subject: [PATCH 7/8] fix --- seedemu/layers/Scion.py | 3 ++- seedemu/layers/ScionRouting.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/seedemu/layers/Scion.py b/seedemu/layers/Scion.py index 95326e8f..98feb6f9 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 79585d6a..8eeb3f8a 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 """ From 1ffc5d98da704f0d416d12c7b94c89e2c541fb7b Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Thu, 17 Apr 2025 13:11:32 +0200 Subject: [PATCH 8/8] fix --- seedemu/core/Node.py | 11 ++++++++++- seedemu/layers/Base.py | 7 +++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index dad90aa5..b7900873 100644 --- a/seedemu/core/Node.py +++ b/seedemu/core/Node.py @@ -365,7 +365,16 @@ def configure(self, emulator: Emulator): # 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/layers/Base.py b/seedemu/layers/Base.py index 94e17a63..311d28fa 100644 --- a/seedemu/layers/Base.py +++ b/seedemu/layers/Base.py @@ -113,17 +113,20 @@ def configure(self, emulator: Emulator): asobj.inheritOptions(emulator) self._log('setting up internet exchanges...') for ix in self.__ixes.values(): ix.configure(emulator) - # now all Networks are registered .. + # 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) + # this calls Node::configure() in turn and instantiates and registers XC networks - super().configure(emulator) + #super().configure(emulator) def render(self, emulator: Emulator) -> None: for ((scope, type, name), obj) in emulator.getRegistry().getAll().items():