Skip to content

Commit 1a9ea48

Browse files
committed
option domains
1 parent 81abf19 commit 1a9ea48

15 files changed

+203
-73
lines changed

seedemu/compiler/Docker.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22
from seedemu.core.Emulator import Emulator
3-
from seedemu.core import Node, Network, Compiler, BaseSystem, BaseOption, Scope, NodeScopeType, NodeScopeTier, OptionHandling, BaseVolume, OptionMode
3+
from seedemu.core import Node, Network, Compiler, BaseSystem, BaseOption, Scope, NodeScope, NodeScopeType, NodeScopeTier, OptionHandling, BaseVolume, OptionMode
44
from seedemu.core.enums import NodeRole, NetworkType
55
from .DockerImage import DockerImage
66
from .DockerImageConstant import *
@@ -1346,7 +1346,7 @@ def _doCompile(self, emulator: Emulator):
13461346
dummies = local_images + self._makeDummies()
13471347
), file=open('docker-compose.yml', 'w'))
13481348

1349-
self.generateEnvFile(Scope(NodeScopeTier.Global),'')
1349+
self.generateEnvFile(NodeScope(NodeScopeTier.Global),'')
13501350

13511351
def _computeComposeTopLvlVolumes(self) -> str:
13521352
"""!@brief render the 'volumes:' section of the docker-compose.yml file

seedemu/core/AutonomousSystem.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from .AddressAssignmentConstraint import AddressAssignmentConstraint
66
from .enums import NetworkType, NodeRole
77
from .Node import Node, Router
8-
from .Scope import NodeScopeTier, Scope
8+
from .Scope import NodeScopeTier, Scope, NodeScope, NetScope, NetScopeTier
9+
from .Option import OptionDomain
910
from .Emulator import Emulator
1011
from .Configurable import Configurable
1112
from .Customizable import Customizable
@@ -137,9 +138,13 @@ def inheritOptions(self, emulator: Emulator):
137138
for n in all_nets:
138139
self.handDown(n) # TODO: this also installs NodeOptions on the Net... which is no harm, but unnecessary
139140

140-
def scope(self)-> Scope:
141+
def scope(self, domain: OptionDomain = None)-> Scope:
141142
"""return a scope specific to this AS"""
142-
return Scope(NodeScopeTier.AS, as_id=self.getAsn())
143+
match domain:
144+
case OptionDomain.NODE:
145+
return NodeScope(NodeScopeTier.AS, as_id=self.getAsn())
146+
case OptionDomain.NET:
147+
return NetScope(NetScopeTier.Scoped, scope_id=self.getAsn())
143148

144149

145150
def configure(self, emulator: Emulator):

seedemu/core/Configurable.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def _prepare(self, emulator: Emulator):
5555
Override this method in your Layer if you want more targeted
5656
setting of Options i.e. only on border-routers or hosts etc..
5757
"""
58-
from .Scope import Scope, NodeScopeTier
58+
from .Scope import NodeScope, NodeScopeTier
5959

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

7070

7171
def configure(self, emulator: Emulator):

seedemu/core/Customizable.py

+58-20
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from functools import cmp_to_key
33
from typing import Optional, Dict, Tuple
44
from seedemu.core.Scope import *
5-
from seedemu.core.Option import BaseOption, OptionMode
5+
from seedemu.core.Option import BaseOption, OptionMode, OptionDomain
66

77
class Customizable(object):
88

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

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

2529

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

3136
keys = [key]
3237

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

7883
def _possible_scopes(scope: Scope) -> List[Scope]:
79-
possible_scopes = [
80-
Scope(NodeScopeTier.Node, scope._node_type,
81-
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
82-
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
83-
Scope(NodeScopeTier.AS, scope._node_type, as_id=scope._as_id) if scope._as_id and scope._node_type else None, # AS & Type
84-
Scope(NodeScopeTier.AS, NodeScopeType.ANY, as_id=scope._as_id) if scope._as_id else None, # AS-wide
85-
Scope(NodeScopeTier.Global, scope._node_type), # Global & Type
86-
Scope(NodeScopeTier.Global) # Global (fallback)
87-
]
84+
if isinstance(scope, NodeScope):
85+
possible_scopes = [
86+
NodeScope(NodeScopeTier.Node, scope._node_type,
87+
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
88+
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
89+
NodeScope(NodeScopeTier.AS, scope._node_type, as_id=scope._as_id) if scope._as_id and scope._node_type else None, # AS & Type
90+
NodeScope(NodeScopeTier.AS, NodeScopeType.ANY, as_id=scope._as_id) if scope._as_id else None, # AS-wide
91+
NodeScope(NodeScopeTier.Global, scope._node_type), # Global & Type
92+
NodeScope(NodeScopeTier.Global) # Global (fallback)
93+
]
94+
if isinstance(scope, NetScope):
95+
possible_scopes = [
96+
NetScope(NetScopeTier.Individual,net_type=scope.type, scope_id=scope.scope, net_id=scope.net )
97+
if scope.scope and scope.net and scope.type else None, # Net specific + type
98+
NetScope(NetScopeTier.Individual, scope_id=scope.scope, net_id=scope.net )
99+
if scope.scope and scope.net else None, # Net specific
100+
101+
NetScope(NetScopeTier.Scoped, scope.type, scope_id=scope.scope)
102+
if scope.scope and scope.type else None, # scope & Type
103+
NetScope(NetScopeTier.Scoped, NetScopeType.ANY, scope_id=scope.scope)
104+
if scope.scope else None, # scope-wide
105+
106+
NetScope(NetScopeTier.Global, net_type=scope.type),
107+
NetScope(NetScopeTier.Global)
108+
]
109+
88110
return possible_scopes
89111

90112
def _getKeys(self) -> List[str]:
@@ -108,15 +130,29 @@ def handDown(self, child: 'Customizable'):
108130
i.e. ASes are a collection of Nodes.
109131
This methods performs the inheritance of options from parent to child.
110132
"""
111-
112-
try: # scopes could be incomparable
113-
assert self.scope()>child.scope(), 'logic error - cannot inherit options from more general scopes'
114-
except :
115-
pass
133+
from .Network import Network
134+
from .Node import Node
135+
dom = OptionDomain.NET if type(child)== Network else OptionDomain.NODE
136+
s1=self.scope(dom)
137+
s2=child.scope(dom)
138+
139+
140+
assert not s1 < s2, 'logic error - cannot inherit options from more general scopes'
116141

117142
for k, val in self._config.items():
118143
for (op, s) in val:
119-
child.setOption(op, s)
144+
if self.valid_down(op,child):
145+
child.setOption(op, s)
146+
147+
@staticmethod
148+
def valid_down(op, child):
149+
from .Network import Network
150+
from .Node import Node
151+
# enforce option-domains only at the lowest granularity (customizable hierarchy): Nodes and Networks
152+
# Any aggregates of higher levels i.e. ASes, ISDs (that don't inherit neither Node nor Network)
153+
# can have Options of mixed domains
154+
return not ((op.optiondomain() == OptionDomain.NET and issubclass(type(child), Node)) or
155+
(issubclass( type(child), Network) and op.optiondomain()==OptionDomain.NODE ) )
120156

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

165+
assert self.valid_down(op, self), 'input error'
166+
129167
if not scope: scope = self.scope()
130168

131169
opname = op.name

seedemu/core/InternetExchange.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __init__(self, id: int, prefix: str = "auto", aac: AddressAssignmentConstrai
3636
network = IPv4Network(prefix) if prefix != "auto" else IPv4Network("10.{}.0.0/24".format(self.__id))
3737

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

4141
if create_rs:
4242
self.__rs = Router(self.__name, NodeRole.RouteServer, self.__id)

seedemu/core/Network.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .Customizable import Customizable
1111
from .Scope import Scope,NetScope, NetScopeTier, NetScopeType
1212
from typing import Dict, Tuple, List
13+
from .OptionUtil import OptionDomain
1314

1415
class Network(Printable, Registrable, Vertex, Customizable):
1516
"""!
@@ -68,10 +69,19 @@ def __init__(self, name: str, type: NetworkType, prefix: IPv4Network, aac: Addre
6869
self.__rap = None
6970
self.__ecp = None
7071

71-
def scope(self)-> Scope:
72+
def scope(self, domain: OptionDomain = None)-> Scope:
7273
"""return a Scope that is specific to this Network"""
73-
return NetScope(tier=NetScopeTier.Individual,
74-
net_type=NetScopeType.from_net(self),
74+
75+
assert domain in [OptionDomain.NET, None], 'input error'
76+
match (nt:=NetScopeType.from_net(self)):
77+
case NetScopeType.XC:
78+
return NetScope(tier=NetScopeTier.Individual,
79+
net_type=nt,
80+
scope_id=0, # scope of XC nets is None otherwise
81+
net_id=self.getName())
82+
case _:
83+
return NetScope(tier=NetScopeTier.Individual,
84+
net_type=nt,
7585
scope_id=int(self.__scope),
7686
net_id=self.getName())
7787

seedemu/core/Node.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .Registry import Registrable
99
from .Emulator import Emulator
1010
from .Customizable import Customizable
11+
from .OptionUtil import OptionDomain
1112
from .Volume import BaseVolume
1213
from .Configurable import Configurable
1314
from .enums import NetworkType
@@ -291,8 +292,9 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None):
291292
self.__note = None
292293

293294

294-
def scope(self)-> Scope:
295-
return Scope(NodeScopeTier.Node,
295+
def scope(self, domain: OptionDomain = None)-> Scope:
296+
assert domain in [OptionDomain.NODE, None], 'input error'
297+
return NodeScope(NodeScopeTier.Node,
296298
node_type=NodeScopeType.from_node(self),
297299
node_id=self.getName(),
298300
as_id=self.getAsn())
@@ -360,6 +362,7 @@ def configure(self, emulator: Emulator):
360362
else:
361363
# netname = 'as{}.{}_as{}.{}'.format(self.getAsn(), self.getName(), peerasn, peername)
362364
netname = ''.join(choice(ascii_letters) for i in range(10))
365+
# TOODO scope of XC nets ?! pair of both ASes .. ?!
363366
net = Network(netname, NetworkType.CrossConnect, localaddr.network, direct = False) # TODO: XC nets w/ direct flag?
364367
net.setDefaultLinkProperties(latency, bandwidth, packetDrop).setMtu(mtu) # Set link properties
365368
self.__joinNetwork(reg.register('xc', 'net', netname, net), str(localaddr.ip))

seedemu/core/Option.py

+33-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from enum import Flag, auto
21
from typing import List, Optional, Type, Any
3-
2+
from .OptionUtil import *
43

54

65
class AutoRegister():
@@ -74,17 +73,6 @@ def components_recursive(cls, prefix: str = None) -> Optional[List['BaseComponen
7473
opts.extend(c.components_recursive(prefix = f'{cls.getName()}_{c.getName()}'))
7574
return opts
7675

77-
class OptionMode(Flag):
78-
"""!@brief characteristics of an option,
79-
during which time it might be changed or set
80-
"""
81-
# static/hardcoded (require re-compile + image-rebuild to change)
82-
BUILD_TIME = auto()
83-
# i.e. envsubst (require only docker compose stop/start )
84-
RUN_TIME = auto()
85-
86-
87-
8876
class OptionGroupMeta(type): # or BaseComponentMeta ..
8977
"""Metaclass to auto-register nested options within a group."""
9078

@@ -112,6 +100,7 @@ def __new__(cls, name, bases, class_dict):
112100
# prefixed_name = f"{name}_{attr_value.name()}"
113101
# better call new_cls.add() # here
114102
new_cls._children[attr_value.name] = attr_value
103+
attr_value.domain = new_cls.optiondomain()
115104
# don't register nested options twice (but only once as child of the parent 'composite')
116105
if '.' not in qname or qname.startswith('SEEDEmuOptionSystemTestCase'):
117106
OptionRegistry().register(new_cls)
@@ -121,6 +110,17 @@ def __new__(cls, name, bases, class_dict):
121110
class BaseOption(BaseComponent, metaclass=OptionGroupMeta):
122111
"""! a base class for KEY-VALUE pairs representing Settings, Parameters or Feature Flags"""
123112

113+
domain: OptionDomain = None # set by the parent option container
114+
115+
@property
116+
def domain(self) -> OptionDomain:
117+
# if domain not specified by parent container,
118+
# user must implement optiondomain()
119+
if (d:= self.__class__.domain) != None:
120+
return d
121+
else:
122+
return self.optiondomain()
123+
124124
def __eq__(self, other):
125125
if not other: return False
126126

@@ -139,10 +139,15 @@ def value(self, new_value: str):
139139
"""Should allow setting a new value."""
140140
pass
141141

142+
@classmethod
143+
def optiondomain(cls) -> OptionDomain:
144+
return cls.domain
145+
142146
@property
143147
def mode(self)->OptionMode:
144148
"""Should return the mode of the option."""
145149
pass
150+
146151
@mode.setter
147152
def mode(self, new_mode: OptionMode):
148153
pass
@@ -161,6 +166,7 @@ class Option(BaseOption):
161166
"""
162167
# Immutable class variable to be defined in subclasses
163168
value_type: Type[Any]
169+
164170

165171
def __init__(self, value: Optional[Any] = None, mode: OptionMode = None):
166172
cls = self.__class__
@@ -229,6 +235,8 @@ def value(self, new_value: Any):
229235
assert new_value != None, 'Logic Error - option value cannot be None!'
230236
self._mutable_value = new_value
231237

238+
239+
232240
@property
233241
def mode(self):
234242
if (mode := self._mutable_mode) != None:
@@ -246,6 +254,12 @@ def description(cls) -> str:
246254
and its allowed values
247255
"""
248256
return cls.__doc__ or "No documentation available."
257+
258+
@classmethod
259+
def domain(cls) -> OptionDomain:
260+
""" the types of entities that a given Option
261+
can apply or refer to"""
262+
return OptionDomain.NODE
249263

250264

251265
#class ScopedOption:
@@ -257,6 +271,7 @@ def description(cls) -> str:
257271

258272
class BaseOptionGroup(BaseComponent , metaclass=OptionGroupMeta):
259273
_children = {}
274+
domain: OptionDomain = None
260275

261276

262277
def describe(self) -> str:
@@ -275,4 +290,8 @@ def get(self, option_name: str) -> Optional[BaseComponent]:
275290

276291
@classmethod
277292
def components(cls):
278-
return [v for _, v in cls._children.items()]
293+
return [v for _, v in cls._children.items()]
294+
295+
@classmethod
296+
def optiondomain(cls) -> OptionDomain:
297+
return cls.domain

seedemu/core/OptionRegistry.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, Type
1+
from typing import Dict, Type, Optional
22

33

44
class SingletonMeta(type):
@@ -51,6 +51,14 @@ def create_option(cls, name: str, *args, **kwargs) -> 'Option':
5151
# Instantiate with given arguments
5252
return option_cls(*args[1:], **kwargs)
5353

54+
@classmethod
55+
def getDomain(cls, key: str, prefix: str = None) -> Optional['OptionDomain']:
56+
o = cls.getType(key, prefix)
57+
if o:
58+
return o.optiondomain()
59+
else:
60+
return None
61+
5462

5563
@classmethod
5664
def getType(cls, name: str, prefix: str = None) -> Type['BaseComponent']:

0 commit comments

Comments
 (0)