From 45638fe2f363cf6989ab7f85cde0aea83950888a Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Thu, 18 Jan 2024 01:02:57 +0100 Subject: [PATCH 1/2] dev-service --- examples/scion/S06-dev-service/README.md | 25 ++ examples/scion/S06-dev-service/dev-service.py | 92 ++++++++ seedemu/compiler/Docker.py | 45 +++- seedemu/core/Network.py | 36 +++ seedemu/core/Node.py | 84 ++++++- seedemu/core/Registry.py | 3 +- seedemu/core/__init__.py | 2 +- seedemu/layers/Routing.py | 29 ++- seedemu/services/DevService.py | 219 ++++++++++++++++++ seedemu/services/__init__.py | 3 +- 10 files changed, 521 insertions(+), 17 deletions(-) create mode 100644 examples/scion/S06-dev-service/README.md create mode 100644 examples/scion/S06-dev-service/dev-service.py create mode 100644 seedemu/services/DevService.py diff --git a/examples/scion/S06-dev-service/README.md b/examples/scion/S06-dev-service/README.md new file mode 100644 index 000000000..9f8017a7d --- /dev/null +++ b/examples/scion/S06-dev-service/README.md @@ -0,0 +1,25 @@ +### Proposal: Remote Development Service + +The DevelopmentService prepares a node for [Remote Development with VSCode](https://code.visualstudio.com/docs/remote/remote-overview). +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. +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. +With the DevService this paradigm can be adapted to meed the need of developing i.e. P2P software to maturity in a distributed environment. + +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) +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. +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. + +### Concerns: +- 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, + to allow as least nodes as possible on it (ideally /30 for only: the router-node, docker-host , broadcast and network address ) + This is to inhibit 'short-circuiting' the simulated network topology (i.e. any crosstalk past the intended network topo among nodes) + 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. + +[^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 + +### TODO: +- 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) +- 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 +- 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 +- 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') + diff --git a/examples/scion/S06-dev-service/dev-service.py b/examples/scion/S06-dev-service/dev-service.py new file mode 100644 index 000000000..8d7d21f05 --- /dev/null +++ b/examples/scion/S06-dev-service/dev-service.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +from seedemu.compiler import Docker,Graphviz +from seedemu.core import Emulator, Binding, Filter +from seedemu.layers import ScionBase, ScionRouting, ScionIsd, Scion +from seedemu.layers.Scion import LinkType as ScLinkType +from seedemu.services import ContainerDevelopmentService + +# Initialize +emu = Emulator() +base = ScionBase() +routing = ScionRouting() +scion_isd = ScionIsd() +scion = Scion() +devsvc = ContainerDevelopmentService() + +# SCION ISDs +base.createIsolationDomain(1) + +# Internet Exchange +base.createInternetExchange(100) + +# AS-150 +as150 = base.createAutonomousSystem(150) +scion_isd.addIsdAs(1, 150, is_core=True) +as150.createNetwork('net0') +as150.createControlService('cs1').joinNetwork('net0') +as150_router = as150.createRouter('br0') +as150_router.joinNetwork('net0').joinNetwork('ix100') +as150_router.crossConnect(153, 'br0', '10.50.0.2/29') + +# Create a host running the bandwidth test server +as150.createHost('node150_0').joinNetwork('net0', address='10.150.0.30') + +devsvc.install('dev_peer150_0').addVSCodeExtension('golang.Go').checkoutRepo('https://github.com/scionproto/scion','/home/root/repos/scion','master') + +emu.addBinding(Binding('dev_peer150_0', filter=Filter(nodeName='node150_0', asn=150))) + + +# AS-151 +as151 = base.createAutonomousSystem(151) +scion_isd.addIsdAs(1, 151, is_core=True) +as151.createNetwork('net0') +as151.createControlService('cs1').joinNetwork('net0') +as151.createRouter('br0').joinNetwork('net0').joinNetwork('ix100') + +as151.createHost('node151_0').joinNetwork('net0', address='10.151.0.30') +devsvc.install('dev_peer151_0').addVSCodeExtension('golang.Go').checkoutRepo('https://github.com/scionproto/scion','/home/root/repos/scion','master') + +emu.addBinding(Binding('dev_peer151_0', filter=Filter(nodeName='node151_0', asn=151,allowBound=True))) + +# AS-152 +as152 = base.createAutonomousSystem(152) +scion_isd.addIsdAs(1, 152, is_core=True) +as152.createNetwork('net0') +as152.createControlService('cs1').joinNetwork('net0') +as152.createRouter('br0').joinNetwork('net0').joinNetwork('ix100') + +as152.createHost('node152_0').joinNetwork('net0', address='10.152.0.30') +emu.addBinding(Binding('peer152_0', filter=Filter(nodeName='node152_0', asn=152))) + +# AS-153 +as153 = base.createAutonomousSystem(153) +scion_isd.addIsdAs(1, 153, is_core=False) +scion_isd.setCertIssuer((1, 153), issuer=150) +as153.createNetwork('net0') +as153.createControlService('cs1').joinNetwork('net0') +as153_router = as153.createRouter('br0') +as153_router.joinNetwork('net0') +as153_router.crossConnect(150, 'br0', '10.50.0.3/29') + +as153.createHost('node153_0').joinNetwork('net0', address='10.153.0.30') +emu.addBinding(Binding('peer153_0', filter=Filter(nodeName='node153_0', asn=153))) + +# Inter-AS routing +scion.addIxLink(100, (1, 150), (1, 151), ScLinkType.Core) +scion.addIxLink(100, (1, 151), (1, 152), ScLinkType.Core) +scion.addIxLink(100, (1, 152), (1, 150), ScLinkType.Core) +scion.addXcLink((1, 150), (1, 153), ScLinkType.Transit) + +# Rendering +emu.addLayer(base) +emu.addLayer(routing) +emu.addLayer(scion_isd) +emu.addLayer(scion) +emu.addLayer(devsvc) + +emu.render() + +# Compilation +emu.compile(Docker(), './output') +emu.compile(Graphviz(), './output/graphs') diff --git a/seedemu/compiler/Docker.py b/seedemu/compiler/Docker.py index 49bc91c6a..81096f791 100644 --- a/seedemu/compiler/Docker.py +++ b/seedemu/compiler/Docker.py @@ -96,6 +96,14 @@ }; done ''' +DockerCompilerFileTemplates['dockerbridge'] ="""\ + {name}: + ipam: + config: + - subnet: {snet} +""" + + DockerCompilerFileTemplates['compose'] = """\ version: "3.4" services: @@ -136,6 +144,8 @@ {networks}{ports}{volumes} labels: {labelList} + environment: + {environment} """ DockerCompilerFileTemplates['compose_label_meta'] = """\ @@ -853,6 +863,7 @@ def _compileNode(self, node: Node) -> str: netId = real_netname, address = address ) + node_nets += '\n'.join(node.getCustomNets()) _ports = node.getPorts() ports = '' @@ -891,7 +902,20 @@ def _compileNode(self, node: Node) -> str: volumeList = lst ) + name = self.__naming_scheme.format( + asn = node.getAsn(), + role = self._nodeRoleToString(node.getRole()), + name = node.getName(), + displayName = node.getDisplayName() if node.getDisplayName() != None else node.getName(), + primaryIp = node.getInterfaces()[0].getAddress() + ) + + name = sub(r'[^a-zA-Z0-9_.-]', '_', name) + dockerfile = DockerCompilerFileTemplates['dockerfile'] + + dockerfile += 'ENV CONTAINER_NAME {}\n'.format(name) + mkdir(real_nodename) chdir(real_nodename) @@ -912,6 +936,7 @@ def _compileNode(self, node: Node) -> str: dockerfile = 'FROM {}\n'.format(md5(image.getName().encode('utf-8')).hexdigest()) + dockerfile self._used_images.add(image.getName()) + for cmd in node.getDockerCommands(): dockerfile += '{}\n'.format(cmd) for cmd in node.getBuildCommands(): dockerfile += 'RUN {}\n'.format(cmd) start_commands = '' @@ -945,20 +970,10 @@ def _compileNode(self, node: Node) -> str: dockerfile += self._importFile(cpath, hpath) dockerfile += 'CMD ["/start.sh"]\n' - print(dockerfile, file=open('Dockerfile', 'w')) + print(dockerfile, file=open('Dockerfile', 'w')) chdir('..') - name = self.__naming_scheme.format( - asn = node.getAsn(), - role = self._nodeRoleToString(node.getRole()), - name = node.getName(), - displayName = node.getDisplayName() if node.getDisplayName() != None else node.getName(), - primaryIp = node.getInterfaces()[0].getAddress() - ) - - name = sub(r'[^a-zA-Z0-9_.-]', '_', name) - return DockerCompilerFileTemplates['compose_service'].format( nodeId = real_nodename, nodeName = name, @@ -967,7 +982,8 @@ def _compileNode(self, node: Node) -> str: # privileged = 'true' if node.isPrivileged() else 'false', ports = ports, labelList = self._getNodeMeta(node), - volumes = volumes + volumes = volumes, + environment= " - CONTAINER_NAME={}\n ".format(name) + '\n '.join(node.getCustomEnv()) ) def _compileNet(self, net: Network) -> str: @@ -1088,6 +1104,11 @@ def _doCompile(self, emulator: Emulator): dirName = image.getDirName() ) + bridges = registry.getByType('global','dockerbridge') + for b in bridges: + id = b.getAttribute('id') + self.__networks += DockerCompilerFileTemplates['dockerbridge'].format(name = b.getAttribute('name'), snet= str( b.getSubnet() ) ) + self._log('creating docker-compose.yml...'.format(scope, name)) print(DockerCompilerFileTemplates['compose'].format( services = self.__services, diff --git a/seedemu/core/Network.py b/seedemu/core/Network.py index a566d027f..11c9c704e 100644 --- a/seedemu/core/Network.py +++ b/seedemu/core/Network.py @@ -8,6 +8,42 @@ from .Visualization import Vertex from typing import Dict, Tuple, List + +class DockerBridge(Registrable): + + # TODO: maybe add an ASn scope to bridges to simplify retrieval from registry + __base:IPv4Network= IPv4Network('172.29.0.0/16') + # the last one will be 172.29.255.248/29 + # i doubt that anyone needs this many containers + + """ + @brief currently only used as a means to connect simulation-node router containers + (and through it any nodes on its 'local network') to the docker hosts internet connection + + the subnetmask for this 'micro' network is deliberately kept as big as possible, + to allow as least nodes as possible on it (ideally /30 for only: the router-node, docker-host , broadcast and network address ) + This is to inhibit 'short-circuiting' the simulated network topology (i.e. crosstalk past the intended network topo among nodes) + """ + def __init__(self,n: str,id: int ): + + super().__init__() + self.__base = IPv4Network('172.29.0.0/16') + super().doRegister('undefined', 'undefined', n) + super().setAttribute('name', n) + super().setAttribute('id',id) + + def getSubnet(self): + i =0 + bid = super().getAttribute('id') + gen = self.__base.subnets(new_prefix=29) + # docker complains, when /30 is used here, but why ?! 4x addresses in the net are enough + p: IPv4Network = IPv4Network('172.29.0.0/29') + while i <= bid: + p = next(gen) + i+=1 + return p + + class Network(Printable, Registrable, Vertex): """! @brief The network class. diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index f621b83c7..5041601bc 100644 --- a/seedemu/core/Node.py +++ b/seedemu/core/Node.py @@ -216,6 +216,7 @@ class Node(Printable, Registrable, Configurable, Vertex): __imported_files: Dict[str, str] __softwares: Set[str] __build_commands: List[str] + __docker_cmds: List[str] __start_commands: List[Tuple[str, bool]] __ports: List[Tuple[int, int, str]] __privileged: bool @@ -223,11 +224,14 @@ class Node(Printable, Registrable, Configurable, Vertex): __configured: bool __pending_nets: List[Tuple[str, str]] __xcs: Dict[Tuple[str, int], Tuple[IPv4Interface, str]] - + __custom_nets: List[str] + __custom_env: List[str] __shared_folders: Dict[str, str] __persistent_storages: List[str] __name_servers: List[str] + # wether this node requires to have 'real' internet access via its gateway + __reach_outside: bool def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): """! @@ -251,6 +255,7 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): self.__scope = scope if scope != None else str(asn) self.__softwares = set() self.__build_commands = [] + self.__docker_cmds = [] self.__start_commands = [] self.__ports = [] self.__privileged = False @@ -262,11 +267,21 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): self.__shared_folders = {} self.__persistent_storages = [] + self.__custom_nets = [] + self.__custom_env = [] # for soft in DEFAULT_SOFTWARE: # self.__softwares.add(soft) self.__name_servers = [] + self.__reach_outside =False + + def requestReachOutside(self): + self.__reach_outside = True + return self + + def reachesOutside(self): + return self.__reach_outside def configure(self, emulator: Emulator): """! @@ -344,6 +359,35 @@ def setNameServers(self, servers: List[str]) -> Node: self.__name_servers = servers return self + + def setCustomNet(self, net: str): + """ + @param net a network that the node shall join (in docker-compose syntax ) + i.e. : + net_153_net0: + ipv4_address: 10.153.0.254 + it will be inserted under the nodes 'networks:' section in the .yml file + """ + self.__custom_nets.append(net) + return self + + def getCustomNets(self): + return self.__custom_nets + + def setCustomEnv(self, env: str ): + """ + @param env an environment variable that docker-compose shall pass to the Dockerfile + + it gets inserted into the 'environment:' section of the node + i.e.: + environment: + - DEBUG=${DEBUG} + """ + self.__custom_env.append(env) + return self + + def getCustomEnv(self): + return self.__custom_env def getNameServers(self) -> List[str]: """! @@ -478,6 +522,12 @@ def joinNetwork(self, netname: str, address: str = "auto") -> Node: self.__pending_nets.append((netname, address)) return self + + def getNetNames(self): + """ + @brief list of names of networks this node has joined + """ + return [tuple[0] for tuple in self.__pending_nets] def updateNetwork(self, netname:str, address: str= "auto") -> Node: """! @@ -712,6 +762,18 @@ def addBuildCommand(self, cmd: str) -> Node: self.__build_commands.append(cmd) return self + + def addDockerCommand(self, cmd: str) -> Node: + """! + @brief Add new docker command to build step (possibly of kind other than RUN). + Unlike the ones added with addBuildCommands these commands wont be prefixed with RUN, + but are assumed to be valid Docker Commands by themselfes + """ + self.__docker_cmds.append(cmd) + return self + + def getDockerCommands(self)-> List[str]: + return self.__docker_cmds def getBuildCommands(self) -> List[str]: """! @@ -927,6 +989,26 @@ class Router(Node): """ __loopback_address: str + __external_bridge_id: int = -1 # -1 means not externally connected + + bridge_cnt: int = 0 + + def isConnectedExternal(self): + """ + @brief externally connected routers can reach the internet outside the simulation via the docker host + + a 'Local Network' with at least one host that requires to 'reach_outside' + needs to have an externally-connected router to do so + """ + return self.__external_bridge_id + + def setConnectedExternal(self): + + if self.__external_bridge_id != -1: return self + + self.__external_bridge_id = Router.bridge_cnt + Router.bridge_cnt += 1 + return self def setLoopbackAddress(self, address: str): """! diff --git a/seedemu/core/Registry.py b/seedemu/core/Registry.py index f17921cf3..6ba1f4054 100644 --- a/seedemu/core/Registry.py +++ b/seedemu/core/Registry.py @@ -25,6 +25,7 @@ def doRegister(self, scope: str, type: str, name: str): """! @brief Handle registration. + Attention: attributes are deleted on registration !! unintuitive ?! @param scope scope. @param type type. @param name name. @@ -32,7 +33,7 @@ def doRegister(self, scope: str, type: str, name: str): self._rscope = scope self._rtype = type self._rname = name - self._attrs = {} + self._attrs = {} # better move to init() ?! def getRegistryInfo(self) -> Tuple[str, str, str]: """! diff --git a/seedemu/core/__init__.py b/seedemu/core/__init__.py index 08d7355b9..1068de025 100644 --- a/seedemu/core/__init__.py +++ b/seedemu/core/__init__.py @@ -3,7 +3,7 @@ from .ScionAutonomousSystem import ScionAutonomousSystem from .IsolationDomain import IsolationDomain from .InternetExchange import InternetExchange -from .Network import Network +from .Network import Network, DockerBridge from .Node import Node, File, Interface, Router, RealWorldRouter, ScionRouter from .Printable import Printable from .Registry import Registry, ScopedRegistry, Registrable diff --git a/seedemu/layers/Routing.py b/seedemu/layers/Routing.py index 39fc99dd1..be7a93027 100644 --- a/seedemu/layers/Routing.py +++ b/seedemu/layers/Routing.py @@ -1,4 +1,4 @@ -from seedemu.core import ScopedRegistry, Node, Interface, Network, Emulator, Layer, Router, RealWorldRouter, BaseSystem +from seedemu.core import ScopedRegistry, Node, Interface, Network, DockerBridge,Emulator, Layer, Router, RealWorldRouter, BaseSystem from typing import List, Dict from ipaddress import IPv4Network @@ -142,6 +142,10 @@ def configure(self, emulator: Emulator): if has_localnet: rnode.addProtocol('direct', 'local_nets', RoutingFileTemplates['rnode_bird_direct'].format(interfaces = ifaces)) def render(self, emulator: Emulator): + + seen_bridge_ids = set() + bridges = [] + reg = emulator.getRegistry() for ((scope, type, name), obj) in reg.getAll().items(): if type == 'rs' or type == 'rnode': @@ -152,6 +156,17 @@ def render(self, emulator: Emulator): if issubclass(rnode.__class__, RealWorldRouter): self._log("Sealing real-world router as{}/{}...".format(rnode.getAsn(), rnode.getName())) rnode.seal() + + if rnode.isConnectedExternal() != -1: + bid = rnode.isConnectedExternal() + seen_bridge_ids.add(bid) + bname = "default{}".format(bid) + router.setCustomNet( " {}:\n".format(bname) ) + db = DockerBridge(bname,bid) + bridges.append(db) + router.addSoftware("iptables") # think this already defaulted ?! + router.appendStartCommand('iptables -t nat -A POSTROUTING -j MASQUERADE') + if type in ['hnode', 'csnode']: hnode: Node = obj @@ -167,12 +182,24 @@ def render(self, emulator: Emulator): for riface in router.getInterfaces(): if riface.getNet() == hnet: rif = riface + + if hnode.reachesOutside(): + assert router.isConnectedExternal() != -1 break assert rif != None, 'Host {} in as{} in network {}: no router'.format(name, scope, hnet.getName()) self._log("Setting default route for host {} ({}) to router {}".format(name, hif.getAddress(), rif.getAddress())) hnode.appendStartCommand('ip rou del default 2> /dev/null') hnode.appendStartCommand('ip route add default via {} dev {}'.format(rif.getAddress(), rif.getNet().getName())) + + for b in bridges: + id = b.getAttribute('id') + name = b.getAttribute('name') + reg.register('global','dockerbridge',name, b).setAttribute("id",id) + b.setAttribute('name',name) + + assert len(seen_bridge_ids) == Router.bridge_cnt + assert Router.bridge_cnt == len(bridges) def print(self, indent: int) -> str: out = ' ' * indent diff --git a/seedemu/services/DevService.py b/seedemu/services/DevService.py new file mode 100644 index 000000000..4ba962c2f --- /dev/null +++ b/seedemu/services/DevService.py @@ -0,0 +1,219 @@ +from __future__ import annotations +from typing import Dict +import re + +from seedemu.core.enums import NodeRole, NetworkType +from seedemu.core import Node, Server, Service, Emulator, Network +from typing import List, Tuple, Dict + + +ServerTemplates: Dict[str, str] = {} + +# create tunnel where the container waits for you to attach +#RUN TUNNEL_URL="$(/home/root/code tunnel --accept-server-license-terms --name $CONTAINER_NAME)" && echo "export TUNNEL_URL=$TUNNEL_URL" >> /etc/profile +#echo "CODE_TUNNEL_URL= $TUNNEL_URL" +ServerTemplates['command'] = """ + +echo "development container started on: $HOSTNAME $CONTAINER_NAME" +""" + +ServerTemplates['repo'] = """ +RUN git clone {repourl} -b {branch} {dir} +VOLUME {dir} +""" + +ServerTemplates['build'] = """ + +RUN wget -qO- "https://github.com/Kitware/CMake/releases/download/v3.28.1/cmake-3.28.1-linux-x86_64.tar.gz" | \ + tar --strip-components=1 -xz -C /usr/local && export PATH=$PATH:/usr/local/bin +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +RUN export PATH=$PATH:~/.cargo/bin +RUN ~/.cargo/bin/rustup default nightly +RUN wget -O- "https://go.dev/dl/go1.21.5.linux-amd64.tar.gz" --connect-timeout 0.1 seconds | tar -xz -C /usr/local && echo "export PATH=$PATH:/usr/local/go/bin" >> /etc/profile +RUN export PATH=$PATH:/usr/local/go/bin && go install -v golang.org/x/tools/gopls@latest +RUN export PATH=$PATH:/usr/local/go/bin && go install -v github.com/go-delve/delve/cmd/dlv@latest +RUN ~/.cargo/bin/cargo install cargo-deb +RUN mkdir /home/root + +# install VSCode CLI +#RUN curl -Lk 'https://code.visualstudio.com/sha/download?build=stable&os=cli-alpine-x64' --output /home/root/vscodecli.tar.gz +#RUN cd /home/root/ && tar -xf vscodecli.tar.gz +#https://go.microsoft.com/fwlink/?LinkID=760868 + +#doesnt seem to work quite yet - attachment to container is just as fast as if this line is commented out :/ +RUN curl -Lk 'https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64' --output /home/root/vscode.deb && apt install /home/root/vscode.deb -y + +{extensions} +""" + +class DevServer(Server): + """! + @brief installs stuff in the container that is required to use it for remote development + """ + + __id: int + # maybe add custom build command here and custom software + #[( dir-to-clone-into, branch-to-checkout, repo-url )] + __repos: [(str,str,str)] + # repos that have to be checked out will most likely vary by node + # and so do the software stacks required for each project + + # VSCode extensions that shall be installed in the service + __vscode_extensions: [str] + + def checkoutRepo(self, url: str, path: str, branch: str ): + """! + @brief checks out the git repository with the given URL and branch in a Docker Volume + @param path an absolute path to the directory where you want the repo to reside in the container + """ + + self.__repos.append( (url,path,branch) ) + + return self + + def __init__(self, id: int): + """! + @brief constructor. + """ + super().__init__() + self.__id = id + self.__vscode_extensions = [] + self.__repos = [] + + def addVSCodeExtension(self, ext: str): + self.__vscode_extensions.append(ext) + return self + + def getVSCodeExtensions(self): + return self.__vscode_extensions + + def install(self, node: Node): + """! + @brief Install the service. + """ + # Developing Software requires Internet connection for i.e. git, go get, cargo build, pip install etc. .. + node.requestReachOutside() + + node.addSoftware("wget curl git build-essential clang gpg") + + extens = '' + for e in self.getVSCodeExtensions(): + extens += 'RUN code --no-sandbox --user-data-dir /home/root/.vscode --install-extension %s\n' % e + # code doesnt like to be run as super user -> maybe useradd here ?! + # if only vscode cli is installed gives error: + # No installation of Visual Studio Code stable was found. + # Install it from your system's package manager or https://code.visualstudio.com, restart your shell, and try again. + # f you already installed Visual Studio Code and we didn't detect it, run `code version use stable --install-dir /path/to/installation` + + node.addDockerCommand(ServerTemplates['build'].format(extensions=extens)) + + for (u,p,b) in self.__repos: + node.addDockerCommand(ServerTemplates['repo'].format(repourl = u, branch=b, dir=p ) ) + + + node.setCustomEnv("- TESTVAR={}".format(node.getName() ) ) + + node.appendStartCommand(ServerTemplates['command']) + node.appendClassName("DevServer") + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'DevServer.\n' + return out + + +class ContainerDevelopmentService(Service): + """! + @brief Container Development service class. + """ + __dev_cnt: int =0 + __server: List[str] = [] + candidates = set() + + def __init__(self): + """! + @brief + """ + super().__init__() + + self.addDependency('Base', False, False) + self.addDependency('Scion', False, True) + + def _createServer(self) -> Server: + d = DevServer(self.__dev_cnt) + ContainerDevelopmentService.__dev_cnt += 1 + return d + + def install(self, vnode: str) -> Server: + + ContainerDevelopmentService.__server.append(vnode) + + return super().install(vnode) + + def configure(self, emulator: Emulator): + + # for all vnodes in self.__server find their local net/s , uniqueify them + # find of all the associated nodes with the net the routers + # and finally create custom bridge DockerNetworks and register them + # with the emulators registry in order for them to be retrieved by the DockerRenderer later on + + + + grouped_candidates =dict() # routers grouped by their net + # ideally there is for each local net, which contains at least one host with a DevService configured + # exactly one Router which is also the hosts default gateway + + # ATTENTION: this means names of virtual hosts with DevServices must be globally unique :| !! + for vnode in self.__server: + pnode = emulator.getBindingFor(vnode) # or resolvVnode(vnode) ?! + + if pnode.getRole() == NodeRole.Router: + ContainerDevelopmentService.candidates.add(pnode) + continue + + allnets: set(Network)= set() + for inf in pnode.getInterfaces(): + net = inf.getNet() + # in theorie a DevService could also be installed on a router node + # which might only be connected to networks of type other-than 'Local' + # but this is handled above + if net.getType() == NetworkType.Local: + if net in allnets: + print("multihomed host: %s" % vnode ) + + allnets.add( net) + for net in allnets: + for node in net.getAssociations(): + if node.getRole() == NodeRole.Router: + if net in grouped_candidates: + grouped_candidates[net].add(node) + else: + grouped_candidates[net]=set([node]) + ContainerDevelopmentService.candidates.add(node) + + # mark the router as external connected and allocate a bridge-id + # but do not create one yet + for c in ContainerDevelopmentService.candidates: + c.setConnectedExternal() + + super().configure(emulator) + + def getName(self) -> str: + return 'ContainerDevelopmentService' + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'ContainerDevelopmentService\n' + return out + +""" +USAGE: + +devsvc = ContainerDevelopmentService() + +... +as150.createHost('node150_0').joinNetwork('net0', address='10.150.0.30') + +devsvc.install('dev150_0').checkoutRepo(url= "https://github.com/your-username/your-project.git", dir="/home/root/pan",branch="awesome-feature").addVSCodeExtension('golang.Go') + +""" \ No newline at end of file diff --git a/seedemu/services/__init__.py b/seedemu/services/__init__.py index 75551ead1..43b8aaaab 100644 --- a/seedemu/services/__init__.py +++ b/seedemu/services/__init__.py @@ -9,4 +9,5 @@ from .BgpLookingGlassService import BgpLookingGlassServer, BgpLookingGlassService from .DHCPService import DHCPServer, DHCPService from .EthereumService import * -from .ScionBwtestService import ScionBwtestService \ No newline at end of file +from .ScionBwtestService import ScionBwtestService +from .DevService import ContainerDevelopmentService \ No newline at end of file From 9f53b4cd635b9ed0a2b313b198aa542479ba8ac7 Mon Sep 17 00:00:00 2001 From: amdfxlucas Date: Fri, 19 Jan 2024 12:51:58 +0100 Subject: [PATCH 2/2] use named volumes for persistent node storage --- seedemu/compiler/Docker.py | 16 ++++++++++++---- seedemu/core/Node.py | 12 ++++++++---- seedemu/services/DevService.py | 9 +++++++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/seedemu/compiler/Docker.py b/seedemu/compiler/Docker.py index 81096f791..673071cad 100644 --- a/seedemu/compiler/Docker.py +++ b/seedemu/compiler/Docker.py @@ -111,6 +111,8 @@ {services} networks: {networks} +volumes: +{named_volumes} """ DockerCompilerFileTemplates['compose_dummy'] = """\ @@ -173,7 +175,7 @@ """ DockerCompilerFileTemplates['compose_storage'] = """\ - - {nodePath} + - {volume_name}:{nodePath} """ DockerCompilerFileTemplates['compose_service_network'] = """\ @@ -332,6 +334,8 @@ class Docker(Compiler): __basesystem_dockerimage_mapping: dict + __named_volumes: List[str] + def __init__( self, platform:Platform = Platform.AMD64, @@ -399,9 +403,10 @@ def __init__( self.__image_per_node_list = {} self.__platform = platform + self.__named_volumes = [] self.__basesystem_dockerimage_mapping = BASESYSTEM_DOCKERIMAGE_MAPPING_PER_PLATFORM[self.__platform] - + for name, image in self.__basesystem_dockerimage_mapping.items(): priority = 0 if name == BaseSystem.DEFAULT: @@ -893,9 +898,11 @@ def _compileNode(self, node: Node) -> str: nodePath = nodePath ) - for path in storages: + for path,vname in storages: + self.__named_volumes.append(vname) lst += DockerCompilerFileTemplates['compose_storage'].format( - nodePath = path + nodePath = path, + volume_name= "{}".format(vname) ) volumes = DockerCompilerFileTemplates['compose_volumes'].format( @@ -1113,5 +1120,6 @@ def _doCompile(self, emulator: Emulator): print(DockerCompilerFileTemplates['compose'].format( services = self.__services, networks = self.__networks, + named_volumes= '\n'.join( map(lambda name: " {}:".format(name) ,self.__named_volumes )), dummies = local_images + self._makeDummies() ), file=open('docker-compose.yml', 'w')) diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index 5041601bc..c216ce6a0 100644 --- a/seedemu/core/Node.py +++ b/seedemu/core/Node.py @@ -227,7 +227,7 @@ class Node(Printable, Registrable, Configurable, Vertex): __custom_nets: List[str] __custom_env: List[str] __shared_folders: Dict[str, str] - __persistent_storages: List[str] + __persistent_storages: List[Tuple[str,str]] __name_servers: List[str] # wether this node requires to have 'real' internet access via its gateway @@ -863,7 +863,7 @@ def getSharedFolders(self) -> Dict[str, str]: """ return self.__shared_folders - def addPersistentStorage(self, path: str) -> Node: + def addPersistentStorage(self, path: str, volume_name: str ='') -> Node: """! @brief Add persistent storage to node. @@ -874,11 +874,15 @@ def addPersistentStorage(self, path: str) -> Node: @returns self, for chaining API calls. """ - self.__persistent_storages.append(path) + if volume_name=='': + volume_name = volume_name.replace('/','-').lstrip('-') + + tpl: Tuple[str,str] = (path,volume_name) + self.__persistent_storages.append(tpl) return self - def getPersistentStorages(self) -> List[str]: + def getPersistentStorages(self) -> List[Tuple[str,str]]: """! @brief Get persistent storage folders on the node. diff --git a/seedemu/services/DevService.py b/seedemu/services/DevService.py index 4ba962c2f..61c20ff90 100644 --- a/seedemu/services/DevService.py +++ b/seedemu/services/DevService.py @@ -1,7 +1,9 @@ from __future__ import annotations from typing import Dict import re - +from urllib.parse import urlparse,unquote +from pathlib import PurePosixPath +import os.path from seedemu.core.enums import NodeRole, NetworkType from seedemu.core import Node, Server, Service, Emulator, Network from typing import List, Tuple, Dict @@ -19,7 +21,6 @@ ServerTemplates['repo'] = """ RUN git clone {repourl} -b {branch} {dir} -VOLUME {dir} """ ServerTemplates['build'] = """ @@ -109,6 +110,10 @@ def install(self, node: Node): for (u,p,b) in self.__repos: node.addDockerCommand(ServerTemplates['repo'].format(repourl = u, branch=b, dir=p ) ) + path = urlparse(u).path + parts = PurePosixPath( unquote(path)).parts + repo = node.getName() + "-"+ parts[-2] + '-' + parts[-1] # gituser-repo + node.addPersistentStorage(p,repo) node.setCustomEnv("- TESTVAR={}".format(node.getName() ) )