Skip to content

Commit ccbed92

Browse files
authored
Merge pull request #287 from amdfxlucas/seed-contrib10
/etc/scion config volume option
2 parents ed6ef82 + fd9e49a commit ccbed92

File tree

7 files changed

+119
-42
lines changed

7 files changed

+119
-42
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
**/.shared/*
12
**/output/*
23
**/.scion_build_output/*
34
**/eth-states*/*

examples/scion/S02_scion_bgp_mixed/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ spec = SetupSpecification.LOCAL_BUILD(
2525
checkout = "v0.12.0" # could be tag, branch or commit-hash
2626
))
2727
opt_spec = OptionRegistry().scion_setup_spec(spec)
28-
routing = ScionRouting(loglevel=loglvl, setup_spec=opt_spec)
28+
etc_cfg = OptionRegistry().scion_etc_config_vol(ScionConfigMode.SHARED_FOLDER)
29+
routing = ScionRouting(loglevel=loglvl, setup_spec=opt_spec, etc_config_vol=etc_cfg)
2930

3031
ospf = Ospf()
3132
scion_isd = ScionIsd()
@@ -37,6 +38,8 @@ ebgp = Ebgp()
3738
In addition to the SCION layers we instantiate the `Ibgp` and `Ebgp` layers for BGP as well as `Ospf` for AS-internal routing.
3839
Note that the ScionRouting layer accepts global default values for options as constructor parameters.
3940
We use it here to decrease the loglevel from 'debug' to 'error' for all SCION distributables in the whole emulation if not overriden elsewhere. Also we change the mode for `LOGLEVEL` from `BUILD_TIME`(default) to `RUN_TIME` in the same statement.
41+
Moreover, the SCION related configuration of nodes shall not be baked into their docker images for this example,
42+
but bind-mounted from a shared folder on the host instead, to facilitate re-configuration of the SCION stack between simulation runs(without image recompile).
4043
Lastly we specify (as a global default for all nodes) that we want to use a local build of the `v0.12.0` SCION stack, rather than the 'official' `.deb` packages (`SetupSpecification.PACKAGES`). The SetupSpec is just an ordinary option and be overriden for ASes or individual nodes just like any other.
4144

4245
## Step 2: Create isolation domains and internet exchanges

examples/scion/S02_scion_bgp_mixed/scion_bgp_mixed.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from seedemu.layers import (
66
ScionBase, ScionRouting, ScionIsd, Scion, Ospf, Ibgp, Ebgp, PeerRelationship,
77
SetupSpecification, CheckoutSpecification)
8-
from seedemu.layers.Scion import LinkType as ScLinkType
8+
from seedemu.layers.Scion import LinkType as ScLinkType, ScionConfigMode
9+
910
# Initialize
1011
emu = Emulator()
1112
base = ScionBase()
@@ -19,7 +20,8 @@
1920
checkout = "v0.12.0" # could be tag, branch or commit-hash
2021
))
2122
opt_spec = OptionRegistry().scion_setup_spec(spec)
22-
routing = ScionRouting(loglevel=loglvl, setup_spec=opt_spec)
23+
etc_cfg = OptionRegistry().scion_etc_config_vol(ScionConfigMode.SHARED_FOLDER)
24+
routing = ScionRouting(loglevel=loglvl, setup_spec=opt_spec, etc_config_vol=etc_cfg)
2325

2426
ospf = Ospf()
2527
scion_isd = ScionIsd()

seedemu/core/OptionRegistry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def create_option(cls, name: str, *args, **kwargs) -> 'Option':
4747
"""Creates an option instance if it's registered."""
4848
option_cls = cls._options.get(name)
4949
if not option_cls:
50-
raise ValueError(f"Option '{name}' is not registered.")
50+
raise ValueError(f'Option "{name}" is not registered.')
5151
# Instantiate with given arguments
5252
return option_cls(*args[1:], **kwargs)
5353

seedemu/layers/Scion.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from sys import version
1111
from seedemu.core import (Emulator, Interface, Layer, Network, Registry,
1212
Router, ScionAutonomousSystem, ScionRouter,
13-
ScopedRegistry, Graphable, Node)
13+
ScopedRegistry, Graphable, Node,
14+
Option, AutoRegister, OptionMode)
1415
from seedemu.core.ScionAutonomousSystem import IA
1516
from seedemu.layers import ScionBase, ScionIsd
1617
import shutil
@@ -63,6 +64,56 @@ def to_json(self, core_as: bool, is_parent: bool) -> str:
6364
assert not core_as, 'Logic error: Core ASes must only provide transit to customers, not receive it!'
6465
return "PARENT"
6566

67+
class ScionConfigMode(Enum):
68+
"""how shall the /etc/scion config dir contents be handled:"""
69+
70+
# statically include config in docker image (ship together)
71+
# this is fine, if no reconfiguration is required or the images
72+
# must be self contained i.e. for docker swarm deployment
73+
BAKED_IN = 0
74+
# mount shared folder from host into /etc/scion path of node
75+
# this saves considerable image build time and eases reconfiguration
76+
SHARED_FOLDER = 1
77+
# create named volumes for each container
78+
NAMED_VOLUME = 2
79+
# TODO all hosts of an AS could in theory share the same volume/bind-mount..
80+
# PER_AS_SHARING # this would only be possible for keys/crypto but not config files
81+
82+
# NOTE this option is used by ScionRouting and ScionIsd layers
83+
# and can be specified in ScionRouting constructor
84+
class SCION_ETC_CONFIG_VOL(Option, AutoRegister):
85+
""" this option controls the policy
86+
where to put all the SCION related files on the host.
87+
"""
88+
value_type = ScionConfigMode
89+
@classmethod
90+
def supportedModes(cls) -> OptionMode:
91+
return OptionMode.BUILD_TIME
92+
@classmethod
93+
def default(cls):
94+
return ScionConfigMode.BAKED_IN
95+
96+
97+
def handleScionConfFile( node, filename: str, filecontent: str, subdir: str = None):
98+
""" wrapper around 'Node::setFile' for /etc/scion config files
99+
@param subdir sub path relative to /etc/scion
100+
"""
101+
if (opt := node.getOption('scion_etc_config_vol')) != None:
102+
suffix = f'/{subdir}' if subdir != None else ''
103+
match opt.value:
104+
case ScionConfigMode.SHARED_FOLDER:
105+
current_dir = os.getcwd()
106+
path = os.path.join(current_dir,
107+
f'.shared/{node.getAsn()}/{node.getName()}/etcscion{suffix}')
108+
os.makedirs(path, exist_ok=True)
109+
with open(os.path.join(path, filename), "w") as file:
110+
file.write(filecontent)
111+
case _:
112+
#case ScionConfigMode.BAKED_IN:
113+
node.setFile(f"/etc/scion{suffix}/{filename}", filecontent)
114+
#case ScionConfigMode.NAMED_VOLUME: will be populated on fst mount
115+
else:
116+
assert False, 'implementation error - lacking global default for option'
66117

67118

68119
@dataclass
@@ -155,10 +206,11 @@ def __init__(self):
155206
pass
156207

157208
def installSCION(self, node: Node):
158-
"""!
209+
"""!@brief install required SCION distributables
210+
(network stack) on the given node
159211
Installs the right SCION stack distributables on the given node based on its role.
160-
161-
The install is performed as instructed by the nodes SetupSpec option
212+
But doesn't configure them ( /etc/scion config dir is untouched by it)
213+
The install is performed as instructed by the nodes SetupSpec option.
162214
"""
163215
spec = node.getOption('setup_spec')
164216
assert spec != None, 'implementation error - all nodes are supposed to have a SetupSpecification set by ScionRoutingLayer'

seedemu/layers/ScionIsd.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from seedemu.core import Emulator, Layer, Node, ScionAutonomousSystem
99
from seedemu.core.ScionAutonomousSystem import IA
1010
from seedemu.layers import ScionBase
11+
from seedemu.layers.Scion import handleScionConfFile
1112

1213

1314
class ScionIsd(Layer): # could be made a Customizable as well ..
@@ -202,10 +203,10 @@ def __gen_topofile(self, base_layer: ScionBase, tempdir: str) -> str:
202203
return path
203204

204205
def __provision_crypto(self, as_: ScionAutonomousSystem, isd: int, is_core: bool, node: Node, tempdir: str):
205-
basedir = "/etc/scion"
206+
206207
asn = as_.getScionAsn()
207208

208-
def copyFile(src, dst):
209+
def copyFile(src: str, filename: str, subdir: str = None):
209210
# Tempdir will be gone when imports are resolved, therefore we must use setFile
210211
with open(src, 'rt', encoding='utf8') as file:
211212
content = file.read()
@@ -215,30 +216,33 @@ def copyFile(src, dst):
215216
# that is later added again.
216217
if content.endswith('\n'):
217218
content = content[:-1]
218-
node.setFile(dst, content)
219+
handleScionConfFile(node, filename, content, subdir)
219220

220-
def myImport(name):
221-
copyFile(pjoin(tempdir, f"AS{asn.getFileStr()}", "crypto", name), pjoin(basedir, "crypto", name))
221+
def myImport(name, subdir: str):
222+
path = pjoin("crypto", subdir)
223+
copyFile(pjoin(tempdir, f"AS{asn.getFileStr()}", path, name),
224+
name, path)
222225

223226
if is_core:
224227
for kind in ["sensitive", "regular"]:
225-
myImport(pjoin("voting", f"ISD{isd}-AS{asn.getFileStr()}.{kind}.crt"))
226-
myImport(pjoin("voting", f"{kind}-voting.key"))
227-
myImport(pjoin("voting", f"{kind}.tmpl"))
228+
myImport(f"ISD{isd}-AS{asn.getFileStr()}.{kind}.crt", "voting")
229+
myImport(f"{kind}-voting.key", "voting")
230+
myImport(f"{kind}.tmpl", "voting")
228231
for kind in ["root", "ca"]:
229-
myImport(pjoin("ca", f"ISD{isd}-AS{asn.getFileStr()}.{kind}.crt"))
230-
myImport(pjoin("ca", f"cp-{kind}.key"))
231-
myImport(pjoin("ca", f"cp-{kind}.tmpl"))
232-
myImport(pjoin("as", f"ISD{isd}-AS{asn.getFileStr()}.pem"))
233-
myImport(pjoin("as", "cp-as.key"))
234-
myImport(pjoin("as", "cp-as.tmpl"))
232+
myImport(f"ISD{isd}-AS{asn.getFileStr()}.{kind}.crt", "ca")
233+
myImport(f"cp-{kind}.key", "ca")
234+
myImport(f"cp-{kind}.tmpl", "ca")
235+
myImport(f"ISD{isd}-AS{asn.getFileStr()}.pem", "as")
236+
myImport("cp-as.key", "as")
237+
myImport("cp-as.tmpl", "as")
235238

236239
#XXX(benthor): trcs need to be known for other isds as well
237240
for isd in self.__isd_core.keys():
238241
trcname = f"ISD{isd}-B1-S1.trc"
239-
copyFile(pjoin(tempdir, f"ISD{isd}", "trcs", trcname), pjoin(basedir, "certs", trcname))
242+
copyFile(pjoin(tempdir, f"ISD{isd}", "trcs", trcname),
243+
trcname, 'certs')
240244

241245
# Master keys are generated only once per AS
242246
key0, key1 = as_.getSecretKeys()
243-
node.setFile(pjoin(basedir, "keys", "master0.key"), key0)
244-
node.setFile(pjoin(basedir, "keys", "master1.key"), key1)
247+
handleScionConfFile(node, 'master0.key', key0, 'keys')
248+
handleScionConfFile(node, 'master1.key', key1, 'keys')

seedemu/layers/ScionRouting.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from seedemu.core.enums import NetworkType
1414
from seedemu.core.ScionAutonomousSystem import IA
1515
from seedemu.layers import Routing, ScionBase, ScionIsd
16-
from seedemu.layers.Scion import Scion, ScionBuilder, SetupSpecification, CheckoutSpecification
16+
from seedemu.layers.Scion import Scion, ScionBuilder, SetupSpecification, CheckoutSpecification, ScionConfigMode, handleScionConfFile
1717
from seedemu.core.ScionAutonomousSystem import IA
1818

1919

@@ -220,9 +220,9 @@ class ScionRouting(Routing):
220220
# >> Maybe include 'NodeType that the option applies to' into the Option itself
221221
def getAvailableOptions(self):
222222
from seedemu.core.OptionRegistry import OptionRegistry
223-
opt_keys = [ o.name for o in ScionStackOpts().components()]
224-
opt = [OptionRegistry().getOption(o, prefix='scion') for o in opt_keys]
225-
return opt
223+
opt_keys = [ o.name for o in ScionStackOpts().components()] + ['etc_config_vol']
224+
return [OptionRegistry().getOption(o, prefix='scion') for o in opt_keys]
225+
226226

227227
def __init__(self, loopback_range: str = '10.0.0.0/16',
228228
static_routing: bool = True,
@@ -233,7 +233,8 @@ def __init__(self, loopback_range: str = '10.0.0.0/16',
233233
experimental_scmp: BaseOption = None,
234234
appropriate_digest: BaseOption = None,
235235
serve_metrics: BaseOption = None,
236-
setup_spec: BaseOption = None ):
236+
setup_spec: BaseOption = None,
237+
etc_config_vol: BaseOption = None ):
237238
"""!
238239
@param static_routing install and configure BIRD routing daemon only on routers
239240
which are connected to more than one local-net (actual intra-domain routers).
@@ -259,7 +260,7 @@ def __init__(self, loopback_range: str = '10.0.0.0/16',
259260
option_names = [name for name in args
260261
if (vals[name] is not None) and
261262
name not in ['self', 'static_routing', 'loopback_range'] ]
262-
assert not any([ vals[name].name != name for name in option_names]), 'option-parameter mismatch!'
263+
assert not any([ vals[name].name != name and not vals[name].name.endswith(name) for name in option_names]), 'option-parameter mismatch!'
263264
ScionRouting._static_routing = static_routing
264265

265266
# let user override the global default options
@@ -355,7 +356,7 @@ def configure(self, emulator: Emulator):
355356
@brief Install SCION on router, control service and host nodes.
356357
"""
357358
if ScionRouting._static_routing:
358-
Layer.configure(self,emulator)
359+
Layer.configure(self, emulator)
359360
# install BIRD routing daemon only where necessary
360361
bird_routers = self.configure_base(emulator)
361362
for br in bird_routers:
@@ -414,6 +415,18 @@ def configure(self, emulator: Emulator):
414415
self.__install_scion(hnode)
415416
self.__append_scion_command(hnode)
416417

418+
if (cfg_vol := obj.getOption('scion_etc_config_vol')) != None:
419+
node: Node = obj
420+
match cfg_vol.value:
421+
# case ScionConfigMode.BAKED_IN:
422+
case ScionConfigMode.SHARED_FOLDER:
423+
current_dir = os.getcwd()
424+
node.addSharedFolder('/etc/scion',
425+
os.path.join(current_dir, f'.shared/{node.getAsn()}/{node.getName()}/etcscion'))
426+
case ScionConfigMode.NAMED_VOLUME:
427+
node.addPersistentStorage('/etc/scion',
428+
f'etcscion_{node.getAsn()}-{node.getName()}')
429+
417430
def __install_scion(self, node: Node):
418431
"""Install SCION stack on the node."""
419432

@@ -459,8 +472,9 @@ def render(self, emulator: Emulator):
459472

460473
# Install AS topology file
461474
as_topology = as_.getTopology(isds[0][0])
462-
node.setFile("/etc/scion/topology.json", json.dumps(as_topology, indent=2))
475+
topo = json.dumps(as_topology, indent=2)
463476

477+
handleScionConfFile(node, 'topology.json', topo)
464478
self._provision_base_config(node)
465479

466480
if type == 'brdnode':
@@ -490,7 +504,7 @@ def _provision_base_config(node: Node):
490504
if node.getOption('serve_metrics').value=='true':
491505
sciond_conf += _Templates["metrics"].format(node.getLocalIPAddress(), 30455)
492506
# No [features] for daemon
493-
node.setFile("/etc/scion/sciond.toml", sciond_conf)
507+
handleScionConfFile(node, 'sciond.toml', sciond_conf)
494508

495509
@staticmethod
496510
def __provision_dispatcher_config(node: Node, isd: int, as_: ScionAutonomousSystem):
@@ -515,7 +529,7 @@ def __provision_dispatcher_config(node: Node, isd: int, as_: ScionAutonomousSyst
515529
dispatcher_conf = _Templates["dispatcher"].format(isd_as=isd_as, ip=ip)
516530
if node.getOption('serve_metrics').value == 'true':
517531
dispatcher_conf += _Templates["metrics"].format(node.getLocalIPAddress(), 30441)
518-
node.setFile("/etc/scion/dispatcher.toml", dispatcher_conf )
532+
handleScionConfFile(node, 'dispatcher.toml', dispatcher_conf)
519533

520534
@staticmethod
521535
def _provision_router_config(router: ScionRouter):
@@ -537,7 +551,7 @@ def _provision_router_config(router: ScionRouter):
537551
if router.getOption('serve_metrics').value == 'true' and (local_ip:=router.getLocalIPAddress()) != None:
538552
config_content += _Templates["metrics"].format(local_ip, 30442)
539553

540-
router.setFile(os.path.join("/etc/scion/", name + ".toml"), config_content)
554+
handleScionConfFile(router, name + ".toml", config_content)
541555

542556
@staticmethod
543557
def _get_networks_from_router(router1 : str, router2 : str, as_ : ScionAutonomousSystem) -> list[Network]:
@@ -740,8 +754,8 @@ def _provision_staticInfo_config(node: Node, as_: ScionAutonomousSystem):
740754
if as_.getNote():
741755
staticInfo["Note"] = as_.getNote()
742756

743-
# Set file
744-
node.setFile("/etc/scion/staticInfoConfig.json", json.dumps(staticInfo, indent=2))
757+
handleScionConfFile(node, 'staticInfoConfig.json',
758+
json.dumps(staticInfo, indent=2))
745759

746760
@staticmethod
747761
def _provision_cs_config(node: Node, as_: ScionAutonomousSystem):
@@ -759,9 +773,10 @@ def _provision_cs_config(node: Node, as_: ScionAutonomousSystem):
759773
for type in ["propagation", "core_registration", "up_registration", "down_registration"]:
760774
policy = as_.getBeaconingPolicy(type)
761775
if policy is not None:
762-
file_name = f"/etc/scion/{type}_policy.yaml"
763-
node.setFile(file_name, yaml.dump(policy, indent=2))
764-
beaconing.append(f'{type} = "{file_name}"')
776+
file_name = f"{type}_policy.yaml"
777+
handleScionConfFile(node, file_name,
778+
yaml.dump(policy, indent=2) )
779+
beaconing.append(f'{type} = "/etc/scion/{file_name}"')
765780

766781
# Concatenate configuration sections
767782
name = node.getName()
@@ -778,4 +793,4 @@ def _provision_cs_config(node: Node, as_: ScionAutonomousSystem):
778793
if node.getOption('serve_metrics').value == 'true':
779794
cs_config += _Templates["metrics"].format(node.getLocalIPAddress(), 30452)
780795
cs_config += "\n".join(beaconing)
781-
node.setFile(os.path.join("/etc/scion/", name + ".toml"), cs_config)
796+
handleScionConfFile(node, name + '.toml', cs_config)

0 commit comments

Comments
 (0)