Skip to content

Commit

Permalink
Add dedicated network support (with default docker config)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dramelac committed Feb 25, 2024
1 parent e2c758c commit 4b52c55
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 9 deletions.
8 changes: 6 additions & 2 deletions exegol/model/ContainerConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ def __prepareContainerConfig(self):
self.enableExegolResources()
if ParametersManager().log:
self.enableShellLogging(ParametersManager().log_method,
UserConfig().shell_logging_compress ^ ParametersManager().log_compress)
UserConfig().shell_logging_compress ^ ParametersManager().log_compress)
if ParametersManager().workspace_path:
if ParametersManager().mount_current_dir:
logger.warning(f'Workspace conflict detected (-cwd cannot be use with -w). Using: {ParametersManager().workspace_path}')
Expand Down Expand Up @@ -936,11 +936,15 @@ def __removeSysctl(self, sysctl_key: str):
return False

def getNetwork(self) -> (str, str):
"""Network mode, docker term getter"""
"""First Network getter for docker API on container creation"""
if len(self.__networks) > 0:
return self.__networks[0].getNetworkConfig()
return None, None

def getNetworks(self):
"""Networks getter"""
return self.__networks

def setExtraHost(self, host: str, ip: str):
"""Add or update an extra host to resolv inside the container."""
self.__extra_host[host] = ip
Expand Down
9 changes: 7 additions & 2 deletions exegol/model/ExegolContainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,13 @@ def remove(self):
self.__container.remove()
logger.success(f"Container {self.name} successfully removed.")
except NotFound:
logger.error(
f"The container {self.name} has already been removed (probably created as a temporary container).")
logger.error(f"The container {self.name} has already been removed (probably created as a temporary container).")
nets = self.config.getNetworks()
# Must be imported locally to avoid circular importation
from exegol.utils.DockerUtils import DockerUtils
for net in nets:
if net.shouldBeRemoved():
DockerUtils.removeNetwork(net.getNetworkName())

def __removeVolume(self):
"""Remove private workspace volume directory if exist"""
Expand Down
14 changes: 11 additions & 3 deletions exegol/model/ExegolNetwork.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from enum import Enum
from typing import Optional, Union, List

from docker.types import EndpointConfig


class DockerDrivers(Enum):
"""Enum for Docker driver type"""
Expand All @@ -21,13 +19,17 @@ class ExegolNetworkMode(Enum):


class ExegolNetwork:
DEFAULT_DOCKER_NETWORK = [d.value for d in DockerDrivers]

__DEFAULT_NETWORK_DRIVER = DockerDrivers.Bridge

def __init__(self, net_mode: ExegolNetworkMode = ExegolNetworkMode.host, net_name: Optional[str] = None):
self.__net_mode: ExegolNetworkMode = net_mode
self.__net_name: str = net_name if net_name is not None else net_mode.value
try:
self.__docker_net_mode: DockerDrivers = DockerDrivers(self.__net_name)
except ValueError:
self.__docker_net_mode = DockerDrivers.Bridge
self.__docker_net_mode = self.__DEFAULT_NETWORK_DRIVER

@classmethod
def instance_network(cls, mode: Union[ExegolNetworkMode, str], container_name: str):
Expand Down Expand Up @@ -60,11 +62,17 @@ def getNetworkConfig(self) -> (str, str):
def getNetworkMode(self) -> ExegolNetworkMode:
return self.__net_mode

def getNetworkName(self):
return self.__net_name

def getTextNetworkMode(self) -> str:
if self.__net_mode is ExegolNetworkMode.attached:
return self.__net_name
return self.__net_mode.name

def shouldBeRemoved(self):
return self.__net_mode == ExegolNetworkMode.nat

def __repr__(self):
repr_str = self.__net_mode.value
if self.__net_mode in [ExegolNetworkMode.nat, ExegolNetworkMode.attached]:
Expand Down
112 changes: 110 additions & 2 deletions exegol/utils/DockerUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from docker import DockerClient
from docker.errors import APIError, DockerException, NotFound, ImageNotFound
from docker.models.images import Image
from docker.models.networks import Network
from docker.models.volumes import Volume
from requests import ReadTimeout

Expand All @@ -20,6 +21,7 @@
from exegol.model.ExegolContainer import ExegolContainer
from exegol.model.ExegolContainerTemplate import ExegolContainerTemplate
from exegol.model.ExegolImage import ExegolImage
from exegol.model.ExegolNetwork import ExegolNetwork
from exegol.model.MetaImages import MetaImages
from exegol.utils.ExeLog import logger, console, ExeLog
from exegol.utils.WebUtils import WebUtils
Expand Down Expand Up @@ -132,6 +134,10 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False
docker_args["network_disabled"] = True
else:
docker_args["network"], docker_args["network_driver_opt"] = model.config.getNetwork()
if not cls.networkExist(docker_args["network"]):
if not cls.createNetwork(network_name=docker_args["network"], driver=docker_args["network_driver_opt"]):
logger.critical("Unable to create the dedicated network for the new container. Aborting.")

# Handle temporary arguments
if temporary:
# Only the 'run' function support the "remove" parameter
Expand All @@ -158,6 +164,11 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False
logger.debug("Container removed")
except Exception:
pass
try:
if cls.removeNetwork(docker_args["network"]):
logger.debug("Network removed")
except Exception:
pass
logger.critical("Error while creating exegol container. Exiting.")
# Not reachable, critical logging will exit
return # type: ignore
Expand Down Expand Up @@ -256,6 +267,103 @@ def __loadDockerVolume(cls, volume_path: str, volume_name: str) -> Volume:
return None # type: ignore
return volume

# # # Network Section # # #

@classmethod
def listAttachableNetworks(cls) -> List[Network]:
"""List every non-default networks"""
networks = []
try:
networks = cls.__client.networks.list()
except APIError as e:
raise e
except ReadTimeout:
logger.critical("Received a timeout error, Docker is busy... Unable to enumerate volume, retry later.")
for net in networks.copy():
if net.name in ExegolNetwork.DEFAULT_DOCKER_NETWORK:
networks.remove(net)
return networks

@classmethod
def listExegolNetworks(cls) -> List[Network]:
"""List every exegol networks"""
try:
return cls.__client.networks.list(filters={"label": "source=exegol"})
except APIError as e:
raise e
except ReadTimeout:
logger.critical("Received a timeout error, Docker is busy... Unable to enumerate volume, retry later.")

@classmethod
def getNetwork(cls, network_name: str, exegol_only: bool = False) -> Optional[Network]:
"""Find a specific network"""
networks: List[Network] = []
filter = {}
if exegol_only:
filter["label"] = "source=exegol"
try:
networks = cls.__client.networks.list(names=network_name, filters=filter)
except APIError as e:
raise e
except ReadTimeout:
logger.critical("Received a timeout error, Docker is busy... Unable to enumerate volume, retry later.")
for net in networks:
# Search for an exact match
if net.name == network_name:
return net
return None

@classmethod
def networkExist(cls, network_name: str) -> bool:
"""Return True is the supplied network name exist"""
return cls.getNetwork(network_name) is not None

@classmethod
def createNetwork(cls, network_name: str, driver: str) -> bool:
"""Create a new exegol network"""
# ip_pool = IPAMPool()
# config = IPAMConfig(pool_configs=[ip_pool])
# TODO use custom network range
try:
network: Network = cls.__client.networks.create(name=network_name, driver=driver, labels={"source": "exegol"}, check_duplicate=True) # ipam=config
return True
except APIError as e:
if e.status_code == 409:
logger.error("This network already exist.")
return False
raise e
except ReadTimeout:
logger.critical("Received a timeout error, Docker is busy... Unable to enumerate volume, retry later.")
return False

@classmethod
def removeNetwork(cls, network_name: Optional[str] = None, network: Optional[Network] = None) -> bool:
"""Remove exegol network"""
if network is None:
if network_name is None:
raise ValueError("One of the parameter must be supplied.")
elif network_name in ExegolNetwork.DEFAULT_DOCKER_NETWORK:
# Default docker driver cannot be deleted
return False
network = cls.getNetwork(network_name, exegol_only=True)

if network is not None:
if network.name in ExegolNetwork.DEFAULT_DOCKER_NETWORK:
# Default docker driver cannot be deleted
return False
try:
network.remove()
logger.success(f"Dedicated network successfully removed.")
return True
except NotFound:
logger.verbose(f"The dedicated network {network.name} was already removed.")
except APIError as e:
logger.error(f"The associated dedicated network cannot be automatically removed. "
f"You have to delete it manually ({network.name}). Error: {e.explanation}")
else:
logger.info(f"The network {network_name} will not be deleted. Only exegol network can be automatically deleted.")
return False

# # # Image Section # # #

@classmethod
Expand Down Expand Up @@ -589,8 +697,8 @@ def __remove_image(cls, image_name: str) -> bool:
else:
logger.debug(f"Unexpected error after timeout: {err}")
except ReadTimeout:
wait_time = wait_time + wait_time*i
logger.info(f"Docker timeout again ({i+1}/{max_retry}). Next retry in {wait_time} seconds...")
wait_time = wait_time + wait_time * i
logger.info(f"Docker timeout again ({i + 1}/{max_retry}). Next retry in {wait_time} seconds...")
sleep(wait_time) # Wait x seconds before retry
logger.error(f"The deletion of the image '{image_name}' has timeout, the deletion may be incomplete.")
return False
Expand Down

0 comments on commit 4b52c55

Please sign in to comment.