Skip to content

Commit df3a54f

Browse files
authored
Implement Dynamic Port Mapping for Function Containers for Local Deployment (#199)
* rebase from master * implement port mapping for function containers for local deployments on non-linux platforms * undo accidental push (configuration files) * use is_linux utility function in sebs/storage/minio.py
1 parent 655f644 commit df3a54f

File tree

8 files changed

+115
-38
lines changed

8 files changed

+115
-38
lines changed

sebs.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def parse_common_params(
130130

131131
sebs_client = sebs.SeBS(cache, output_dir, verbose, logging_filename)
132132
output_dir = sebs.utils.create_output(output_dir, preserve_out, verbose)
133-
133+
134134
sebs_client.logging.info("Created experiment output at {}".format(output_dir))
135135

136136
# CLI overrides JSON options
@@ -140,6 +140,9 @@ def parse_common_params(
140140
update_nested_dict(config_obj, ["experiments", "update_code"], update_code)
141141
update_nested_dict(config_obj, ["experiments", "update_storage"], update_storage)
142142

143+
# set the path the configuration was loaded from
144+
update_nested_dict(config_obj, ["deployment", "local", "path"], config)
145+
143146
if storage_configuration:
144147
cfg = json.load(open(storage_configuration, 'r'))
145148
update_nested_dict(config_obj, ["deployment", deployment, "storage"], cfg)
@@ -453,6 +456,9 @@ def start(benchmark, benchmark_input_size, output, deployments, storage_configur
453456
# Disable shutdown of storage only after we succed
454457
# Otherwise we want to clean up as much as possible
455458
deployment_client.shutdown_storage = False
459+
460+
deployment_client.config.serialize()
461+
456462
result.serialize(output)
457463
sebs_client.logging.info(f"Save results to {os.path.abspath(output)}")
458464

sebs/aws/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ def http_api(
199199
self.logging.info(f"Using cached HTTP API {api_name}")
200200
return http_api
201201

202+
# FIXME: python3.7+ future annotations
202203
@staticmethod
203204
def initialize(res: Resources, dct: dict):
204205

sebs/local/config.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import json
2+
13
from typing import cast, Optional
24

35
from sebs.cache import Cache
46
from sebs.faas.config import Config, Credentials, Resources
57
from sebs.storage.minio import MinioConfig
6-
from sebs.utils import LoggingHandlers
8+
from sebs.utils import serialize, LoggingHandlers
79

810

911
class LocalCredentials(Credentials):
@@ -23,15 +25,28 @@ def deserialize(config: dict, cache: Cache, handlers: LoggingHandlers) -> Creden
2325

2426
class LocalResources(Resources):
2527
def __init__(self, storage_cfg: Optional[MinioConfig] = None):
28+
self._path: str = ""
2629
super().__init__(name="local")
2730
self._storage = storage_cfg
31+
self._allocated_ports = set()
2832

2933
@property
3034
def storage_config(self) -> Optional[MinioConfig]:
3135
return self._storage
3236

37+
@property
38+
def path(self) -> str:
39+
return self._path
40+
41+
@property
42+
def allocated_ports(self) -> set:
43+
return self._allocated_ports
44+
3345
def serialize(self) -> dict:
34-
return {}
46+
out = {
47+
"allocated_ports": list(self._allocated_ports)
48+
}
49+
return out
3550

3651
@staticmethod
3752
def initialize(res: Resources, cfg: dict):
@@ -40,10 +55,15 @@ def initialize(res: Resources, cfg: dict):
4055
@staticmethod
4156
def deserialize(config: dict, cache: Cache, handlers: LoggingHandlers) -> Resources:
4257
ret = LocalResources()
58+
ret._path = config["path"]
4359
# Check for new config
4460
if "storage" in config:
4561
ret._storage = MinioConfig.deserialize(config["storage"])
4662
ret.logging.info("Using user-provided configuration of storage for local containers.")
63+
64+
if "allocated_ports" in config:
65+
ret._allocated_ports = set(config["allocated_ports"])
66+
4767
return ret
4868

4969

@@ -84,6 +104,12 @@ def deserialize(config: dict, cache: Cache, handlers: LoggingHandlers) -> Config
84104
return config_obj
85105

86106
def serialize(self) -> dict:
107+
with open(self.resources.path, "r+") as out:
108+
config = json.load(out)
109+
config["deployment"]["local"].update(self.resources.serialize())
110+
out.seek(0)
111+
out.write(serialize(config))
112+
87113
return {}
88114

89115
def update_cache(self, cache: Cache):

sebs/local/function.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
from typing import Optional
55

6+
from sebs.utils import is_linux
67
from sebs.faas.function import ExecutionResult, Function, FunctionConfig, Trigger
78

89

@@ -53,17 +54,21 @@ def __init__(
5354
self._instance.reload()
5455
networks = self._instance.attrs["NetworkSettings"]["Networks"]
5556
self._port = port
56-
self._url = "{IPAddress}:{Port}".format(
57-
IPAddress=networks["bridge"]["IPAddress"], Port=port
58-
)
59-
if not self._url:
60-
self.logging.error(
61-
f"Couldn't read the IP address of container from attributes "
62-
f"{json.dumps(self._instance.attrs, indent=2)}"
63-
)
64-
raise RuntimeError(
65-
f"Incorrect detection of IP address for container with id {self._instance_id}"
57+
58+
if is_linux():
59+
self._url = "{IPAddress}:{Port}".format(
60+
IPAddress=networks["bridge"]["IPAddress"], Port=port
6661
)
62+
if not self._url:
63+
self.logging.error(
64+
f"Couldn't read the IP address of container from attributes "
65+
f"{json.dumps(self._instance.attrs, indent=2)}"
66+
)
67+
raise RuntimeError(
68+
f"Incorrect detection of IP address for container with id {self._instance_id}"
69+
)
70+
else:
71+
self._url = f"localhost:{port}"
6772

6873
self._measurement_pid = measurement_pid
6974

sebs/local/local.py

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
import time
55
from typing import cast, Dict, List, Optional, Type, Tuple # noqa
66
import subprocess
7+
import socket
78

89
import docker
910

1011
from sebs.cache import Cache
1112
from sebs.config import SeBSConfig
12-
from sebs.utils import LoggingHandlers
13+
from sebs.utils import LoggingHandlers, is_linux
1314
from sebs.local.config import LocalConfig
1415
from sebs.local.storage import Minio
1516
from sebs.local.function import LocalFunction
@@ -174,27 +175,60 @@ def create_function(self, code_package: Benchmark, func_name: str) -> "LocalFunc
174175
self.name(), code_package.language_name
175176
),
176177
}
177-
container = self._docker_client.containers.run(
178-
image=container_name,
179-
command=f"/bin/bash /sebs/run_server.sh {self.DEFAULT_PORT}",
180-
volumes={code_package.code_location: {"bind": "/function", "mode": "ro"}},
181-
environment=environment,
182-
# FIXME: make CPUs configurable
183-
# FIXME: configure memory
184-
# FIXME: configure timeout
185-
# cpuset_cpus=cpuset,
186-
# required to access perf counters
187-
# alternative: use custom seccomp profile
188-
privileged=True,
189-
security_opt=["seccomp:unconfined"],
190-
network_mode="bridge",
191-
# somehow removal of containers prevents checkpointing from working?
192-
remove=self.remove_containers,
193-
stdout=True,
194-
stderr=True,
195-
detach=True,
196-
# tty=True,
197-
)
178+
179+
# FIXME: make CPUs configurable
180+
# FIXME: configure memory
181+
# FIXME: configure timeout
182+
# cpuset_cpus=cpuset,
183+
# required to access perf counters
184+
# alternative: use custom seccomp profile
185+
container_kwargs = {
186+
"image": container_name,
187+
"command": f"/bin/bash /sebs/run_server.sh {self.DEFAULT_PORT}",
188+
"volumes": {code_package.code_location: {"bind": "/function", "mode": "ro"}},
189+
"environment": environment,
190+
"privileged": True,
191+
"security_opt": ["seccomp:unconfined"],
192+
"network_mode": "bridge",
193+
"remove": self.remove_containers,
194+
"stdout": True,
195+
"stderr": True,
196+
"detach": True,
197+
# "tty": True,
198+
}
199+
200+
# If SeBS is running on non-linux platforms, container port must be mapped to host port to make it reachable
201+
# Check if the system is NOT Linux or that it is WSL
202+
port = self.DEFAULT_PORT
203+
if not is_linux():
204+
port_found = False
205+
for p in range(self.DEFAULT_PORT, self.DEFAULT_PORT + 1000):
206+
# check no container has been deployed on docker's port p
207+
if p not in self.config.resources.allocated_ports:
208+
# check if port p on the host is free
209+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
210+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
211+
try:
212+
s.bind(("127.0.0.1", p))
213+
# The port is available
214+
port = p
215+
port_found = True
216+
self.config.resources.allocated_ports.add(p)
217+
break
218+
except socket.error as e:
219+
# The port is already in use
220+
continue
221+
222+
if not port_found:
223+
raise RuntimeError(
224+
f"Failed to allocate port for container: No ports available between "
225+
f"{self.DEFAULT_PORT} and {self.DEFAULT_PORT + 999}"
226+
)
227+
228+
container_kwargs["command"] = f"/bin/bash /sebs/run_server.sh {port}"
229+
container_kwargs["ports"] = {f'{port}/tcp': port}
230+
231+
container = self._docker_client.containers.run(**container_kwargs)
198232

199233
pid: Optional[int] = None
200234
if self.measurements_enabled and self._memory_measurement_path is not None:
@@ -216,7 +250,7 @@ def create_function(self, code_package: Benchmark, func_name: str) -> "LocalFunc
216250
function_cfg = FunctionConfig.from_benchmark(code_package)
217251
func = LocalFunction(
218252
container,
219-
self.DEFAULT_PORT,
253+
port,
220254
func_name,
221255
code_package.benchmark,
222256
code_package.hash,

sebs/openwhisk/openwhisk.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def build_base_image(
143143
else:
144144
registry_name = "Docker Hub"
145145

146-
# Check if we the image is already in the registry.
146+
# Check if the image is already in the registry.
147147
# cached package, rebuild not enforced -> check for new one
148148
if is_cached:
149149
if self.find_image(repository_name, image_tag):

sebs/storage/minio.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from sebs.faas.config import Resources
1515
from sebs.faas.storage import PersistentStorage
1616
from sebs.storage.config import MinioConfig
17+
from sebs.utils import is_linux
1718

1819

1920
class Minio(PersistentStorage):
@@ -108,7 +109,7 @@ def configure_connection(self):
108109
self._storage_container.reload()
109110

110111
# Check if the system is Linux and that it's not WSL
111-
if platform.system() == "Linux" and "microsoft" not in platform.release().lower():
112+
if is_linux():
112113
networks = self._storage_container.attrs["NetworkSettings"]["Networks"]
113114
self._cfg.address = "{IPAddress}:{Port}".format(
114115
IPAddress=networks["bridge"]["IPAddress"], Port=9000

sebs/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import uuid
77
import click
88
import datetime
9+
import platform
910

1011
from typing import List, Optional
1112

@@ -251,6 +252,9 @@ def logging_handlers(self, handlers: LoggingHandlers):
251252
def has_platform(name: str) -> bool:
252253
return os.environ.get(f"SEBS_WITH_{name.upper()}", "False").lower() == "true"
253254

255+
# Check if the system is Linux and that it's not WSL
256+
def is_linux() -> bool:
257+
return platform.system() == "Linux" and "microsoft" not in platform.release().lower()
254258

255259
def catch_interrupt():
256260

0 commit comments

Comments
 (0)