Skip to content

Commit ed6ef82

Browse files
authored
Merge pull request #286 from amdfxlucas/seed-contrib08
support named docker volumes
2 parents 96f40c9 + 3a47c19 commit ed6ef82

File tree

5 files changed

+339
-54
lines changed

5 files changed

+339
-54
lines changed

seedemu/compiler/DistributedDocker.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Distributed Docker Compiler is not maintained
22

33
from .Docker import Docker, DockerCompilerFileTemplates
4-
from seedemu.core import Emulator, ScopedRegistry, Node, Network
4+
from seedemu.core import Emulator, ScopedRegistry, Node, Network, BaseVolume
55
from seedemu.core.enums import NodeRole
66
from typing import Dict
77
from hashlib import md5
88
from os import mkdir, chdir, rmdir
9+
from yaml import dump
910

1011
DistributedDockerCompilerFileTemplates: Dict[str, str] = {}
1112

@@ -70,6 +71,28 @@ def __compileIxNetWorker(self, net) -> str:
7071

7172
def _doCompile(self, emulator: Emulator):
7273
registry = emulator.getRegistry()
74+
75+
self._groupSoftware(emulator)
76+
77+
toplevelvolumes = ''
78+
if len(topvols := self._getVolumes()) > 0:
79+
toplevelvolumes += 'volumes:\n'
80+
#topvols = set(map( lambda vol: TopLvlVolume(vol), pool.getVolumes() ))
81+
82+
for v in topvols:
83+
v.mode = 'toplevel'
84+
85+
#toplevelvolumes += '\n'.join(map( lambda line: ' ' + line ,dump( topvols ).split('\n') ) )
86+
87+
# sharedFolders/bind mounts do not belong in the top-level volumes section
88+
for v in [vv for vv in topvols if vv.asDict()['type'] == 'volume' ]:
89+
toplevelvolumes += ' {}:\n'.format(v.asDict()['source']) # why not 'name'
90+
lines = dump( v ).rstrip('\n').split('\n')
91+
toplevelvolumes += '\n'.join( map( lambda x: ' ' if x[0] != 0
92+
else ' ' + x[1] if x[1] != ''
93+
else '', enumerate(lines ) ) )
94+
toplevelvolumes += '\n'
95+
7396
scopes = set()
7497
for (scope, _, _) in registry.getAll().keys(): scopes.add(scope)
7598

@@ -114,6 +137,7 @@ def _doCompile(self, emulator: Emulator):
114137
print(DockerCompilerFileTemplates['compose'].format(
115138
services = services,
116139
networks = networks,
140+
volumes = toplevelvolumes,
117141
dummies = self._makeDummies()
118142
), file=open('docker-compose.yml', 'w'))
119143

seedemu/compiler/Docker.py

Lines changed: 69 additions & 28 deletions
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, ScopeType, ScopeTier, OptionHandling
3+
from seedemu.core import Node, Network, Compiler, BaseSystem, BaseOption, Scope, ScopeType, ScopeTier, OptionHandling, BaseVolume
44
from seedemu.core.enums import NodeRole, NetworkType
55
from .DockerImage import DockerImage
66
from .DockerImageConstant import *
@@ -12,7 +12,7 @@
1212
from ipaddress import IPv4Network, IPv4Address
1313
from shutil import copyfile
1414
import json
15-
15+
from yaml import dump
1616

1717
SEEDEMU_INTERNET_MAP_IMAGE='handsonsecurity/seedemu-multiarch-map:buildx-latest'
1818
SEEDEMU_ETHER_VIEW_IMAGE='handsonsecurity/seedemu-multiarch-etherview:buildx-latest'
@@ -104,6 +104,7 @@
104104
{services}
105105
networks:
106106
{networks}
107+
{volumes}
107108
"""
108109

109110
DockerCompilerFileTemplates['compose_dummy'] = """\
@@ -159,16 +160,6 @@
159160
{volumeList}
160161
"""
161162

162-
DockerCompilerFileTemplates['compose_volume'] = """\
163-
- type: bind
164-
source: {hostPath}
165-
target: {nodePath}
166-
"""
167-
168-
DockerCompilerFileTemplates['compose_storage'] = """\
169-
- {nodePath}
170-
"""
171-
172163
DockerCompilerFileTemplates['compose_service_network'] = """\
173164
{netId}:
174165
{address}
@@ -394,6 +385,13 @@ def __init__(
394385
self._used_images = set()
395386
self.__image_per_node_list = {}
396387
self.__config = [] # variables for '.env' file alongside 'docker-compose.yml'
388+
389+
self.__volumes_dedup = (
390+
[]
391+
) # unforunately set(()) failed to automatically deduplicate
392+
self.__vol_names = []
393+
super().__init__()
394+
397395
self.__platform = platform
398396

399397
self.__basesystem_dockerimage_mapping = BASESYSTEM_DOCKERIMAGE_MAPPING_PER_PLATFORM[self.__platform]
@@ -404,6 +402,24 @@ def __init__(
404402
priority = 1
405403
self.addImage(image, priority=priority)
406404

405+
def _addVolume(self, vol: BaseVolume):
406+
"""! @brief add a docker volume/bind mount/or tmpfs
407+
408+
Remember them for later, to generate the top lvl 'volumes:' section of docker-compose.yml
409+
"""
410+
# if vol.type() == 'volume': # then it is a named-volume
411+
key = vol.asDict()["source"]
412+
if key not in self.__vol_names:
413+
self.__volumes_dedup.append(vol)
414+
self.__vol_names.append(key)
415+
return self
416+
417+
def _getVolumes(self) -> List[BaseVolume]:
418+
"""! @brief get all docker volumes/mounts that must appear
419+
in docker-compose.yml top-level 'volumes:' section
420+
"""
421+
return self.__volumes_dedup
422+
407423
def optionHandlingCapabilities(self) -> OptionHandling:
408424
return OptionHandling.DIRECT_DOCKER_COMPOSE | OptionHandling.CREATE_SEPARATE_ENV_FILE
409425

@@ -910,28 +926,23 @@ def _getComposeNodeNets(self, node: Node) -> str:
910926
return node_nets, dummy_addr_map
911927

912928
def _getComposeNodeVolumes(self, node: Node) -> str:
913-
_volumes = node.getSharedFolders()
914-
storages = node.getPersistentStorages()
929+
""" compute the docker-compose 'volumes:' section for this service(emulation node)"""
915930

916931
volumes = ''
932+
# svcvols = map( lambda vol: ServiceLvlVolume(vol), node.getCustomVolumes() )
933+
svcvols = list (set(node.getDockerVolumes() ))
934+
for v in svcvols:
935+
v.mode = 'service'
936+
yamlvols = '\n'.join(map( lambda line: ' ' + line ,dump( svcvols ).split('\n') ) )
917937

918-
if len(_volumes) > 0 or len(storages) > 0:
919-
lst = ''
938+
volumes +=' volumes:\n' + yamlvols if len(node.getDockerVolumes()) > 0 else ''
920939

921-
for (nodePath, hostPath) in _volumes.items():
922-
lst += DockerCompilerFileTemplates['compose_volume'].format(
923-
hostPath = hostPath,
924-
nodePath = nodePath
925-
)
926940

927-
for path in storages:
928-
lst += DockerCompilerFileTemplates['compose_storage'].format(
929-
nodePath = path
930-
)
941+
# the top-level docker-compose volumes section is rendered at a later stage ..
942+
# Remember encountered volumes until then
943+
for v in node.getDockerVolumes():
944+
self._addVolume(v)
931945

932-
volumes = DockerCompilerFileTemplates['compose_volumes'].format(
933-
volumeList = lst
934-
)
935946
return volumes
936947

937948
def _computeDockerfile(self, node: Node) -> str:
@@ -1260,11 +1271,41 @@ def _doCompile(self, emulator: Emulator):
12601271
dirName = image.getDirName()
12611272
)
12621273

1274+
toplevelvolumes = self._computeComposeTopLvlVolumes()
1275+
12631276
self._log('creating docker-compose.yml...'.format(scope, name))
12641277
print(DockerCompilerFileTemplates['compose'].format(
12651278
services = self.__services,
12661279
networks = self.__networks,
1280+
volumes = toplevelvolumes,
12671281
dummies = local_images + self._makeDummies()
12681282
), file=open('docker-compose.yml', 'w'))
12691283

12701284
self.generateEnvFile(Scope(ScopeTier.Global),'')
1285+
1286+
def _computeComposeTopLvlVolumes(self) -> str:
1287+
"""!@brief render the 'volumes:' section of the docker-compose.yml file
1288+
It contains named volumes but not bind-mounts.
1289+
"""
1290+
toplevelvolumes = ''
1291+
if len(topvols := self._getVolumes()) > 0:
1292+
hit = False
1293+
#topvols = set(map( lambda vol: TopLvlVolume(vol), pool.getVolumes() ))
1294+
1295+
for v in topvols:
1296+
v.mode = 'toplevel'
1297+
1298+
#toplevelvolumes += '\n'.join(map( lambda line: ' ' + line ,dump( topvols ).split('\n') ) )
1299+
1300+
# sharedFolders/bind mounts do not belong in the top-level volumes section
1301+
for v in [vv for vv in topvols if vv.asDict()['type'] == 'volume' ]:
1302+
hit = True
1303+
toplevelvolumes += ' {}:\n'.format(v.asDict()['source']) # why not 'name'
1304+
lines = dump( v ).rstrip('\n').split('\n')
1305+
toplevelvolumes += '\n'.join( map( lambda x: ' '
1306+
if x[0] != 0 else ' ' + x[1]
1307+
if x[1] != ''else '' , enumerate(lines ) ) )
1308+
toplevelvolumes += '\n'
1309+
1310+
if hit: toplevelvolumes = 'volumes:\n' + toplevelvolumes
1311+
return toplevelvolumes

seedemu/core/Node.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import annotations
2+
import random
3+
import string
24
from .Printable import Printable
35
from .Network import Network
46
from .enums import NodeRole
57
from .Scope import *
68
from .Registry import Registrable
79
from .Emulator import Emulator
810
from .Customizable import Customizable
11+
from .Volume import BaseVolume
912
from .Configurable import Configurable
1013
from .enums import NetworkType
1114
from .Visualization import Vertex
@@ -230,8 +233,7 @@ class Node(Printable, Registrable, Configurable, Vertex, Customizable):
230233
# Dict of (peername, peerasn) -> (localaddr, netname, netProperties) -- netProperties = (latency, bandwidth, packetDrop, MTU)
231234
__xcs: Dict[Tuple[str, int], Tuple[IPv4Interface, str, Tuple[int,int,float,int]]]
232235

233-
__shared_folders: Dict[str, str]
234-
__persistent_storages: List[str]
236+
__custom_vols: List[BaseVolume]
235237
__name_servers: List[str]
236238

237239
__geo: Tuple[float,float,str] # (Latitude,Longitude,Address) -- optional parameter that contains the geographical location of the Node
@@ -270,8 +272,7 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None):
270272
self.__xcs = {}
271273
self.__configured = False
272274

273-
self.__shared_folders = {}
274-
self.__persistent_storages = []
275+
self.__custom_vols = []
275276

276277
for soft in DEFAULT_SOFTWARE:
277278
self.__softwares.add(soft)
@@ -384,6 +385,17 @@ def setNameServers(self, servers: List[str]) -> Node:
384385

385386
return self
386387

388+
def addDockerVolume(self, vol: BaseVolume ):
389+
"""!@brief adds the docker volume to this node's container
390+
It can be a shared folder, named docker volume or tmpfs
391+
"""
392+
self.__custom_vols.append(vol )
393+
return self
394+
395+
def getDockerVolumes(self):
396+
"""!@brief retrieve any volumes mounted on this node's container"""
397+
return self.__custom_vols
398+
387399
# TODO: if a separate .env file is created, or the values are given directly in the docker-compose.yml 'environment' section
388400
# could be a setting of the Docker compiler
389401
# def setCustomEnv(self, key: str, actual_value: str, scope: ScopeTier=ScopeTier.Node, use_envsubst: bool=False):
@@ -891,50 +903,43 @@ def getInterfaces(self) -> List[Interface]:
891903
"""
892904
return self.__interfaces
893905

894-
def addSharedFolder(self, nodePath: str, hostPath: str) -> Node:
906+
def addSharedFolder(self, nodePath: str, hostPath: str, **kwargs) -> Node:
895907
"""!
896908
@@brief Add a new shared folder between the node and host.
897909
898910
@param nodePath path to the folder inside the container.
899911
@param hostPath path to the folder on the emulator host node.
912+
@param kwargs any other docker volume options i.e. 'readonly'
900913
901914
@returns self, for chaining API calls.
902915
"""
903-
self.__shared_folders[nodePath] = hostPath
916+
# bind mounts are never named!
917+
self.__custom_vols.append( BaseVolume(source=hostPath, target=nodePath, type='bind', **kwargs) )
904918

905919
return self
906920

907-
def getSharedFolders(self) -> Dict[str, str]:
908-
"""!
909-
@brief Get shared folders between the node and host.
910-
911-
@returns dict, where key is the path in container and value is path on
912-
host.
913-
"""
914-
return self.__shared_folders
915-
916-
def addPersistentStorage(self, path: str) -> Node:
921+
def addPersistentStorage(self, path: str, name: str=None, **kwargs) -> Node:
917922
"""!
918923
@brief Add persistent storage to node.
919924
920925
Nodes usually start fresh when you re-start them. This allow setting a
921926
directory where data will be persistent.
922927
923928
@param path path to put the persistent storage folder in the container.
929+
@param name if specified a named-volume is created.
930+
By specifying the same name on multiple nodes
931+
the volume is effectively shared between those containers.
932+
@param kwargs any other docker volume options i.e. 'readonly'
924933
925934
@returns self, for chaining API calls.
926935
"""
927-
self.__persistent_storages.append(path)
928936

929-
return self
937+
if name == None: # generate random name for anonymus volume
938+
name = ''.join(random.choice(string.ascii_lowercase) for i in range(6))
930939

931-
def getPersistentStorages(self) -> List[str]:
932-
"""!
933-
@brief Get persistent storage folders on the node.
940+
self.__custom_vols.append( BaseVolume(target=path, type='volume', name=name, source=name, **kwargs) )
934941

935-
@returns list of persistent storage folder.
936-
"""
937-
return self.__persistent_storages
942+
return self
938943

939944
def setGeo(self, Lat: float, Long: float, Address: str="") -> Node:
940945
"""!
@@ -995,7 +1000,7 @@ def copySettings(self, node: Node):
9951000
# if node.getBaseSystem() != None : self.setBaseSystem(node.getClasses())
9961001

9971002
for (h, n, p) in node.getPorts(): self.addPort(h, n, p)
998-
for p in node.getPersistentStorages(): self.addPersistentStorage(p)
1003+
for v in node.getDockerVolumes(): self.addDockerVolume(v)
9991004
for (c, f) in node.getStartCommands(): self.appendStartCommand(c, f)
10001005
# for (c, f) in node.getUserStartCommands(): self.appendUserStartCommand(c, f)
10011006
for c in node.getBuildCommands(): self.addBuildCommand(c)

0 commit comments

Comments
 (0)