Skip to content

Commit 31dbe7b

Browse files
committed
dev-service
1 parent 548b1ae commit 31dbe7b

File tree

10 files changed

+503
-17
lines changed

10 files changed

+503
-17
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
### Proposal: Remote Development Service
2+
3+
The DevelopmentService prepares a node for [Remote Development with VSCode](https://code.visualstudio.com/docs/remote/remote-overview).
4+
It installs the VSCode Remote Server on the Node during the docker image build, which allows to ssh into the running container later i.e. to debug software.
5+
As of now the primary anticipated use of the seed-emulator has been the deployment of fully developed applications into an emulated szenario i.e. to observe its behaviour.
6+
With the DevService this paradigm can be adapted to meed the need of developing i.e. P2P software to maturity in a distributed environment.
7+
8+
The DevService allows for each individual node that it is installed on, to specify one or more Git repositories that shall be checked out (along with the desired filesystem path and branch) as well as the VSCode Extensions required for the projects software stack (i.e. golang.Go for the SCION implementation which is written in Go)
9+
The DevService takes care to install the entirety of build and analysis tools that are needed for a given programming language at docker image build time (for Go this being i.e. the compiler, language server gopls, debugger delve) so that no annoying time delay arises once the emulator is running and you want to attach to a container.
10+
Any specified Git repositories are checked out on its own separate docker volume, for the changes to persist between runs of the simulator in case one forgets to push.
11+
12+
### Concerns:
13+
- software development requires a 'real internet' connection of the container be it to git push/pull or fetch project dependencies for a build (i.e. go get, cargo build etc. ) Currently this is achieved by adding the nodes default gateway ( router node ) in the simulation to a docker-bridge network which it shares only with one other node, the docker-host that acts as a default gateway for the router. The subnetmask for these 'micro' networks is deliberately kept as big as possible,
14+
to allow as least nodes as possible on it (ideally /30 for only: the router-node, docker-host , broadcast and network address )
15+
This is to inhibit 'short-circuiting' the simulated network topology (i.e. any crosstalk past the intended network topo among nodes)
16+
I tried an alternative approach of configuring these 'service-networks' as IPv6 [^1] to avoid confusion with the simulated IPv4 network and it worked just fine, but was nonetheless rendered impractical as github.com doesnt seem to support IPv6.
17+
18+
[^1]: requires some tinkering with the /etc/docker/daemon.json , setting the 'inet6' option in the /etc/resolv.conf so that any domain names resolve to AAAA records and setting a default ip -6 route
19+
20+
### TODO:
21+
- set up a script to install DROP rules in the POSTROUTING Chain of the docker hosts iptables to prevent crosstalk via the service network(s)
22+
- maybe its a better idea to have several subclasses of the server for different software stacks i.e. rust / Go /python etc. and leave only the basics i.e git in the service
23+
- I have to look into the VSCode CLI again !! Time delays when first attaching to a container ought to be kept small, and thus as much as possible should be done at image build time
24+
- I need to rework the way the PATH and other environment variables are reliably set in the containers ( it is annoying to always have to type i.e. 'export PATH=$PATH:/usr/local/go/bin')
25+
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env python3
2+
3+
from seedemu.compiler import Docker,Graphviz
4+
from seedemu.core import Emulator, Binding, Filter
5+
from seedemu.layers import ScionBase, ScionRouting, ScionIsd, Scion
6+
from seedemu.layers.Scion import LinkType as ScLinkType
7+
from seedemu.services import ContainerDevelopmentService
8+
9+
# Initialize
10+
emu = Emulator()
11+
base = ScionBase()
12+
routing = ScionRouting()
13+
scion_isd = ScionIsd()
14+
scion = Scion()
15+
devsvc = ContainerDevelopmentService()
16+
17+
# SCION ISDs
18+
base.createIsolationDomain(1)
19+
20+
# Internet Exchange
21+
base.createInternetExchange(100)
22+
23+
# AS-150
24+
as150 = base.createAutonomousSystem(150)
25+
scion_isd.addIsdAs(1, 150, is_core=True)
26+
as150.createNetwork('net0')
27+
as150.createControlService('cs1').joinNetwork('net0')
28+
as150_router = as150.createRouter('br0')
29+
as150_router.joinNetwork('net0').joinNetwork('ix100')
30+
as150_router.crossConnect(153, 'br0', '10.50.0.2/29')
31+
32+
# Create a host running the bandwidth test server
33+
as150.createHost('node150_0').joinNetwork('net0', address='10.150.0.30')
34+
35+
devsvc.install('dev_peer150_0').addVSCodeExtension('golang.Go').checkoutRepo('https://github.com/scionproto/scion','/home/root/repos/scion','master')
36+
37+
emu.addBinding(Binding('dev_peer150_0', filter=Filter(nodeName='node150_0', asn=150)))
38+
39+
40+
# AS-151
41+
as151 = base.createAutonomousSystem(151)
42+
scion_isd.addIsdAs(1, 151, is_core=True)
43+
as151.createNetwork('net0')
44+
as151.createControlService('cs1').joinNetwork('net0')
45+
as151.createRouter('br0').joinNetwork('net0').joinNetwork('ix100')
46+
47+
as151.createHost('node151_0').joinNetwork('net0', address='10.151.0.30')
48+
devsvc.install('dev_peer151_0').addVSCodeExtension('golang.Go').checkoutRepo('https://github.com/scionproto/scion','/home/root/repos/scion','master')
49+
50+
emu.addBinding(Binding('dev_peer151_0', filter=Filter(nodeName='node151_0', asn=151,allowBound=True)))
51+
52+
# AS-152
53+
as152 = base.createAutonomousSystem(152)
54+
scion_isd.addIsdAs(1, 152, is_core=True)
55+
as152.createNetwork('net0')
56+
as152.createControlService('cs1').joinNetwork('net0')
57+
as152.createRouter('br0').joinNetwork('net0').joinNetwork('ix100')
58+
59+
as152.createHost('node152_0').joinNetwork('net0', address='10.152.0.30')
60+
emu.addBinding(Binding('peer152_0', filter=Filter(nodeName='node152_0', asn=152)))
61+
62+
# AS-153
63+
as153 = base.createAutonomousSystem(153)
64+
scion_isd.addIsdAs(1, 153, is_core=False)
65+
scion_isd.setCertIssuer((1, 153), issuer=150)
66+
as153.createNetwork('net0')
67+
as153.createControlService('cs1').joinNetwork('net0')
68+
as153_router = as153.createRouter('br0')
69+
as153_router.joinNetwork('net0')
70+
as153_router.crossConnect(150, 'br0', '10.50.0.3/29')
71+
72+
as153.createHost('node153_0').joinNetwork('net0', address='10.153.0.30')
73+
emu.addBinding(Binding('peer153_0', filter=Filter(nodeName='node153_0', asn=153)))
74+
75+
# Inter-AS routing
76+
scion.addIxLink(100, (1, 150), (1, 151), ScLinkType.Core)
77+
scion.addIxLink(100, (1, 151), (1, 152), ScLinkType.Core)
78+
scion.addIxLink(100, (1, 152), (1, 150), ScLinkType.Core)
79+
scion.addXcLink((1, 150), (1, 153), ScLinkType.Transit)
80+
81+
# Rendering
82+
emu.addLayer(base)
83+
emu.addLayer(routing)
84+
emu.addLayer(scion_isd)
85+
emu.addLayer(scion)
86+
emu.addLayer(devsvc)
87+
88+
emu.render()
89+
90+
# Compilation
91+
emu.compile(Docker(), './output')
92+
emu.compile(Graphviz(), './output/graphs')

seedemu/compiler/Docker.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@
9696
}; done
9797
'''
9898

99+
DockerCompilerFileTemplates['dockerbridge'] ="""\
100+
{name}:
101+
ipam:
102+
config:
103+
- subnet: 172.29.0.{bid}/29
104+
"""
105+
106+
99107
DockerCompilerFileTemplates['compose'] = """\
100108
version: "3.4"
101109
services:
@@ -136,6 +144,8 @@
136144
{networks}{ports}{volumes}
137145
labels:
138146
{labelList}
147+
environment:
148+
{environment}
139149
"""
140150

141151
DockerCompilerFileTemplates['compose_label_meta'] = """\
@@ -853,6 +863,7 @@ def _compileNode(self, node: Node) -> str:
853863
netId = real_netname,
854864
address = address
855865
)
866+
node_nets += '\n'.join(node.getCustomNets())
856867

857868
_ports = node.getPorts()
858869
ports = ''
@@ -891,7 +902,20 @@ def _compileNode(self, node: Node) -> str:
891902
volumeList = lst
892903
)
893904

905+
name = self.__naming_scheme.format(
906+
asn = node.getAsn(),
907+
role = self._nodeRoleToString(node.getRole()),
908+
name = node.getName(),
909+
displayName = node.getDisplayName() if node.getDisplayName() != None else node.getName(),
910+
primaryIp = node.getInterfaces()[0].getAddress()
911+
)
912+
913+
name = sub(r'[^a-zA-Z0-9_.-]', '_', name)
914+
894915
dockerfile = DockerCompilerFileTemplates['dockerfile']
916+
917+
dockerfile += 'ENV CONTAINER_NAME {}\n'.format(name)
918+
895919
mkdir(real_nodename)
896920
chdir(real_nodename)
897921

@@ -912,6 +936,7 @@ def _compileNode(self, node: Node) -> str:
912936
dockerfile = 'FROM {}\n'.format(md5(image.getName().encode('utf-8')).hexdigest()) + dockerfile
913937
self._used_images.add(image.getName())
914938

939+
for cmd in node.getDockerCommands(): dockerfile += '{}\n'.format(cmd)
915940
for cmd in node.getBuildCommands(): dockerfile += 'RUN {}\n'.format(cmd)
916941

917942
start_commands = ''
@@ -945,20 +970,10 @@ def _compileNode(self, node: Node) -> str:
945970
dockerfile += self._importFile(cpath, hpath)
946971

947972
dockerfile += 'CMD ["/start.sh"]\n'
948-
print(dockerfile, file=open('Dockerfile', 'w'))
949973

974+
print(dockerfile, file=open('Dockerfile', 'w'))
950975
chdir('..')
951976

952-
name = self.__naming_scheme.format(
953-
asn = node.getAsn(),
954-
role = self._nodeRoleToString(node.getRole()),
955-
name = node.getName(),
956-
displayName = node.getDisplayName() if node.getDisplayName() != None else node.getName(),
957-
primaryIp = node.getInterfaces()[0].getAddress()
958-
)
959-
960-
name = sub(r'[^a-zA-Z0-9_.-]', '_', name)
961-
962977
return DockerCompilerFileTemplates['compose_service'].format(
963978
nodeId = real_nodename,
964979
nodeName = name,
@@ -967,7 +982,8 @@ def _compileNode(self, node: Node) -> str:
967982
# privileged = 'true' if node.isPrivileged() else 'false',
968983
ports = ports,
969984
labelList = self._getNodeMeta(node),
970-
volumes = volumes
985+
volumes = volumes,
986+
environment= " - CONTAINER_NAME={}\n ".format(name) + '\n '.join(node.getCustomEnv())
971987
)
972988

973989
def _compileNet(self, net: Network) -> str:
@@ -1088,6 +1104,11 @@ def _doCompile(self, emulator: Emulator):
10881104
dirName = image.getDirName()
10891105
)
10901106

1107+
bridges = registry.getByType('global','dockerbridge')
1108+
for b in bridges:
1109+
id = b.getAttribute('id')
1110+
self.__networks += DockerCompilerFileTemplates['dockerbridge'].format(name = b.getAttribute('name'), bid = str(4+ 4*int(id) ) )
1111+
10911112
self._log('creating docker-compose.yml...'.format(scope, name))
10921113
print(DockerCompilerFileTemplates['compose'].format(
10931114
services = self.__services,

seedemu/core/Network.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@
88
from .Visualization import Vertex
99
from typing import Dict, Tuple, List
1010

11+
12+
class DockerBridge(Registrable):
13+
"""
14+
@brief currently only used as a means to connect simulation-node router containers
15+
(and through it any nodes on its 'local network') to the docker hosts internet connection
16+
17+
the subnetmask for this 'micro' network is deliberately kept as big as possible,
18+
to allow as least nodes as possible on it (ideally /30 for only: the router-node, docker-host , broadcast and network address )
19+
This is to inhibit 'short-circuiting' the simulated network topology (i.e. crosstalk past the intended network topo among nodes)
20+
"""
21+
def __init__(self,n: str,id: int ):
22+
23+
super().__init__()
24+
super().doRegister('undefined', 'undefined', n)
25+
super().setAttribute('name', n)
26+
super().setAttribute('id',id)
27+
28+
1129
class Network(Printable, Registrable, Vertex):
1230
"""!
1331
@brief The network class.

seedemu/core/Node.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,18 +216,22 @@ class Node(Printable, Registrable, Configurable, Vertex):
216216
__imported_files: Dict[str, str]
217217
__softwares: Set[str]
218218
__build_commands: List[str]
219+
__docker_cmds: List[str]
219220
__start_commands: List[Tuple[str, bool]]
220221
__ports: List[Tuple[int, int, str]]
221222
__privileged: bool
222223

223224
__configured: bool
224225
__pending_nets: List[Tuple[str, str]]
225226
__xcs: Dict[Tuple[str, int], Tuple[IPv4Interface, str]]
226-
227+
__custom_nets: List[str]
228+
__custom_env: List[str]
227229
__shared_folders: Dict[str, str]
228230
__persistent_storages: List[str]
229231

230232
__name_servers: List[str]
233+
# wether this node requires to have 'real' internet access via its gateway
234+
__reach_outside: bool
231235

232236
def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None):
233237
"""!
@@ -251,6 +255,7 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None):
251255
self.__scope = scope if scope != None else str(asn)
252256
self.__softwares = set()
253257
self.__build_commands = []
258+
self.__docker_cmds = []
254259
self.__start_commands = []
255260
self.__ports = []
256261
self.__privileged = False
@@ -262,11 +267,21 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None):
262267

263268
self.__shared_folders = {}
264269
self.__persistent_storages = []
270+
self.__custom_nets = []
271+
self.__custom_env = []
265272

266273
# for soft in DEFAULT_SOFTWARE:
267274
# self.__softwares.add(soft)
268275

269276
self.__name_servers = []
277+
self.__reach_outside =False
278+
279+
def requestReachOutside(self):
280+
self.__reach_outside = True
281+
return self
282+
283+
def reachesOutside(self):
284+
return self.__reach_outside
270285

271286
def configure(self, emulator: Emulator):
272287
"""!
@@ -344,6 +359,35 @@ def setNameServers(self, servers: List[str]) -> Node:
344359
self.__name_servers = servers
345360

346361
return self
362+
363+
def setCustomNet(self, net: str):
364+
"""
365+
@param net a network that the node shall join (in docker-compose syntax )
366+
i.e. :
367+
net_153_net0:
368+
ipv4_address: 10.153.0.254
369+
it will be inserted under the nodes 'networks:' section in the .yml file
370+
"""
371+
self.__custom_nets.append(net)
372+
return self
373+
374+
def getCustomNets(self):
375+
return self.__custom_nets
376+
377+
def setCustomEnv(self, env: str ):
378+
"""
379+
@param env an environment variable that docker-compose shall pass to the Dockerfile
380+
381+
it gets inserted into the 'environment:' section of the node
382+
i.e.:
383+
environment:
384+
- DEBUG=${DEBUG}
385+
"""
386+
self.__custom_env.append(env)
387+
return self
388+
389+
def getCustomEnv(self):
390+
return self.__custom_env
347391

348392
def getNameServers(self) -> List[str]:
349393
"""!
@@ -478,6 +522,12 @@ def joinNetwork(self, netname: str, address: str = "auto") -> Node:
478522
self.__pending_nets.append((netname, address))
479523

480524
return self
525+
526+
def getNetNames(self):
527+
"""
528+
@brief list of names of networks this node has joined
529+
"""
530+
return [tuple[0] for tuple in self.__pending_nets]
481531

482532
def updateNetwork(self, netname:str, address: str= "auto") -> Node:
483533
"""!
@@ -712,6 +762,18 @@ def addBuildCommand(self, cmd: str) -> Node:
712762
self.__build_commands.append(cmd)
713763

714764
return self
765+
766+
def addDockerCommand(self, cmd: str) -> Node:
767+
"""!
768+
@brief Add new docker command to build step (possibly of kind other than RUN).
769+
Unlike the ones added with addBuildCommands these commands wont be prefixed with RUN,
770+
but are assumed to be valid Docker Commands by themselfes
771+
"""
772+
self.__docker_cmds.append(cmd)
773+
return self
774+
775+
def getDockerCommands(self)-> List[str]:
776+
return self.__docker_cmds
715777

716778
def getBuildCommands(self) -> List[str]:
717779
"""!
@@ -927,6 +989,26 @@ class Router(Node):
927989
"""
928990

929991
__loopback_address: str
992+
__external_bridge_id: int = -1 # -1 means not externally connected
993+
994+
bridge_cnt: int = 0
995+
996+
def isConnectedExternal(self):
997+
"""
998+
@brief externally connected routers can reach the internet outside the simulation via the docker host
999+
1000+
a 'Local Network' with at least one host that requires to 'reach_outside'
1001+
needs to have an externally-connected router to do so
1002+
"""
1003+
return self.__external_bridge_id
1004+
1005+
def setConnectedExternal(self):
1006+
1007+
if self.__external_bridge_id != -1: return self
1008+
1009+
self.__external_bridge_id = Router.bridge_cnt
1010+
Router.bridge_cnt += 1
1011+
return self
9301012

9311013
def setLoopbackAddress(self, address: str):
9321014
"""!

0 commit comments

Comments
 (0)